diff --git a/package.json b/package.json
index d722139d32..ca8ec8e1ff 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
"build:subnets": "node scripts/cmc.subnets.mjs && npm run format",
"build:passkey:aaguids": "node scripts/passkey.aaguids.mjs && npm run format",
"build:csp": "node scripts/build.csp.mjs",
- "build:post-process": "npm run build:csp",
+ "build:seo": "node scripts/build.seo.mjs",
+ "build:post-process": "npm run build:seo && npm run build:csp",
"dev": "npm run i18n && vite dev",
"dev:skylab": "npm run i18n && vite dev --mode skylab",
"build": "npm run build:frontend",
diff --git a/scripts/build.seo.mjs b/scripts/build.seo.mjs
new file mode 100644
index 0000000000..e53ddb40e3
--- /dev/null
+++ b/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(), 'build');
+const SITE_ROOT_CANONICAL = 'https://console.juno.build';
+
+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((htmlFile) => updateCanonical(htmlFile));