Files
pw/astro.config.mjs
T
Asger Geel Weirsøe fc7ca339a1
Build & Deploy / deploy (push) Successful in 1m43s
seo: én kanonisk URL-form — uden afsluttende skråstreg
Google kendte 41 af sitemapets 44 URL'er som "Webadressen er ikke kendt af
Google". Årsagen var at siden fandtes i to former: de interne links peger alle
på no-slash (/blog, /cv, /tags/proxmox), mens sitemap og <link rel="canonical">
brugte skråstreg-formen. Origin serverer begge med 200 uden redirect, så
Googles linkbaserede opdagelse ramte kun det ene sæt, og sitemapets sæt blev
aldrig opdaget.

Nu peger canonical, sitemap og RSS alle på no-slash-formen, som de interne
links i forvejen brugte. lastmod slås stadig op på Astros egne ruter (som har
skråstregen) FØR URL'en strippes, så sitemapets 44 lastmod-datoer er uændrede.

Claude-Session: https://claude.ai/code/session_0198n4owY35jQpwht1wCGzfh
2026-09-10 20:15:00 +02:00

174 lines
5.9 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;
// Samme kanoniske form som <link rel="canonical"> og de interne links:
// uden afsluttende skråstreg. Slås lastmod op FØR strippet, fordi
// lastmodByRoute er nøglet på Astros egne ruter (som har skråstregen).
if (pathname !== '/') item.url = item.url.replace(/\/$/, '');
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()] },
});