diff --git a/apps/compliance-portal/index.html b/apps/compliance-portal/index.html index ef9b5abc1..83ed320a3 100644 --- a/apps/compliance-portal/index.html +++ b/apps/compliance-portal/index.html @@ -1,5 +1,5 @@ - + diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-114x114.png b/apps/compliance-portal/public/favicons/apple-touch-icon-114x114.png new file mode 100644 index 000000000..444ea14a8 Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-114x114.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-120x120.png b/apps/compliance-portal/public/favicons/apple-touch-icon-120x120.png new file mode 100644 index 000000000..def547fc9 Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-120x120.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-144x144.png b/apps/compliance-portal/public/favicons/apple-touch-icon-144x144.png new file mode 100644 index 000000000..7149269f0 Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-144x144.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-152x152.png b/apps/compliance-portal/public/favicons/apple-touch-icon-152x152.png new file mode 100644 index 000000000..26b41822b Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-152x152.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-57x57.png b/apps/compliance-portal/public/favicons/apple-touch-icon-57x57.png new file mode 100644 index 000000000..f53ce4bcf Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-57x57.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-60x60.png b/apps/compliance-portal/public/favicons/apple-touch-icon-60x60.png new file mode 100644 index 000000000..6209814a1 Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-60x60.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-72x72.png b/apps/compliance-portal/public/favicons/apple-touch-icon-72x72.png new file mode 100644 index 000000000..b59da6556 Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-72x72.png differ diff --git a/apps/compliance-portal/public/favicons/apple-touch-icon-76x76.png b/apps/compliance-portal/public/favicons/apple-touch-icon-76x76.png new file mode 100644 index 000000000..c40e2387c Binary files /dev/null and b/apps/compliance-portal/public/favicons/apple-touch-icon-76x76.png differ diff --git a/apps/compliance-portal/public/favicons/favicon-128.png b/apps/compliance-portal/public/favicons/favicon-128.png new file mode 100644 index 000000000..015f14572 Binary files /dev/null and b/apps/compliance-portal/public/favicons/favicon-128.png differ diff --git a/apps/compliance-portal/public/favicons/favicon-16x16.png b/apps/compliance-portal/public/favicons/favicon-16x16.png new file mode 100644 index 000000000..ab74a6336 Binary files /dev/null and b/apps/compliance-portal/public/favicons/favicon-16x16.png differ diff --git a/apps/compliance-portal/public/favicons/favicon-196x196.png b/apps/compliance-portal/public/favicons/favicon-196x196.png new file mode 100644 index 000000000..f9a182d38 Binary files /dev/null and b/apps/compliance-portal/public/favicons/favicon-196x196.png differ diff --git a/apps/compliance-portal/public/favicons/favicon-32x32.png b/apps/compliance-portal/public/favicons/favicon-32x32.png new file mode 100644 index 000000000..0d5835f0e Binary files /dev/null and b/apps/compliance-portal/public/favicons/favicon-32x32.png differ diff --git a/apps/compliance-portal/public/favicons/favicon-96x96.png b/apps/compliance-portal/public/favicons/favicon-96x96.png new file mode 100644 index 000000000..d45c99538 Binary files /dev/null and b/apps/compliance-portal/public/favicons/favicon-96x96.png differ diff --git a/apps/compliance-portal/public/favicons/favicon.ico b/apps/compliance-portal/public/favicons/favicon.ico new file mode 100644 index 000000000..d3925d54e Binary files /dev/null and b/apps/compliance-portal/public/favicons/favicon.ico differ diff --git a/apps/compliance-portal/public/favicons/mstile-144x144.png b/apps/compliance-portal/public/favicons/mstile-144x144.png new file mode 100644 index 000000000..7149269f0 Binary files /dev/null and b/apps/compliance-portal/public/favicons/mstile-144x144.png differ diff --git a/apps/compliance-portal/public/favicons/mstile-150x150.png b/apps/compliance-portal/public/favicons/mstile-150x150.png new file mode 100644 index 000000000..a1b242663 Binary files /dev/null and b/apps/compliance-portal/public/favicons/mstile-150x150.png differ diff --git a/apps/compliance-portal/public/favicons/mstile-310x150.png b/apps/compliance-portal/public/favicons/mstile-310x150.png new file mode 100644 index 000000000..33898ee97 Binary files /dev/null and b/apps/compliance-portal/public/favicons/mstile-310x150.png differ diff --git a/apps/compliance-portal/public/favicons/mstile-310x310.png b/apps/compliance-portal/public/favicons/mstile-310x310.png new file mode 100644 index 000000000..7e2b51750 Binary files /dev/null and b/apps/compliance-portal/public/favicons/mstile-310x310.png differ diff --git a/apps/compliance-portal/public/favicons/mstile-70x70.png b/apps/compliance-portal/public/favicons/mstile-70x70.png new file mode 100644 index 000000000..015f14572 Binary files /dev/null and b/apps/compliance-portal/public/favicons/mstile-70x70.png differ diff --git a/apps/compliance-portal/src/_locales/ko-KR.json b/apps/compliance-portal/src/_locales/ko-KR.json index f1515f34d..7052a35f6 100644 --- a/apps/compliance-portal/src/_locales/ko-KR.json +++ b/apps/compliance-portal/src/_locales/ko-KR.json @@ -98,9 +98,9 @@ "label": "언어", "updateFailed": "언어를 업데이트하지 못했습니다", "mismatch": { - "message": "{{language}}(으)로 이 페이지를 보고 있습니다.", - "switchToMine": "{{language}}(으)로 전환", - "useThis": "{{language}}을(를) 내 언어로 사용", + "message": "이 페이지의 표시 언어는 {{language}}입니다.", + "switchToMine": "내 언어({{language}})로 전환", + "useThis": "표시 언어({{language}})를 내 언어로 사용", "dismiss": "닫기" } } diff --git a/apps/compliance-portal/vite.config.ts b/apps/compliance-portal/vite.config.ts index 212cb6a36..f5f39fb5e 100644 --- a/apps/compliance-portal/vite.config.ts +++ b/apps/compliance-portal/vite.config.ts @@ -39,7 +39,7 @@ function goHtmlTemplateDevDefaults(): Plugin { handler(html) { return html .replace( - /\{\{if \.HtmlLang\}\}\{\{\.HtmlLang\}\}\{\{else\}\}en\{\{end\}\}/g, + /\{\{if \.HTMLLang\}\}\{\{\.HTMLLang\}\}\{\{else\}\}en\{\{end\}\}/g, "en", ) .replace( diff --git a/pkg/server/api/complianceportal/sni_middleware.go b/pkg/server/api/complianceportal/sni_middleware.go index 8e1f796ad..66f52929a 100644 --- a/pkg/server/api/complianceportal/sni_middleware.go +++ b/pkg/server/api/complianceportal/sni_middleware.go @@ -97,9 +97,11 @@ func NewSNIMiddleware(visitorSvc *visitor.Service) func(next http.Handler) http. } } + // Origin only — consumers append their own paths (SEO, sitemap, + // robots, brand assets, OAuth). Including r.URL.Path here would + // duplicate the route (e.g. /fr/documents/fr/documents). baseURL := &url.URL{ Host: r.Host, - Path: r.URL.Path, Scheme: "https", } baseURLString := baseURL.String() diff --git a/pkg/server/api/complianceportal/v1/mux.go b/pkg/server/api/complianceportal/v1/mux.go index 2d3270679..21592c19f 100644 --- a/pkg/server/api/complianceportal/v1/mux.go +++ b/pkg/server/api/complianceportal/v1/mux.go @@ -134,8 +134,6 @@ func compliancePageHeadData() HeadDataFunc { } compliancePageBaseURL := complianceportal.CompliancePortalBaseURLFromContext(r.Context()) - pageBase := ref.UnrefOrZero(compliancePageBaseURL) - htmlLang, canonical, hreflang := SEOFromRequest(r, pageBase) description := tc.Title if tc.Description != nil && *tc.Description != "" { @@ -143,12 +141,18 @@ func compliancePageHeadData() HeadDataFunc { } headData := HeadData{ - Title: tc.Title, - Description: description, - OGURL: pageBase, - HtmlLang: htmlLang, - CanonicalURL: canonical, - Hreflang: hreflang, + Title: tc.Title, + Description: description, + } + + // Canonical / hreflang / og:url must be absolute. Without a portal + // origin in context, skip them rather than emit relative paths. + if pageBase := ref.UnrefOrZero(compliancePageBaseURL); pageBase != "" { + htmlLang, canonical, hreflang := SEOFromRequest(r, pageBase) + headData.HTMLLang = htmlLang + headData.OGURL = canonical + headData.CanonicalURL = canonical + headData.Hreflang = hreflang } if tc.LogoFileID != nil && compliancePageBaseURL != nil { diff --git a/pkg/server/api/complianceportal/v1/seo.go b/pkg/server/api/complianceportal/v1/seo.go index 7399bcc0d..eabc41d6f 100644 --- a/pkg/server/api/complianceportal/v1/seo.go +++ b/pkg/server/api/complianceportal/v1/seo.go @@ -34,7 +34,10 @@ const defaultCompliancePortalLocale = "en" // SEOFromRequest derives html lang, a self-referencing canonical URL, and // hreflang alternates (including x-default → English) for the SPA shell. // Portals are host-routed (slug subdomain / custom domain); the request path -// is already relative to the portal root. +// is already relative to the portal root. pageBaseURL is normalized to its +// origin so a stale path in the base cannot double the route. When pageBaseURL +// has no usable origin, canonical and hreflang are left empty so callers do +// not emit relative SEO links. func SEOFromRequest(r *http.Request, pageBaseURL string) (htmlLang, canonical string, hreflang []HreflangLink) { pathname := r.URL.Path if pathname == "" { @@ -42,9 +45,16 @@ func SEOFromRequest(r *http.Request, pageBaseURL string) (htmlLang, canonical st } locale, rest := splitLocaleFromAppPath(pathname) - htmlLang = locale - canonical = localizedPageURL(pageBaseURL, locale, rest) + + origin := portalOrigin(pageBaseURL) + if origin == "" { + // No absolute origin — return lang only; callers must not emit + // relative canonical/hreflang URLs. + return htmlLang, "", nil + } + + canonical = localizedPageURL(origin, locale, rest) locales := iam.SupportedIdentityLocales @@ -52,18 +62,29 @@ func SEOFromRequest(r *http.Request, pageBaseURL string) (htmlLang, canonical st for _, loc := range locales { hreflang = append(hreflang, HreflangLink{ Lang: loc, - Href: localizedPageURL(pageBaseURL, loc, rest), + Href: localizedPageURL(origin, loc, rest), }) } hreflang = append(hreflang, HreflangLink{ Lang: "x-default", - Href: localizedPageURL(pageBaseURL, defaultCompliancePortalLocale, rest), + Href: localizedPageURL(origin, defaultCompliancePortalLocale, rest), }) return htmlLang, canonical, hreflang } +// portalOrigin returns scheme://host from pageBaseURL, dropping any path, +// query, or fragment. Callers may pass a full request URL by mistake. +func portalOrigin(pageBaseURL string) string { + parsed, err := url.Parse(pageBaseURL) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return strings.TrimRight(pageBaseURL, "/") + } + + return (&url.URL{Scheme: parsed.Scheme, Host: parsed.Host}).String() +} + func splitLocaleFromAppPath(appPath string) (locale, rest string) { segments := strings.Split(strings.Trim(appPath, "/"), "/") if len(segments) == 0 || segments[0] == "" { diff --git a/pkg/server/api/complianceportal/v1/seo_test.go b/pkg/server/api/complianceportal/v1/seo_test.go index 4995d112c..06b203fa1 100644 --- a/pkg/server/api/complianceportal/v1/seo_test.go +++ b/pkg/server/api/complianceportal/v1/seo_test.go @@ -82,3 +82,45 @@ func TestSEOFromRequest_EscapesPathSegments(t *testing.T) { ) assert.Equal(t, "https://acme.probopage.localhost/en/docs/foo%20bar", canonical) } + +func TestSEOFromRequest_StripsPathFromBaseURL(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest( + http.MethodGet, + "https://trust.acme.com/fr/documents", + nil, + ) + require.NoError(t, err) + + // SNI middleware once stored scheme+host+path; SEO must not double it. + _, canonical, hreflang := complianceportal_v1.SEOFromRequest( + req, + "https://trust.acme.com/fr/documents", + ) + assert.Equal(t, "https://trust.acme.com/fr/documents", canonical) + + var enHref string + for _, link := range hreflang { + if link.Lang == "en" { + enHref = link.Href + } + } + assert.Equal(t, "https://trust.acme.com/en/documents", enHref) +} + +func TestSEOFromRequest_EmptyBaseURLOmitsLinks(t *testing.T) { + t.Parallel() + + req, err := http.NewRequest( + http.MethodGet, + "https://trust.acme.com/fr/documents", + nil, + ) + require.NoError(t, err) + + lang, canonical, hreflang := complianceportal_v1.SEOFromRequest(req, "") + assert.Equal(t, "fr", lang) + assert.Empty(t, canonical) + assert.Nil(t, hreflang) +} diff --git a/pkg/server/api/complianceportal/v1/spa.go b/pkg/server/api/complianceportal/v1/spa.go index e2db108b6..4c636ad91 100644 --- a/pkg/server/api/complianceportal/v1/spa.go +++ b/pkg/server/api/complianceportal/v1/spa.go @@ -37,7 +37,7 @@ type ( Description string OGURL string FaviconURL string - HtmlLang string + HTMLLang string CanonicalURL string Hreflang []HreflangLink }