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', () => {