From 1149de7ab770132d6c0d77598e60ec78458248a7 Mon Sep 17 00:00:00 2001 From: Timo <6156589+Shik3i@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:33:53 +0200 Subject: [PATCH] fix: derive sitemap dates from page history --- .github/workflows/beta-server-image.yml | 2 + .github/workflows/ci.yml | 2 + .github/workflows/release.yml | 2 + scripts/test-website-theme.mjs | 8 +-- website/build.cjs | 70 ++++++++++++++++++------- 5 files changed, 61 insertions(+), 23 deletions(-) diff --git a/.github/workflows/beta-server-image.yml b/.github/workflows/beta-server-image.yml index 2ad6f9d..1b70b9b 100644 --- a/.github/workflows/beta-server-image.yml +++ b/.github/workflows/beta-server-image.yml @@ -37,6 +37,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14bd8e9..8e4d203 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afce636..5352bdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,6 +72,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/scripts/test-website-theme.mjs b/scripts/test-website-theme.mjs index c7dd168..bab02e3 100644 --- a/scripts/test-website-theme.mjs +++ b/scripts/test-website-theme.mjs @@ -83,8 +83,10 @@ for (const metadata of requiredSupportSocialMetadata) { throw new Error(`site-access-help.html is missing SEO metadata: ${metadata}`); } } -if (//.test(websiteBuild) || /const today = new Date\(\)/.test(websiteBuild)) { - throw new Error('Sitemap must not claim synthetic modification dates for every URL'); +if (!websiteBuild.includes("['log', '-1', '--format=%cs', '--', ...sourceFiles]") + || !websiteBuild.includes("lastmod(['website/site-access-help.html'])") + || /const today = new Date\(\)/.test(websiteBuild)) { + throw new Error('Sitemap lastmod values must come from the mapped source files in Git'); } const documentedRelayUrls = [...llmsText.matchAll(/`(wss:\/\/[^`\s]+)`/g)].map(([, value]) => new URL(value)); const hasCanonicalPublicRelay = documentedRelayUrls.some((url) => ( @@ -199,7 +201,7 @@ if (!/animation:\s*none\s*!important/.test(reducedMotionRule)) { console.log('Extension mockup theme-sensitive text uses theme-aware colors'); console.log('Landing CSS is render-blocking, single-request, and cascade-stable'); console.log('Landing head discovers a complete and architecture-accurate llms.txt'); -console.log('Support-page social metadata and sitemap dates remain truthful'); +console.log('Support-page social metadata and Git-derived sitemap dates remain truthful'); console.log('Website self-hosting examples include production secrets and consistent networking'); console.log('Foreground film birds use complete, always-on wing and glide animations'); console.log('All Getting Started mockups define explicit light-theme surfaces'); diff --git a/website/build.cjs b/website/build.cjs index 05155e9..57c4a46 100644 --- a/website/build.cjs +++ b/website/build.cjs @@ -5,6 +5,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const { execFileSync } = require('child_process'); const sharp = require('sharp'); const esbuild = require('esbuild'); const htmlnano = require('htmlnano'); @@ -549,7 +550,7 @@ async function compile() { } // ── 5.5 Generate dynamic sitemap ── - generateSitemap(wwwDir); + generateSitemap(websiteDir, wwwDir); // Auto-copy Google verification files and IndexNow/txt key files from website source to www root const websiteFiles = fs.readdirSync(websiteDir); @@ -697,7 +698,7 @@ async function compile() { console.log('KoalaSync build finished successfully! Output: website/www/'); } -function generateSitemap(wwwDir) { +function generateSitemap(websiteDir, wwwDir) { const languages = [ { code: 'en', prefix: '', hreflang: 'en' }, { code: 'de', prefix: 'de/', hreflang: 'de' }, @@ -716,6 +717,33 @@ function generateSitemap(wwwDir) { { code: 'pt', prefix: 'pt/', hreflang: 'pt' } ]; + const repoRoot = path.resolve(websiteDir, '..'); + const lastModifiedCache = new Map(); + + function getLastModified(sourceFiles) { + const cacheKey = sourceFiles.slice().sort().join('\0'); + if (lastModifiedCache.has(cacheKey)) return lastModifiedCache.get(cacheKey); + let date = null; + try { + const output = execFileSync( + 'git', + ['log', '-1', '--format=%cs', '--', ...sourceFiles], + { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] } + ).trim(); + if (/^\d{4}-\d{2}-\d{2}$/.test(output)) date = output; + } catch (_) { + // Source archives and minimal deployment environments may not contain + // Git metadata. Omitting lastmod is more accurate than inventing a date. + } + lastModifiedCache.set(cacheKey, date); + return date; + } + + function lastmod(sourceFiles, indent = ' ') { + const date = getLastModified(sourceFiles); + return date ? `\n${indent}${date}` : ''; + } + let xml = ` `; @@ -723,37 +751,38 @@ function generateSitemap(wwwDir) { // Static legal pages xml += ` - https://sync.koalastuff.net/imprint + https://sync.koalastuff.net/imprint${lastmod(['website/imprint.html'])} monthly 0.3 - https://sync.koalastuff.net/privacy + https://sync.koalastuff.net/privacy${lastmod(['website/privacy.html'])} monthly 0.3 - https://sync.koalastuff.net/site-access-help.html + https://sync.koalastuff.net/site-access-help.html${lastmod(['website/site-access-help.html'])} weekly 0.8 - https://sync.koalastuff.net/de/impressum + https://sync.koalastuff.net/de/impressum${lastmod(['website/impressum-de.html'])} monthly 0.3 - https://sync.koalastuff.net/de/datenschutz + https://sync.koalastuff.net/de/datenschutz${lastmod(['website/datenschutz-de.html'])} monthly 0.3 `; - function addPage(relativePath, changefreq, priority) { + function addPage(relativePath, changefreq, priority, templateSource) { for (const lang of languages) { const loc = `https://sync.koalastuff.net/${lang.prefix}${relativePath}`; + const sourceFiles = [templateSource, `website/locales/${lang.code}.json`]; xml += ` - ${loc} + ${loc}${lastmod(sourceFiles)} ${changefreq} ${priority}`; for (const alt of languages) { @@ -768,24 +797,25 @@ function generateSitemap(wwwDir) { } } - addPage('', 'weekly', '1.0'); - addPage('alternatives', 'weekly', '0.7'); + addPage('', 'weekly', '1.0', 'website/template.html'); + addPage('alternatives', 'weekly', '0.7', 'website/alternatives/index.html'); const subpages = [ - 'alternatives/teleparty', - 'alternatives/screen-sharing', - 'alternatives/watch2gether', - 'alternatives/scener', - 'alternatives/kosmi', - 'alternatives/twoseven' + ['alternatives/teleparty', 'website/alternatives/teleparty.html'], + ['alternatives/screen-sharing', 'website/alternatives/screen-sharing.html'], + ['alternatives/watch2gether', 'website/alternatives/watch2gether.html'], + ['alternatives/scener', 'website/alternatives/scener.html'], + ['alternatives/kosmi', 'website/alternatives/kosmi.html'], + ['alternatives/twoseven', 'website/alternatives/twoseven.html'] ]; - for (const sub of subpages) { - addPage(sub, 'weekly', '0.7'); + for (const [sub, templateSource] of subpages) { + addPage(sub, 'weekly', '0.7', templateSource); } xml += `\n\n`; fs.writeFileSync(path.join(wwwDir, 'sitemap.xml'), xml.trim() + '\n', 'utf8'); - console.log(' ✓ Dynamically generated sitemap.xml without synthetic modification dates'); + const datedEntries = (xml.match(//g) || []).length; + console.log(` ✓ Dynamically generated sitemap.xml with ${datedEntries} Git-derived modification dates`); } compile().catch(err => { console.error('Build failed:', err); process.exit(1); });