/** * Erzeugt alle Sprach-Landingpages aus EINER Vorlage + texte.js. * * node tools/sprachseiten/build.js * * Warum ein Generator: Bei 28 Sprachen müsste sonst jede Änderung (neuer * Abschnitt, neuer Link, neues Brevo-Formular) 28-mal von Hand gemacht werden. * So ist es eine Änderung an einer Stelle. * * Deutsch (index.html) wird NICHT erzeugt — die Startseite ist handgemacht * und deutlich umfangreicher. Sie bekommt nur hreflang + Sprachwähler gepflegt * (siehe hreflangBlock() / waehlerBlock(), die auch index.html aktualisieren). */ const fs = require('fs'); const path = require('path'); const { SPRACHEN, TEXTE } = require('./texte.js'); const AUS = path.join(__dirname, '..', '..', 'webseite'); const BASIS = 'https://behoerdenklar.de'; const BREVO = 'https://86550585.sibforms.com/serve/MUIFAAvOQCpFg148k-xD2Ze2PDVKKaFEIaLECCCMe9fGKd_uIurPljyn_IhTrkR0EkM_pQp_8VBzcHZ6N9sKDX5GBZi9elYKN6isXGPUUxcAgO37OKhTFZyMlJiC9ikRmPI8Q2tU2GvqP6esJ5ReGfv1htZV_OfXRIgW59P6pkeUsYKxqdXA7nWOaWxNTYz4MzzoGg7czJMJ2fxuEg=='; /** Alle Sprachen inkl. Deutsch, in der Reihenfolge aus texte.js. */ const ALLE = [{ code: 'de', eigenname: 'Deutsch', datei: 'index.html', pfad: '/' }].concat( SPRACHEN.map((s) => ({ ...s, datei: `${s.code}.html`, pfad: `/${s.code}` })) ); /** hreflang-Satz — identisch auf JEDER Seite, sonst wertet Google ihn ab. */ function hreflangBlock() { return ( ALLE.map((s) => ` `).join('\n') + `\n ` ); } /** * Sprachwähler als Auswahlliste. Bei 28 Sprachen wäre eine Linkleiste * unbenutzbar — ein Aufklappmenü bleibt auch auf dem Handy bedienbar. * Google findet die Seiten trotzdem: über hreflang und die Sitemap. */ function waehlerBlock(aktiv, label) { const optionen = ALLE.map( (s) => ` ` ).join('\n'); return ` `; } const WAEHLER_CSS = ` .sprachwahl { font-family: inherit; font-size: 15px; color: var(--tinte); background: #fff; border: 1.5px solid var(--tinte); padding: 6px 10px; max-width: 220px; cursor: pointer; }`; /** * Strukturierte Daten je Sprachseite. Beschreibung und Sprache kommen aus * texte.js, damit Google die Seite in der jeweiligen Sprache einordnet — * ein deutscher Block auf der tuerkischen Seite waere widerspruechlich. */ function schemaBlock(s, t) { const daten = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'BehördenKlar', description: t.beschreibung, applicationCategory: 'AccessibilityApplication', operatingSystem: 'iOS, Web', inLanguage: ALLE.map((x) => x.code), offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' }, author: { '@type': 'Organization', name: 'Webklar GbR', url: BASIS }, }; return JSON.stringify(daten, null, 2) .split('\n').map((z) => ' ' + z).join('\n'); } /** * Sitemap mit Sprach-Angaben (xhtml:link). Google erkennt die Alternativen * dadurch auch ohne die HTML-Tags — doppelt haelt besser, gerade bei 28 Sprachen. * Die Nicht-Sprachseiten (Demo, Ratgeber) bleiben unveraendert bestehen. */ function sitemapSchreiben() { const pfad = path.join(AUS, 'sitemap.xml'); const heute = new Date().toISOString().slice(0, 10); const alt = fs.existsSync(pfad) ? fs.readFileSync(pfad, 'utf8') : ''; const andere = (alt.match(/.*?<\/url>/gs) || []).filter( (u) => /\/(demo|ratgeber)/.test(u) ); const sprachLinks = ALLE.map( (x) => ` ` ).join('\n') + `\n `; const bloecke = ALLE.map( (x) => ` \n ${BASIS}${x.pfad}\n ${heute}\n${sprachLinks}\n ` ); const xml = [ '', '', ...bloecke, ...andere.map((u) => ' ' + u.replace(/\s+/g, ' ')), '', ].join('\n'); fs.writeFileSync(pfad, xml + '\n', 'utf8'); return ALLE.length + andere.length; } function seite(s) { const t = TEXTE[s.code]; if (!t) throw new Error('Keine Texte für ' + s.code); const rtl = s.rtl ? ' dir="rtl"' : ''; const vorlage = fs.readFileSync(path.join(__dirname, 'vorlage.html'), 'utf8'); return vorlage .replace(/\{\{LANG\}\}/g, s.code) .replace(/\{\{DIR\}\}/g, rtl) .replace(/\{\{LOCALE\}\}/g, t.locale || s.code) .replace(/\{\{TITEL\}\}/g, t.titel) .replace(/\{\{BESCHREIBUNG\}\}/g, t.beschreibung) .replace(/\{\{OG_TITEL\}\}/g, t.ogTitel) .replace(/\{\{OG_BESCHREIBUNG\}\}/g, t.ogBeschreibung) .replace(/\{\{URL\}\}/g, `${BASIS}/${s.code}`) .replace(/\{\{HREFLANG\}\}/g, hreflangBlock()) .replace(/\{\{WAEHLER_CSS\}\}/g, WAEHLER_CSS) .replace(/\{\{SCHEMA\}\}/g, schemaBlock(s, t)) .replace(/\{\{WAEHLER\}\}/g, waehlerBlock(s.code, t.navLabel)) .replace(/\{\{H1_A\}\}/g, t.h1a) .replace(/\{\{H1_B\}\}/g, t.h1b) .replace(/\{\{INTRO\}\}/g, t.intro) .replace(/\{\{CTA_HERO\}\}/g, t.ctaHero) .replace(/\{\{STEMPEL\}\}/g, t.stempel) .replace(/\{\{S1_TITEL\}\}/g, t.s1Titel) .replace(/\{\{VORHER_LABEL\}\}/g, t.vorherLabel) .replace(/\{\{PFEIL\}\}/g, t.pfeil) .replace(/\{\{NACHHER_LABEL\}\}/g, t.nachherLabel) .replace(/\{\{NACHHER_TEXT\}\}/g, t.nachherText) .replace(/\{\{HINWEIS\}\}/g, t.hinweis) .replace(/\{\{S2_TITEL\}\}/g, t.s2Titel) .replace(/\{\{SCHRITT1_T\}\}/g, t.schritt1T).replace(/\{\{SCHRITT1_B\}\}/g, t.schritt1B) .replace(/\{\{SCHRITT2_T\}\}/g, t.schritt2T).replace(/\{\{SCHRITT2_B\}\}/g, t.schritt2B) .replace(/\{\{SCHRITT3_T\}\}/g, t.schritt3T).replace(/\{\{SCHRITT3_B\}\}/g, t.schritt3B) .replace(/\{\{S3_TITEL\}\}/g, t.s3Titel) .replace(/\{\{FORMULAR_KOPF\}\}/g, t.formularKopf) .replace(/\{\{FORM_H3\}\}/g, t.formH3) .replace(/\{\{FORM_TEXT\}\}/g, t.formText) .replace(/\{\{BONUS\}\}/g, t.bonus) .replace(/\{\{BREVO\}\}/g, BREVO) .replace(/\{\{EMAIL_PLATZHALTER\}\}/g, t.emailPlatzhalter) .replace(/\{\{CTA_FORM\}\}/g, t.ctaForm) .replace(/\{\{KLEIN\}\}/g, t.klein) .replace(/\{\{DATENSCHUTZ\}\}/g, t.datenschutz) .replace(/\{\{IMPRESSUM\}\}/g, t.impressum) .replace(/\{\{FUSS_FRAGE\}\}/g, t.fussFrage); } let n = 0; for (const s of SPRACHEN) { fs.writeFileSync(path.join(AUS, `${s.code}.html`), seite(s), 'utf8'); n++; } // index.html (Deutsch, handgemacht) mitpflegen: hreflang + Sprachwähler const idxPfad = path.join(AUS, 'index.html'); let idx = fs.readFileSync(idxPfad, 'utf8'); idx = idx.replace( /( ]*>)/, hreflangBlock() ); idx = idx.replace( /