Files
pw/astro.config.mjs
T
Asger Geel Weirsøe 034e5e3312
Build & Deploy / deploy (push) Successful in 2m19s
seo: præcis sitemap-lastmod, IndexNow-ping ved deploy og favicon.ico
Fire ugers adgangslog (81k requests) viser at ægte Googlebot kun hentede robots.txt,
favicon og et par billeder — plus én enkelt HTML-side i hele perioden. Intet af det her
løser dét (kun indgående links gør), men det fjerner de undskyldninger vi selv havde
lagt ud.

- Sitemappet havde ingen lastmod overhovedet. Nu udleder astro.config.mjs den pr. side:
  statiske sider fra deres sidste commit, blogindlæg fra updated/date i frontmatter,
  og oversigts-/tagsider fra det nyeste indlæg de viser. Kan en dato ikke fastslås
  præcist, udelades lastmod helt — en forkert dato er værre end ingen, fordi Google
  holder op med at stole på feltet.
- Det kræver rigtig git-historik, så checkout kører nu fetch-depth: 0 (default shallow
  ville give alle filer samme dato).
- Nyt deploy-step pinger IndexNow med de sider dette push faktisk ændrede — udregnet af
  scripts/indexnow.mjs ud fra diffen, ikke hele sitemappet, da gentagne fuld-lister er
  støj. Nøglefilen lå allerede i public/, men blev aldrig brugt. Hjælper kun Bing/Yandex;
  Google understøtter ikke IndexNow. continue-on-error, så det aldrig vælter et deploy.
- /favicon.ico gav 404. Tilføjet som rigtig 3-størrelses ICO (16/32/48) genereret fra
  favicon.svg med samme baggrund som de øvrige ikoner.

Claude-Session: https://claude.ai/code/session_018oXJWTRxTYdcV9Kw6M7e5J
2026-08-09 09:15:38 +02:00

170 lines
5.6 KiB
JavaScript

import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import AstroPWA from '@vite-pwa/astro';
import tailwindcss from '@tailwindcss/vite';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.dirname(fileURLToPath(import.meta.url));
// ── lastmod til sitemap ────────────────────────────────────────────────────────
// Google bruger kun lastmod hvis den er troværdig, så vi gætter aldrig: statiske
// sider får datoen for deres SIDSTE COMMIT, blogindlæg får updated/date fra
// frontmatter, og alt vi ikke kan datere præcist får slet ingen lastmod.
// NB: kræver fuld git-historik — deploy-workflowen bruger fetch-depth: 0.
// Er historikken shallow (eller mangler .git), falder vi stille tilbage til
// "ingen lastmod" i stedet for at udgive en forkert dato.
const gitLastModified = (file) => {
try {
const out = execFileSync('git', ['log', '-1', '--format=%cI', '--', file], {
cwd: ROOT,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return out || null;
} catch {
return null;
}
};
// Shallow clone => hver fil ser ud til at være ændret i den ene commit vi har.
// Så er datoerne værdiløse, og vi dropper lastmod helt.
const gitHistoryIsUsable = (() => {
try {
const n = execFileSync('git', ['rev-list', '--count', 'HEAD'], {
cwd: ROOT,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return Number(n) > 1;
} catch {
return false;
}
})();
const walk = (dir) =>
fs.existsSync(dir)
? fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const p = path.join(dir, e.name);
return e.isDirectory() ? walk(p) : [p];
})
: [];
// src/pages/foo.astro -> /foo/ ; src/pages/index.astro -> / ; [dynamiske] springes over
const routeForPage = (file) => {
const rel = path.relative(path.join(ROOT, 'src/pages'), file).replace(/\\/g, '/');
if (rel.includes('[') || rel.startsWith('404.')) return null;
if (!rel.endsWith('.astro')) return null;
const noExt = rel.slice(0, -'.astro'.length);
if (noExt === 'index') return '/';
return `/${noExt.replace(/\/index$/, '')}/`;
};
// Minimal frontmatter-læsning — vi skal kun bruge date/updated/tags/draft.
const readFrontmatter = (file) => {
const src = fs.readFileSync(file, 'utf8');
const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!m) return {};
const fm = {};
for (const line of m[1].split(/\r?\n/)) {
const kv = line.match(/^(\w+):\s*(.*)$/);
if (!kv) continue;
const [, k, rawv] = kv;
const v = rawv.trim().replace(/^['"]|['"]$/g, '');
if (k === 'tags') {
const inline = v.match(/^\[(.*)\]$/);
fm.tags = inline
? inline[1].split(',').map((t) => t.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
: [];
} else {
fm[k] = v;
}
}
return fm;
};
const iso = (v) => {
const d = new Date(v);
return Number.isNaN(d.getTime()) ? null : d.toISOString();
};
const newest = (dates) => dates.filter(Boolean).sort().at(-1) ?? null;
const buildLastmodMap = () => {
const map = new Map();
if (!gitHistoryIsUsable) return map;
for (const file of walk(path.join(ROOT, 'src/pages'))) {
const route = routeForPage(file);
if (!route) continue;
const d = gitLastModified(file);
if (d) map.set(route, d);
}
// Blogindlæg: frontmatter er sandheden om hvornår indholdet sidst ændrede sig.
const postDates = [];
const tagDates = new Map();
for (const file of walk(path.join(ROOT, 'src/content/blog'))) {
if (!file.endsWith('.md')) continue;
const fm = readFrontmatter(file);
if (fm.draft === 'true') continue;
const d = iso(fm.updated || fm.date);
if (!d) continue;
const slug = path.basename(file, '.md');
map.set(`/blog/${slug}/`, d);
postDates.push(d);
for (const t of fm.tags ?? []) {
const key = t.toLowerCase();
tagDates.set(key, newest([tagDates.get(key), d]));
}
}
// Oversigtssider er lige så friske som det nyeste de viser.
const latest = newest(postDates);
if (latest) {
map.set('/blog/', latest);
map.set('/tags/', latest);
for (const [tag, d] of tagDates) map.set(`/tags/${tag}/`, d);
}
return map;
};
const lastmodByRoute = buildLastmodMap();
export default defineConfig({
site: 'https://asger.xn--weirse-fya.dk',
trailingSlash: 'ignore',
integrations: [
sitemap({
changefreq: 'weekly',
priority: 0.7,
serialize(item) {
const pathname = new URL(item.url).pathname;
if (pathname === '/') item.priority = 1.0;
const lastmod = lastmodByRoute.get(pathname);
if (lastmod) item.lastmod = lastmod;
return item;
},
}),
// Service worker → offline support + installable PWA. autoUpdate + outdated-cache
// cleanup so a redeploy never strands a visitor on a stale version. Reuses the
// existing public/site.webmanifest (manifest: false = don't generate another).
AstroPWA({
registerType: 'autoUpdate',
manifest: false,
includeManifestIcons: false,
workbox: {
globPatterns: ['**/*.{html,css,js,svg,png,woff2,xml,txt,webmanifest}'],
cleanupOutdatedCaches: true,
clientsClaim: true,
skipWaiting: true,
},
devOptions: { enabled: false },
}),
],
prefetch: { prefetchAll: true, defaultStrategy: 'viewport' },
vite: { plugins: [tailwindcss()] },
});