diff --git a/src/steps/render.js b/src/steps/render.js index 2532f589..150ba259 100644 --- a/src/steps/render.js +++ b/src/steps/render.js @@ -41,6 +41,39 @@ function sanitizeJsonLd(jsonLd) { return JSON.stringify(JSON.parse(sanitizedJsonLd.trim()), null, 2); } +function getCurrentLang(canonical, langs) { + const { pathname } = canonical; + // find the longest matching prefix + const { lang: currentLang, prefix: currentPrefix } = langs.reduce((acc, lang) => { + const { prefix } = lang; + if ((pathname === prefix || pathname.startsWith(`${prefix}/`)) // path matches prefix + && (!acc || prefix.length > acc.prefix.length)) { // prefix is longer than previous + return lang; + } + return acc; + }); + return { + currentLang, + currentPrefix, + }; +} + +function getLangHref(canonical, currentPrefix, prefix) { + if (currentPrefix === prefix) { + // current prefix is identical to prefix -> canonical + return canonical.href; + } else { + const { pathname } = canonical; + if (!currentPrefix) { + // current prefix empty -> prepend prefix + return new URL(`${prefix}${pathname}`, canonical).href; + } else { + // replace current prefix with prefix + return new URL(pathname.replace(currentPrefix, prefix), canonical).href; + } + } +} + /** * @type PipelineStep * @param {PipelineState} state @@ -116,6 +149,32 @@ export default async function render(state, req, res) { $head.children.push(...$headHtml.children); } + // language support + const { langs, defaultLang } = state.config?.features?.['language-support'] || {}; + if (Array.isArray(langs)) { + const canonicalUrl = new URL(meta.canonical); + const { currentLang, currentPrefix } = getCurrentLang(canonicalUrl, langs); + if (currentLang) { + // set html lang if not already set via metadata + if (!htmlLang) { + htmlLang = currentLang; + } + // inject hreflang links + langs.forEach(({ lang, prefix }) => { + const href = getLangHref(canonicalUrl, currentPrefix, prefix); + $head.children.push(createElement('link', 'rel', 'alternate', 'hreflang', lang, 'href', href)); + }); + // write x-default hreflang link if default lang exists + if (defaultLang) { + const { lang, prefix } = langs.find((l) => l.lang === defaultLang); + if (lang) { + const href = getLangHref(canonicalUrl, currentPrefix, prefix); + $head.children.push(createElement('link', 'rel', 'alternate', 'hreflang', 'x-default', 'href', href)); + } + } + } + } + res.document = { type: 'root', children: [ diff --git a/test/fixtures/content/en/uk/page-metadata-language-support.html b/test/fixtures/content/en/uk/page-metadata-language-support.html new file mode 100644 index 00000000..9e60c9da --- /dev/null +++ b/test/fixtures/content/en/uk/page-metadata-language-support.html @@ -0,0 +1,32 @@ + + + Home | Helix Project Boilerplate + + + + + + + + + + + + + + + + + + + +
+
+
+

HTML lang test

+

This is great.

+
+
+ + + \ No newline at end of file diff --git a/test/fixtures/content/en/uk/page-metadata-language-support.md b/test/fixtures/content/en/uk/page-metadata-language-support.md new file mode 100644 index 00000000..ebe144dc --- /dev/null +++ b/test/fixtures/content/en/uk/page-metadata-language-support.md @@ -0,0 +1,9 @@ +# HTML lang test + +This is great. + ++-------------------------------------------------------------------------------------------+ +| Metadata | ++================+==========================================================================+ +| title | Home \| Helix Project Boilerplate | ++----------------+--------------------------------------------------------------------------+ diff --git a/test/fixtures/content/page-metadata-language-support.html b/test/fixtures/content/page-metadata-language-support.html new file mode 100644 index 00000000..ebaea24d --- /dev/null +++ b/test/fixtures/content/page-metadata-language-support.html @@ -0,0 +1,32 @@ + + + Home | Helix Project Boilerplate + + + + + + + + + + + + + + + + + + + +
+
+
+

HTML lang test

+

This is great.

+
+
+ + + \ No newline at end of file diff --git a/test/fixtures/content/page-metadata-language-support.md b/test/fixtures/content/page-metadata-language-support.md new file mode 100644 index 00000000..ebe144dc --- /dev/null +++ b/test/fixtures/content/page-metadata-language-support.md @@ -0,0 +1,9 @@ +# HTML lang test + +This is great. + ++-------------------------------------------------------------------------------------------+ +| Metadata | ++================+==========================================================================+ +| title | Home \| Helix Project Boilerplate | ++----------------+--------------------------------------------------------------------------+ diff --git a/test/rendering.test.js b/test/rendering.test.js index c2136020..3a4d13bf 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -167,6 +167,33 @@ const DEFAULT_CONFIG_EMPTY = { }, }; +const CONFIG_LANGUAGE_SUPPORT = { + ...DEFAULT_CONFIG_EMPTY, + features: { + 'language-support': { + defaultLang: 'en-US', + langs: [ + { + lang: 'en-US', + prefix: '', + }, + { + lang: 'en-GB', + prefix: '/en/uk', + }, + { + lang: 'fr', + prefix: '/fr', + }, + { + lang: 'de-DE', + prefix: '/de', + }, + ], + }, + }, +}; + describe('Rendering', () => { let loader; let config; @@ -535,6 +562,16 @@ describe('Rendering', () => { config = DEFAULT_CONFIG_EMPTY; await testRender('page-metadata-htmllang-invalid', ':scope'); }); + + it('detects current lang and injects html lang and hreflang links based on feature config', async () => { + config = CONFIG_LANGUAGE_SUPPORT; + await testRender('en/uk/page-metadata-language-support', ':scope'); + }); + + it('detects current lang with empty prefix', async () => { + config = CONFIG_LANGUAGE_SUPPORT; + await testRender('page-metadata-language-support', ':scope'); + }); }); describe('Miscellaneous', () => {