{copy.hero.verdictTitle}
diff --git a/marketing-site/features/comparison/dokku/page-content.tsx b/marketing-site/features/comparison/dokku/page-content.tsx
index e9ce4bbc..44bd15ce 100644
--- a/marketing-site/features/comparison/dokku/page-content.tsx
+++ b/marketing-site/features/comparison/dokku/page-content.tsx
@@ -1,5 +1,6 @@
import { DEFAULT_LOCALE, docsUrl, type Locale } from "@/i18n/config";
import { getMessages, messageList } from "@/i18n/messages";
+import routes from "@/routes.json";
import {
ChoiceGrid,
ComparePage,
@@ -23,7 +24,7 @@ export function DokkuComparisonPage({
const copy = getMessages(locale).comparisonPages.dokku;
const sources = copy.sourceLabels;
return (
-
+
{copy.hero.verdictTitle}
diff --git a/marketing-site/features/comparison/dokploy/page-content.tsx b/marketing-site/features/comparison/dokploy/page-content.tsx
index db9cf92c..347a1384 100644
--- a/marketing-site/features/comparison/dokploy/page-content.tsx
+++ b/marketing-site/features/comparison/dokploy/page-content.tsx
@@ -1,5 +1,6 @@
import { DEFAULT_LOCALE, docsUrl, type Locale } from "@/i18n/config";
import { getMessages, messageList } from "@/i18n/messages";
+import routes from "@/routes.json";
import {
ChoiceGrid,
ComparePage,
@@ -23,7 +24,7 @@ export function DokployComparisonPage({
const copy = getMessages(locale).comparisonPages.dokploy;
const sources = copy.sourceLabels;
return (
-
+
{copy.hero.verdictTitle}
diff --git a/marketing-site/features/comparison/page-content.tsx b/marketing-site/features/comparison/page-content.tsx
index eda9f9f5..7123d023 100644
--- a/marketing-site/features/comparison/page-content.tsx
+++ b/marketing-site/features/comparison/page-content.tsx
@@ -1,5 +1,6 @@
import { DEFAULT_LOCALE, docsUrl, type Locale } from "@/i18n/config";
import { getMessages } from "@/i18n/messages";
+import routes from "@/routes.json";
import {
ComparePage,
ComparisonPrinciples,
@@ -21,7 +22,7 @@ export function ComparisonHubPage({
const copy = getMessages(locale).comparisonPages.hub;
const sources = copy.sourceLabels;
return (
-
+
diff --git a/marketing-site/i18n/metadata.ts b/marketing-site/i18n/metadata.ts
index a4bbc73b..e1ea26b4 100644
--- a/marketing-site/i18n/metadata.ts
+++ b/marketing-site/i18n/metadata.ts
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
+import routes from "../routes.json";
import { getFlatMessages, type MessageKey } from "./messages";
import {
DEFAULT_LOCALE,
@@ -11,27 +12,27 @@ const SITE = "https://caprover.com";
const PAGE_METADATA = {
home: {
- path: "/",
+ path: routes.home.path,
titleKey: "homepage.metadata.title",
descriptionKey: "homepage.metadata.description",
},
comparisonHub: {
- path: "/compare/",
+ path: routes.comparisonHub.path,
titleKey: "comparisonPages.hub.metadata.title",
descriptionKey: "comparisonPages.hub.metadata.description",
},
coolify: {
- path: "/compare/coolify/",
+ path: routes.coolify.path,
titleKey: "comparisonPages.coolify.metadata.title",
descriptionKey: "comparisonPages.coolify.metadata.description",
},
dokploy: {
- path: "/compare/dokploy/",
+ path: routes.dokploy.path,
titleKey: "comparisonPages.dokploy.metadata.title",
descriptionKey: "comparisonPages.dokploy.metadata.description",
},
dokku: {
- path: "/compare/dokku/",
+ path: routes.dokku.path,
titleKey: "comparisonPages.dokku.metadata.title",
descriptionKey: "comparisonPages.dokku.metadata.description",
},
diff --git a/marketing-site/routes.json b/marketing-site/routes.json
new file mode 100644
index 00000000..8a91e8fb
--- /dev/null
+++ b/marketing-site/routes.json
@@ -0,0 +1,22 @@
+{
+ "home": {
+ "path": "/",
+ "sitemapPriority": 1
+ },
+ "comparisonHub": {
+ "path": "/compare/",
+ "sitemapPriority": 0.7
+ },
+ "coolify": {
+ "path": "/compare/coolify/",
+ "sitemapPriority": 0.7
+ },
+ "dokploy": {
+ "path": "/compare/dokploy/",
+ "sitemapPriority": 0.7
+ },
+ "dokku": {
+ "path": "/compare/dokku/",
+ "sitemapPriority": 0.6
+ }
+}
diff --git a/marketing-site/tests/static-export.test.mjs b/marketing-site/tests/static-export.test.mjs
index 41bbcd5e..2ee0d281 100644
--- a/marketing-site/tests/static-export.test.mjs
+++ b/marketing-site/tests/static-export.test.mjs
@@ -10,13 +10,18 @@ const expectedAssets = [
"caprover-workflow.png",
"slack-icon.png",
];
-const routes = [
- { path: "/", titleKey: "homepage.hero.titleLine1" },
- { path: "/compare/", titleKey: "comparisonPages.hub.hero.title" },
- { path: "/compare/coolify/", titleKey: "comparisonPages.coolify.hero.title" },
- { path: "/compare/dokploy/", titleKey: "comparisonPages.dokploy.hero.title" },
- { path: "/compare/dokku/", titleKey: "comparisonPages.dokku.hero.title" },
-];
+const routeManifest = JSON.parse(await readFile("routes.json", "utf8"));
+const routeTitleKeys = {
+ home: "homepage.hero.titleLine1",
+ comparisonHub: "comparisonPages.hub.hero.title",
+ coolify: "comparisonPages.coolify.hero.title",
+ dokploy: "comparisonPages.dokploy.hero.title",
+ dokku: "comparisonPages.dokku.hero.title",
+};
+const routes = Object.entries(routeManifest).map(([name, route]) => ({
+ path: route.path,
+ titleKey: routeTitleKeys[name],
+}));
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const assetPath = "homepage-assets";
const locales = JSON.parse(await readFile("../content/locales.json", "utf8"));
@@ -93,11 +98,26 @@ test("exports complete localized website routes", async () => {
});
test("keeps comparison regressions covered across locales", async () => {
- const homepage = await readFile("out/index.html", "utf8");
- const hub = await readFile("out/compare/index.html", "utf8");
- const coolify = await readFile("out/compare/coolify/index.html", "utf8");
- const dokploy = await readFile("out/compare/dokploy/index.html", "utf8");
- const dokku = await readFile("out/compare/dokku/index.html", "utf8");
+ const homepage = await readFile(
+ `out${routeManifest.home.path}index.html`,
+ "utf8",
+ );
+ const hub = await readFile(
+ `out${routeManifest.comparisonHub.path}index.html`,
+ "utf8",
+ );
+ const coolify = await readFile(
+ `out${routeManifest.coolify.path}index.html`,
+ "utf8",
+ );
+ const dokploy = await readFile(
+ `out${routeManifest.dokploy.path}index.html`,
+ "utf8",
+ );
+ const dokku = await readFile(
+ `out${routeManifest.dokku.path}index.html`,
+ "utf8",
+ );
const sitemap = await readFile("out/sitemap.xml", "utf8");
assert.doesNotMatch(homepage, /href=["'][^"']*\/compare\/?["']/);
diff --git a/scripts/capture-docs-ui.js b/scripts/capture-docs-ui.js
index 190d2d6f..4c20cbfb 100644
--- a/scripts/capture-docs-ui.js
+++ b/scripts/capture-docs-ui.js
@@ -1,5 +1,6 @@
const fs = require("fs");
const path = require("path");
+const { fromDocusaurusCatalogs } = require("./docs-ui-catalogs");
const root = path.resolve(__dirname, "..");
const locales = require(path.join(root, "content", "locales.json"));
@@ -25,39 +26,10 @@ function readCatalog(...parts) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
-function message(catalog, key) {
- if (!catalog[key] || typeof catalog[key].message !== "string") {
- throw new Error(`Generated documentation UI key is missing: ${key}`);
- }
- return catalog[key].message;
-}
-
const docs = readCatalog("docusaurus-plugin-content-docs", "current.json");
const navbar = readCatalog("docusaurus-theme-classic", "navbar.json");
const footer = readCatalog("docusaurus-theme-classic", "footer.json");
-const result = {
- "version.label": message(docs, "version.label"),
- "navbar.Docs": message(navbar, "item.label.Docs"),
- "navbar.GitHub": message(navbar, "item.label.GitHub"),
- "navbar.Slack Group": message(navbar, "item.label.Slack Group"),
- "footer.column.Docs": message(footer, "link.title.Docs"),
- "footer.column.Community": message(footer, "link.title.Community"),
- "footer.column.More": message(footer, "link.title.More"),
- "footer.Getting Started": message(
- footer,
- "link.item.label.Getting Started",
- ),
- "footer.X": message(footer, "link.item.label.X"),
- "footer.Slack Group": message(footer, "link.item.label.Slack Group"),
- "footer.GitHub": message(footer, "link.item.label.GitHub"),
-};
-
-for (const [key, value] of Object.entries(docs)) {
- const prefix = "sidebar.docs.category.";
- if (key.startsWith(prefix)) {
- result[`sidebar.${key.slice(prefix.length)}`] = value.message;
- }
-}
+const result = fromDocusaurusCatalogs({ docs, navbar, footer });
fs.writeFileSync(canonical, `${JSON.stringify(result, null, 2)}\n`);
console.log(`Updated ${path.relative(root, canonical)}`);
diff --git a/scripts/compose-site.mjs b/scripts/compose-site.mjs
index 72de50b2..162b2cef 100644
--- a/scripts/compose-site.mjs
+++ b/scripts/compose-site.mjs
@@ -17,17 +17,14 @@ const root = process.cwd();
const docsSite = path.join(root, "docs-site/build");
const marketingSite = path.join(root, "marketing-site/out");
const combinedSite = path.join(root, "build/combined-site");
+const cnameSource = path.join(root, "docs-site/static/CNAME");
const locales = JSON.parse(
await readFile(path.join(root, "content/locales.json"), "utf8"),
).filter((locale) => locale.enabled);
const defaultLocale = locales.find((locale) => locale.default);
-const marketingRoutes = [
- "",
- "compare",
- "compare/coolify",
- "compare/dokploy",
- "compare/dokku",
-];
+const marketingRoutes = Object.values(
+ JSON.parse(await readFile(path.join(root, "marketing-site/routes.json"), "utf8")),
+).map((route) => route.path.replace(/^\//, "").replace(/\/$/, ""));
assert(defaultLocale, "An enabled default locale is required");
@@ -127,6 +124,7 @@ await Promise.all([
),
requirePath(path.join(docsSite, "img/logo.png")),
requirePath(path.join(docsSite, "CNAME")),
+ requirePath(cnameSource),
...locales.flatMap((locale) =>
marketingRoutes.map((route) => requirePath(marketingRouteFile(locale, route))),
),
@@ -139,7 +137,7 @@ await Promise.all([
requireNoCollision("compare"),
]);
-const originalCname = await readFile(path.join(docsSite, "CNAME"));
+const originalCname = await readFile(cnameSource);
assert.equal(originalCname.toString().trim(), "caprover.com");
const [docsBefore, imagesBefore] = await Promise.all([
diff --git a/scripts/docs-ui-catalogs.js b/scripts/docs-ui-catalogs.js
new file mode 100644
index 00000000..62b7e991
--- /dev/null
+++ b/scripts/docs-ui-catalogs.js
@@ -0,0 +1,60 @@
+const docsUiMappings = [
+ ["docs", "version.label", "version.label"],
+ ["navbar", "navbar.Docs", "item.label.Docs"],
+ ["navbar", "navbar.GitHub", "item.label.GitHub"],
+ ["navbar", "navbar.Slack Group", "item.label.Slack Group"],
+ ["footer", "footer.column.Docs", "link.title.Docs"],
+ ["footer", "footer.column.Community", "link.title.Community"],
+ ["footer", "footer.column.More", "link.title.More"],
+ ["footer", "footer.Getting Started", "link.item.label.Getting Started"],
+ ["footer", "footer.X", "link.item.label.X"],
+ ["footer", "footer.Slack Group", "link.item.label.Slack Group"],
+ ["footer", "footer.GitHub", "link.item.label.GitHub"],
+];
+
+const docsUiSidebarPrefix = "sidebar.";
+const docusaurusSidebarPrefix = "sidebar.docs.category.";
+
+function toDocusaurusCatalogs(docsUi) {
+ const catalogs = { docs: {}, navbar: {}, footer: {} };
+
+ for (const [catalog, docsUiKey, docusaurusKey] of docsUiMappings) {
+ catalogs[catalog][docusaurusKey] = docsUi[docsUiKey];
+ }
+
+ for (const [key, value] of Object.entries(docsUi)) {
+ if (key.startsWith(docsUiSidebarPrefix)) {
+ catalogs.docs[
+ `${docusaurusSidebarPrefix}${key.slice(docsUiSidebarPrefix.length)}`
+ ] = value;
+ }
+ }
+
+ return catalogs;
+}
+
+function fromDocusaurusCatalogs(catalogs) {
+ const docsUi = {};
+
+ function message(catalog, key) {
+ if (!catalog[key] || typeof catalog[key].message !== "string") {
+ throw new Error(`Generated documentation UI key is missing: ${key}`);
+ }
+ return catalog[key].message;
+ }
+
+ for (const [catalog, docsUiKey, docusaurusKey] of docsUiMappings) {
+ docsUi[docsUiKey] = message(catalogs[catalog], docusaurusKey);
+ }
+
+ for (const [key, value] of Object.entries(catalogs.docs)) {
+ if (key.startsWith(docusaurusSidebarPrefix)) {
+ docsUi[`${docsUiSidebarPrefix}${key.slice(docusaurusSidebarPrefix.length)}`] =
+ value.message;
+ }
+ }
+
+ return docsUi;
+}
+
+module.exports = { fromDocusaurusCatalogs, toDocusaurusCatalogs };
diff --git a/scripts/prepare-localized-content.js b/scripts/prepare-localized-content.js
index ae143f11..344b5afc 100644
--- a/scripts/prepare-localized-content.js
+++ b/scripts/prepare-localized-content.js
@@ -1,5 +1,6 @@
const fs = require("fs");
const path = require("path");
+const { toDocusaurusCatalogs } = require("./docs-ui-catalogs");
const root = path.resolve(__dirname, "..");
const contentRoot = path.join(root, "content");
@@ -263,43 +264,22 @@ for (const locale of enabledLocales) {
);
const docsUi = require(path.join(localeRoot, "docs-ui.json"));
+ const catalogs = toDocusaurusCatalogs(docsUi);
writeCatalog(
path.join(
localeI18n,
"docusaurus-plugin-content-docs",
"current.json",
),
- {
- "version.label": docsUi["version.label"],
- ...Object.fromEntries(
- Object.entries(docsUi)
- .filter(([key]) => key.startsWith("sidebar."))
- .map(([key, value]) => [
- `sidebar.docs.category.${key.slice("sidebar.".length)}`,
- value,
- ]),
- ),
- },
+ catalogs.docs,
);
writeCatalog(
path.join(localeI18n, "docusaurus-theme-classic", "navbar.json"),
- {
- "item.label.Docs": docsUi["navbar.Docs"],
- "item.label.GitHub": docsUi["navbar.GitHub"],
- "item.label.Slack Group": docsUi["navbar.Slack Group"],
- },
+ catalogs.navbar,
);
writeCatalog(
path.join(localeI18n, "docusaurus-theme-classic", "footer.json"),
- {
- "link.title.Docs": docsUi["footer.column.Docs"],
- "link.title.Community": docsUi["footer.column.Community"],
- "link.title.More": docsUi["footer.column.More"],
- "link.item.label.Getting Started": docsUi["footer.Getting Started"],
- "link.item.label.X": docsUi["footer.X"],
- "link.item.label.Slack Group": docsUi["footer.Slack Group"],
- "link.item.label.GitHub": docsUi["footer.GitHub"],
- },
+ catalogs.footer,
);
}
}
diff --git a/scripts/smoke-combined-site.mjs b/scripts/smoke-combined-site.mjs
index 3683f803..5fcc16bc 100644
--- a/scripts/smoke-combined-site.mjs
+++ b/scripts/smoke-combined-site.mjs
@@ -7,13 +7,9 @@ const siteRoot = path.resolve("build/combined-site");
const locales = JSON.parse(
await readFile(path.resolve("content/locales.json"), "utf8"),
).filter((locale) => locale.enabled);
-const localizedRoutes = [
- "",
- "compare/",
- "compare/coolify/",
- "compare/dokploy/",
- "compare/dokku/",
-];
+const marketingRoutes = JSON.parse(
+ await readFile(path.resolve("marketing-site/routes.json"), "utf8"),
+);
const contentTypes = new Map([
[".css", "text/css"],
@@ -78,7 +74,9 @@ try {
assert(docsTitle, `${locale.code} documentation title is missing`);
const outputPrefix = locale.pathPrefix ? `${locale.pathPrefix.slice(1)}/` : "";
const routeResponses = await Promise.all(
- localizedRoutes.map((route) => fetch(`${origin}/${outputPrefix}${route}index.html`)),
+ Object.values(marketingRoutes).map((route) =>
+ fetch(`${origin}/${outputPrefix}${route.path.slice(1)}index.html`),
+ ),
);
const docsResponse = await fetch(
`${origin}${locale.pathPrefix}/docs/get-started`,
@@ -105,60 +103,75 @@ try {
const nextAsset = homepage.match(/(?:href|src)=["'](\/_next\/[^"']+\.(?:css|js))["']/)?.[1];
assert(nextAsset, "Homepage does not reference a Next.js CSS or JavaScript asset");
- const checks = await Promise.all([
- fetch(`${origin}/.nojekyll`),
- fetch(`${origin}/docs/get-started.html`),
- fetch(`${origin}/docs/en/get-started.html`),
- fetch(`${origin}/docs/es-ES/get-started.html`),
- fetch(`${origin}/docs/get-started`),
- fetch(`${origin}/es-ES/docs/get-started`),
- fetch(`${origin}/en/index.html`),
- fetch(`${origin}/es-ES/index.html`),
- fetch(`${origin}/es-ES/compare/index.html`),
- fetch(`${origin}/es-ES/compare/coolify/index.html`),
- fetch(`${origin}/es-ES/compare/dokploy/index.html`),
- fetch(`${origin}/es-ES/compare/dokku/index.html`),
- fetch(`${origin}/homepage-assets/caprover-dashboard.png`),
- fetch(`${origin}${nextAsset}`),
- fetch(`${origin}/robots.txt`),
- ]);
-
- for (const response of checks) {
+ const responseUrls = {
+ noJekyll: `${origin}/.nojekyll`,
+ docsHtml: `${origin}/docs/get-started.html`,
+ legacyEnglishDocs: `${origin}/docs/en/get-started.html`,
+ legacySpanishDocs: `${origin}/docs/es-ES/get-started.html`,
+ docs: `${origin}/docs/get-started`,
+ spanishDocs: `${origin}/es-ES/docs/get-started`,
+ englishHomepageAlias: `${origin}/en/index.html`,
+ spanishHomepage: `${origin}/es-ES/index.html`,
+ spanishComparisonHub: `${origin}/es-ES${marketingRoutes.comparisonHub.path}index.html`,
+ spanishCoolifyComparison: `${origin}/es-ES${marketingRoutes.coolify.path}index.html`,
+ spanishDokployComparison: `${origin}/es-ES${marketingRoutes.dokploy.path}index.html`,
+ spanishDokkuComparison: `${origin}/es-ES${marketingRoutes.dokku.path}index.html`,
+ dashboardImage: `${origin}/homepage-assets/caprover-dashboard.png`,
+ nextAsset: `${origin}${nextAsset}`,
+ robots: `${origin}/robots.txt`,
+ };
+ const responses = Object.fromEntries(
+ await Promise.all(
+ Object.entries(responseUrls).map(async ([name, url]) => [name, await fetch(url)]),
+ ),
+ );
+
+ for (const response of Object.values(responses)) {
assert.equal(response.status, 200, `${response.url} did not return HTTP 200`);
}
- assert.equal(await checks[0].text(), "", ".nojekyll must be empty");
- const legacyEnglishDocsRedirect = await checks[2].text();
+ assert.equal(await responses.noJekyll.text(), "", ".nojekyll must be empty");
+ const legacyEnglishDocsRedirect = await responses.legacyEnglishDocs.text();
assert.match(legacyEnglishDocsRedirect, /window\.location\.replace\(["']\/docs\/get-started["']/);
- const legacySpanishDocsRedirect = await checks[3].text();
+ const legacySpanishDocsRedirect = await responses.legacySpanishDocs.text();
assert.match(legacySpanishDocsRedirect, /window\.location\.replace\(["']\/es-ES\/docs\/get-started["']/);
- const docs = await checks[4].text();
+ const docs = await responses.docs.text();
assert.match(docs, /Getting Started/i);
assert.match(docs, /href=["']?\/es-ES\/docs\/get-started(?:["'\s>])/);
- const spanishDocs = await checks[5].text();
+ const spanishDocs = await responses.spanishDocs.text();
assert.match(spanishDocs, /])/);
assert.match(spanishDocs, /Primeros pasos/);
assert.match(spanishDocs, /Conceptos básicos/);
assert.match(spanishDocs, /href=["']?\/docs\/get-started(?:["'\s>])/);
- const englishHomepageAlias = await checks[6].text();
+ const englishHomepageAlias = await responses.englishHomepageAlias.text();
assert.match(englishHomepageAlias, /Deploy apps\./);
assert.match(englishHomepageAlias, /rel=["']canonical["'][^>]+href=["']https:\/\/caprover\.com\/["']/);
- const spanishHomepage = await checks[7].text();
+ const spanishHomepage = await responses.spanishHomepage.text();
assert.match(spanishHomepage, /Despliega aplicaciones\./);
assert.match(spanishHomepage, /href=["']https:\/\/caprover\.com\/es-ES\/docs\/get-started["']/);
assert.match(spanishHomepage, /]+href=["']https:\/\/caprover\.com\/es-ES\/["']/);
- const spanishComparisonPages = await Promise.all(checks.slice(8, 12).map((response) => response.text()));
+ const spanishComparisonPages = await Promise.all(
+ [
+ responses.spanishComparisonHub,
+ responses.spanishCoolifyComparison,
+ responses.spanishDokployComparison,
+ responses.spanishDokkuComparison,
+ ].map((response) => response.text()),
+ );
assert.match(spanishComparisonPages[0], /Empieza de forma sencilla\./);
for (const page of spanishComparisonPages) {
assert.match(page, /\/es-ES\/compare\//);
assert.match(page, /\/es-ES\/docs\/get-started/);
}
- assert(Number(checks[12].headers.get("content-length") ?? 0) > 0 || (await checks[12].arrayBuffer()).byteLength > 0);
+ assert(
+ Number(responses.dashboardImage.headers.get("content-length") ?? 0) > 0 ||
+ (await responses.dashboardImage.arrayBuffer()).byteLength > 0,
+ );
assert.equal(
- await checks[14].text(),
+ await responses.robots.text(),
"User-agent: *\nAllow: /\nSitemap: https://caprover.com/sitemap.xml\n",
);