diff --git a/frontend/package.json b/frontend/package.json
index ab30bee186d..cbb5e8960db 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,8 +5,9 @@
"license": "SEE LICENSE IN LICENSE.md",
"scripts": {
"build:csp": "node scripts/build.csp.mjs",
+ "build:seo": "node scripts/build.seo.mjs",
"build:preload": "node scripts/build.preload.mjs",
- "build:post-process": "npm run build:csp",
+ "build:post-process": "npm run build:seo && npm run build:csp",
"i18n": "node scripts/i18n.types.js && prettier --write ./src/lib/types/i18n.d.ts",
"dev": "npm run i18n && vite dev",
"build": "npm run i18n && tsc --noEmit && vite build && npm run build:post-process",
diff --git a/frontend/scripts/build.seo.mjs b/frontend/scripts/build.seo.mjs
new file mode 100644
index 00000000000..657a17df747
--- /dev/null
+++ b/frontend/scripts/build.seo.mjs
@@ -0,0 +1,38 @@
+import { readFileSync, writeFileSync } from "node:fs";
+import { dirname, join, relative } from "node:path";
+import { findHtmlFiles } from "./build.utils.mjs";
+
+const OUTPUT_DIR = join(process.cwd(), "public");
+const SITE_ROOT_CANONICAL = "https://nns.ic0.app/";
+
+const updateCanonical = (htmlFilePath) => {
+ // 1. We determine the route based on the output
+ const routePath = dirname(relative(OUTPUT_DIR, htmlFilePath));
+
+ // 2. Build the effective canonical route
+ const canonicalPath = `${SITE_ROOT_CANONICAL}${routePath}/`;
+
+ // 2. Read content
+ let html = readFileSync(htmlFilePath, "utf-8");
+
+ // 3. Update canonical
+ html = html.replace(
+ ``,
+ ``
+ );
+
+ // 4. Update og:url to reflect the canonical
+ html = html.replace(
+ ``,
+ ``
+ );
+
+ // 5. Save the content with the updated canonical URL
+ writeFileSync(htmlFilePath, html);
+};
+
+// Do not replace canonical for root and 404 pages
+const filterSubPages = (htmlFile) => dirname(htmlFile) !== OUTPUT_DIR;
+
+const htmlFiles = findHtmlFiles().filter(filterSubPages);
+htmlFiles.forEach(updateCanonical);