@@ -9,11 +9,92 @@ const BUILD_DIR = path.join(ROOT, "build");
99const GENERATED_BLOCKLIST_PATH = path . join ( BUILD_DIR , "blocklist.generated.json" ) ;
1010const RUNTIME_BLOCKLIST_PATH = path . join ( BUILD_DIR , "v8s-blocklist.json" ) ;
1111const RUNTIME_REGISTRY_PATH = path . join ( BUILD_DIR , "v8s.json" ) ;
12+ const RUNTIME_SITE_CONFIG_PATH = path . join ( BUILD_DIR , "v8s-site-config.json" ) ;
1213const DEFAULTS_DIR = path . join ( ROOT , "defaults" ) ;
1314const CUSTOM_DIR = path . join ( ROOT , "custom" ) ;
1415const LOCAL_CONFIG_PATH = path . join ( CUSTOM_DIR , "v8s-local-config.json" ) ;
1516const WORKER_SOURCE_DIR = path . join ( ROOT , "scripts" , "src" ) ;
1617const 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
1899function 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+ / c o n s t L O C A L I Z E D _ H T M L _ L A N G U A G E S = \[ [ ^ \] ] * \] ; [ ^ \n ] * / ,
172+ `const LOCALIZED_HTML_LANGUAGES = ${ JSON . stringify ( localizedLanguages ) } ; // generated from v8s-site-config.json`
173+ ) ;
174+
175+ fs . writeFileSync ( workerPath , next ) ;
176+ }
177+
85178function 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 ( "&" , "&" )
305+ . replaceAll ( "<" , "<" )
306+ . replaceAll ( ">" , ">" )
307+ . replaceAll ( '"' , """ ) ;
308+ }
309+
96310function copyRuntimeBlocklist ( ) {
97311 log ( "Building v8s-blocklist.json" ) ;
98312
@@ -211,9 +425,13 @@ function expandLocalPath(value) {
211425}
212426
213427function 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 ( ) ;
0 commit comments