Skip to content

Commit d8c53e0

Browse files
committed
feat: configure supported site languages
1 parent 693d6d8 commit d8c53e0

9 files changed

Lines changed: 241 additions & 9 deletions

File tree

defaults/functions/pages/[[path]].js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export async function onRequest(context) {
88
return context.next();
99
}
1010

11-
if (slug === "v8s.json" || slug === "redirect-targets.json") {
11+
if (slug === "v8s.json" || slug === "v8s-blocklist.json" || slug === "v8s-site-config.json") {
1212
return new Response("Not found", {
1313
status: 404,
1414
headers: {
@@ -151,7 +151,7 @@ function shouldBypassToAssets(slug) {
151151
if (slug === "_stats" || slug.startsWith("_stats/")) return true;
152152
if (slug === "expand" || slug.startsWith("expand/")) return true;
153153

154-
if (slug === "v8s.json" || slug === "redirect-targets.json") return false;
154+
if (slug === "v8s.json" || slug === "v8s-blocklist.json" || slug === "v8s-site-config.json") return false;
155155

156156
return /\.(html|css|js|json|png|svg|ico|webmanifest|txt|xml)$/i.test(slug);
157157
}

defaults/functions/pages/_stats/api/redirects.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export async function onRequest(context) {
5757
const dynamicRoutes = all.filter(r => r.type === 'dynamic');
5858

5959
// Reserved path violations
60-
const reservedPrefixes = ['/_stats', '/api', '/_worker', '/v8s.json', '/redirect-targets.json'];
60+
const reservedPrefixes = ['/_stats', '/api', '/_worker', '/v8s.json', '/v8s-blocklist.json', '/v8s-site-config.json'];
6161
const reservedViolations = all.filter(r =>
6262
reservedPrefixes.some(prefix => r.source.startsWith(prefix))
6363
);

defaults/public/_headers

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,6 @@
4040
Cache-Control: no-store
4141
X-Robots-Tag: noindex, nofollow
4242

43-
/redirect-targets.json
43+
/v8s-site-config.json
4444
Cache-Control: no-store
4545
X-Robots-Tag: noindex, nofollow

defaults/public/_tests/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<meta name="robots" content="noindex, nofollow">
7-
<title>dicai.re QA Tests</title>
7+
<title>VanityURLs QA Tests</title>
88
<link rel="stylesheet" href="/style.css?v=20260504">
99
</head>
1010
<body>

defaults/v8s-site-config.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"schema_version": "1.0",
3+
"i18n": {
4+
"default_language": "en",
5+
"supported_languages": ["en", "fr", "es", "it", "de"]
6+
}
7+
}

scripts/build.mjs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,92 @@ const BUILD_DIR = path.join(ROOT, "build");
99
const GENERATED_BLOCKLIST_PATH = path.join(BUILD_DIR, "blocklist.generated.json");
1010
const RUNTIME_BLOCKLIST_PATH = path.join(BUILD_DIR, "v8s-blocklist.json");
1111
const RUNTIME_REGISTRY_PATH = path.join(BUILD_DIR, "v8s.json");
12+
const RUNTIME_SITE_CONFIG_PATH = path.join(BUILD_DIR, "v8s-site-config.json");
1213
const DEFAULTS_DIR = path.join(ROOT, "defaults");
1314
const CUSTOM_DIR = path.join(ROOT, "custom");
1415
const LOCAL_CONFIG_PATH = path.join(CUSTOM_DIR, "v8s-local-config.json");
1516
const WORKER_SOURCE_DIR = path.join(ROOT, "scripts", "src");
1617
const RUNTIME_SOURCE_DIR = path.join(ROOT, "src");
18+
const LANGUAGE_METADATA = {
19+
en: {
20+
name: "English",
21+
pagesTitle: "Pages",
22+
statusTitle: "Status Pages",
23+
links: {
24+
index: "Index",
25+
expand: "Expand",
26+
stats: "Stats",
27+
privacy: "Privacy",
28+
terms: "Terms",
29+
abuse: "Abuse",
30+
security: "Security",
31+
notFound: "404",
32+
expired: "Expired",
33+
disabled: "Disabled",
34+
maintenance: "Maintenance"
35+
}
36+
},
37+
fr: {
38+
name: "Français",
39+
pagesTitle: "Pages",
40+
statusTitle: "Pages d'état",
41+
links: {
42+
index: "Accueil",
43+
expand: "Développer",
44+
stats: "Stats",
45+
privacy: "Confidentialité",
46+
terms: "Conditions",
47+
abuse: "Abus",
48+
security: "Sécurité",
49+
notFound: "404",
50+
expired: "Expiré",
51+
disabled: "Désactivé",
52+
maintenance: "Maintenance"
53+
}
54+
},
55+
es: {
56+
name: "Español",
57+
pagesTitle: "Páginas",
58+
statusTitle: "Páginas de estado",
59+
links: {
60+
index: "Inicio",
61+
expand: "Expandir",
62+
stats: "Stats",
63+
notFound: "404",
64+
expired: "Caducado",
65+
disabled: "Desactivado",
66+
maintenance: "Mantenimiento"
67+
}
68+
},
69+
it: {
70+
name: "Italiano",
71+
pagesTitle: "Pagine",
72+
statusTitle: "Pagine di stato",
73+
links: {
74+
index: "Home",
75+
expand: "Espandi",
76+
stats: "Stats",
77+
notFound: "404",
78+
expired: "Scaduto",
79+
disabled: "Disattivato",
80+
maintenance: "Manutenzione"
81+
}
82+
},
83+
de: {
84+
name: "Deutsch",
85+
pagesTitle: "Seiten",
86+
statusTitle: "Statusseiten",
87+
links: {
88+
index: "Start",
89+
expand: "Erweitern",
90+
stats: "Stats",
91+
notFound: "404",
92+
expired: "Abgelaufen",
93+
disabled: "Deaktiviert",
94+
maintenance: "Wartung"
95+
}
96+
}
97+
};
1798

1899
function log(message) {
19100
console.log(`[build] ${message}`);
@@ -82,6 +163,18 @@ function copyRuntimeSource() {
82163
copyDirectory(WORKER_SOURCE_DIR, RUNTIME_SOURCE_DIR);
83164
}
84165

166+
function patchRuntimeLanguages(siteConfig) {
167+
const workerPath = path.join(RUNTIME_SOURCE_DIR, "worker.mjs");
168+
const localizedLanguages = supportedLanguages(siteConfig).filter((language) => language !== "en");
169+
const text = fs.readFileSync(workerPath, "utf8");
170+
const next = text.replace(
171+
/const LOCALIZED_HTML_LANGUAGES = \[[^\]]*\];[^\n]*/,
172+
`const LOCALIZED_HTML_LANGUAGES = ${JSON.stringify(localizedLanguages)}; // generated from v8s-site-config.json`
173+
);
174+
175+
fs.writeFileSync(workerPath, next);
176+
}
177+
85178
function copyPublic() {
86179
log("Copying defaults/public/");
87180
copyDirectory(path.join(DEFAULTS_DIR, "public"), BUILD_DIR);
@@ -93,6 +186,127 @@ function copyPublic() {
93186
}
94187
}
95188

189+
function loadSiteConfig() {
190+
const defaultConfig = readJsonFile(path.join(DEFAULTS_DIR, "v8s-site-config.json"));
191+
const customConfigPath = path.join(CUSTOM_DIR, "v8s-site-config.json");
192+
if (fs.existsSync(customConfigPath)) {
193+
return mergeSiteConfig(defaultConfig, readJsonFile(customConfigPath));
194+
}
195+
196+
return defaultConfig;
197+
}
198+
199+
function mergeSiteConfig(base, custom) {
200+
return {
201+
...base,
202+
...custom,
203+
i18n: {
204+
...(base.i18n || {}),
205+
...(custom.i18n || {})
206+
}
207+
};
208+
}
209+
210+
function supportedLanguages(siteConfig) {
211+
const configured = Array.isArray(siteConfig?.i18n?.supported_languages)
212+
? siteConfig.i18n.supported_languages
213+
: ["en"];
214+
const languages = configured
215+
.map((language) => String(language || "").trim().toLowerCase().split("-")[0])
216+
.filter(Boolean);
217+
return [...new Set(languages)].includes("en") ? [...new Set(languages)] : ["en", ...new Set(languages)];
218+
}
219+
220+
function writeSiteConfig(siteConfig) {
221+
fs.writeFileSync(RUNTIME_SITE_CONFIG_PATH, `${JSON.stringify(siteConfig, null, 2)}\n`);
222+
}
223+
224+
function buildTestsPage(siteConfig) {
225+
const languages = supportedLanguages(siteConfig);
226+
const panels = languages.map((language) => renderTestsPanel(language)).join("\n\n");
227+
const html = `<!doctype html>
228+
<html lang="en">
229+
<head>
230+
<meta charset="utf-8">
231+
<meta name="viewport" content="width=device-width, initial-scale=1">
232+
<meta name="robots" content="noindex, nofollow">
233+
<title>VanityURLs QA Tests</title>
234+
<link rel="stylesheet" href="/style.css?v=20260504">
235+
</head>
236+
<body>
237+
<main class="home-shell qa-shell">
238+
<header class="home-card qa-header">
239+
<h1 class="instance-brand-title"><span>Vanity</span><span>URLs</span></h1>
240+
<p class="lede">Instance pages and localized variants for quick checks after custom template changes</p>
241+
</header>
242+
243+
<section class="qa-grid" aria-label="Page test links">
244+
${panels}
245+
</section>
246+
</main>
247+
</body>
248+
</html>
249+
`;
250+
251+
const testsPath = path.join(BUILD_DIR, "_tests", "index.html");
252+
fs.mkdirSync(path.dirname(testsPath), { recursive: true });
253+
fs.writeFileSync(testsPath, html);
254+
}
255+
256+
function renderTestsPanel(language) {
257+
const metadata = LANGUAGE_METADATA[language] || {
258+
name: language,
259+
pagesTitle: "Pages",
260+
statusTitle: "Status Pages",
261+
links: LANGUAGE_METADATA.en.links
262+
};
263+
const prefix = language === "en" ? "" : `/${language}`;
264+
const extension = language === "en" ? "" : ".html";
265+
const indexHref = language === "en" ? "/index" : `${prefix}/index.html`;
266+
const expandHref = language === "en" ? "/expand" : `${prefix}/expand/index.html`;
267+
const policyLinks = language === "en" || language === "fr"
268+
? [
269+
["privacy", metadata.links.privacy || "Privacy"],
270+
["terms", metadata.links.terms || "Terms"],
271+
["abuse", metadata.links.abuse || "Abuse"],
272+
["security", metadata.links.security || "Security"]
273+
]
274+
: [];
275+
const pageLinks = [
276+
` <li><a href="${escapeHtml(indexHref)}">${escapeHtml(metadata.links.index)}</a></li>`,
277+
` <li><a href="${escapeHtml(expandHref)}">${escapeHtml(metadata.links.expand)}</a></li>`,
278+
` <li><a href="/_stats/">${escapeHtml(metadata.links.stats)}</a></li>`,
279+
...policyLinks.map(([slug, label]) => ` <li><a href="${prefix}/${slug}${extension}">${escapeHtml(label)}</a></li>`)
280+
].join("\n");
281+
282+
return ` <article class="qa-panel"${language === "en" ? "" : ` lang="${escapeHtml(language)}"`}>
283+
<h2>${escapeHtml(metadata.name)}</h2>
284+
<section class="qa-section">
285+
<h3>${escapeHtml(metadata.pagesTitle)}</h3>
286+
<ul class="qa-links">
287+
${pageLinks}
288+
</ul>
289+
</section>
290+
<section class="qa-section">
291+
<h3>${escapeHtml(metadata.statusTitle)}</h3>
292+
<ul class="qa-links">
293+
<li><a href="${prefix}/404${extension}">${escapeHtml(metadata.links.notFound)}</a></li>
294+
<li><a href="${prefix}/expired${extension}">${escapeHtml(metadata.links.expired)}</a></li>
295+
<li><a href="${prefix}/disabled${extension}">${escapeHtml(metadata.links.disabled)}</a></li>
296+
<li><a href="${prefix}/maintenance${extension}">${escapeHtml(metadata.links.maintenance)}</a></li>
297+
</ul>
298+
</section>
299+
</article>`;
300+
}
301+
302+
function escapeHtml(value) {
303+
return String(value ?? "")
304+
.replaceAll("&", "&amp;")
305+
.replaceAll("<", "&lt;")
306+
.replaceAll(">", "&gt;")
307+
.replaceAll('"', "&quot;");
308+
}
309+
96310
function copyRuntimeBlocklist() {
97311
log("Building v8s-blocklist.json");
98312

@@ -211,9 +425,13 @@ function expandLocalPath(value) {
211425
}
212426

213427
function main() {
428+
const siteConfig = loadSiteConfig();
214429
copyRuntimeSource();
430+
patchRuntimeLanguages(siteConfig);
215431
cleanBuild();
216432
copyPublic();
433+
writeSiteConfig(siteConfig);
434+
buildTestsPage(siteConfig);
217435
copyRuntimeBlocklist();
218436
buildRedirectTargets();
219437
validateRuntimeRegistry();

scripts/src/worker.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const WEEKDAY_ALIASES = {
2121
let runtimeBlocklistPromise;
2222
const accessJwksPromises = new Map();
2323

24-
const LOCALIZED_HTML_LANGUAGES = ["fr", "es", "it", "de"];
24+
const LOCALIZED_HTML_LANGUAGES = ["fr", "es", "it", "de"]; // build replaces this list from v8s-site-config.json
2525

2626
const BOT_PATTERNS = [
2727
{ re: /googlebot/i, name: "Googlebot" },
@@ -287,7 +287,7 @@ function isTestsPath(slug) {
287287
}
288288

289289
function isPrivateRuntimeAsset(slug) {
290-
return slug === "v8s.json" || slug === "redirect-targets.json" || slug === "v8s-blocklist.json";
290+
return slug === "v8s.json" || slug === "v8s-blocklist.json" || slug === "v8s-site-config.json";
291291
}
292292

293293
async function loadRegistry(request, env) {
@@ -1026,7 +1026,7 @@ async function renderStatsRedirects(request, env) {
10261026

10271027
const missingDescriptions = all.filter((redirect) => !redirect.description);
10281028
const dynamicRoutes = all.filter((redirect) => redirect.type === "dynamic");
1029-
const reservedPrefixes = ["/_stats", "/api", "/_worker", "/v8s.json", "/redirect-targets.json"];
1029+
const reservedPrefixes = ["/_stats", "/api", "/_worker", "/v8s.json", "/v8s-blocklist.json", "/v8s-site-config.json"];
10301030
const reservedViolations = all.filter((redirect) => {
10311031
return reservedPrefixes.some((prefix) => redirect.source.startsWith(prefix));
10321032
});

scripts/src/worker.test.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,13 @@ await run("blocks raw runtime blocklist asset", async () => {
394394
assert(response.headers.get("x-robots-tag") === "noindex, nofollow", "robots header");
395395
});
396396

397+
await run("blocks raw site config asset", async () => {
398+
const ctx = mockCtx();
399+
const response = await worker.fetch(request("/v8s-site-config.json"), env(), ctx);
400+
assert(response.status === 404, "status");
401+
assert(response.headers.get("x-robots-tag") === "noindex, nofollow", "robots header");
402+
});
403+
397404
await run("requires Cloudflare Access config for protected paths", async () => {
398405
const ctx = mockCtx();
399406
const response = await worker.fetch(request("/_tests"), env(), ctx);

scripts/validate-lnk.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const blocklistPolicy = loadBlocklistPolicy();
99

1010
const allowedStates = new Set(["permanent", "ephemeral", "expired", "disabled", "maintenance", "deactivated"]);
1111
const targetRedirectStates = new Set(["permanent", "ephemeral"]);
12-
const reservedTopLevel = new Set(["_stats", "assets", "expand", "404", "404.html", "v8s.json", "redirect-targets.json", "expired", "disabled", "maintenance", "deactivated"]);
12+
const reservedTopLevel = new Set(["_stats", "assets", "expand", "404", "404.html", "v8s.json", "v8s-blocklist.json", "v8s-site-config.json", "expired", "disabled", "maintenance", "deactivated"]);
1313

1414
function fail(message) { errors.push(message); }
1515
function warn(message) { warnings.push(message); }

0 commit comments

Comments
 (0)