seo: præcis sitemap-lastmod, IndexNow-ping ved deploy og favicon.ico
Build & Deploy / deploy (push) Successful in 2m19s
Build & Deploy / deploy (push) Successful in 2m19s
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
This commit is contained in:
@@ -12,6 +12,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
# Fuld historik: astro.config.mjs udleder sitemap-lastmod fra hver sides
|
||||
# sidste commit, og IndexNow-steppet differ pushet. Begge er værdiløse
|
||||
# med den default shallow clone (fetch-depth: 1).
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -56,3 +60,12 @@ jobs:
|
||||
&& { mv $DEPLOY_PATH $DEPLOY_PATH.old 2>/dev/null || true; } \
|
||||
&& mv $DEPLOY_PATH.new $DEPLOY_PATH \
|
||||
&& rm -rf $DEPLOY_PATH.old"
|
||||
|
||||
# Fortæl Bing/Yandex m.fl. med det samme hvilke sider der ændrede sig.
|
||||
# Kører EFTER deploy, så nøglefilen og siderne er live når de kommer og tjekker.
|
||||
# Google understøtter ikke IndexNow. Må aldrig vælte et ellers vellykket deploy.
|
||||
- name: Ping IndexNow
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
||||
run: node scripts/indexnow.mjs
|
||||
|
||||
+134
-1
@@ -2,6 +2,136 @@ 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',
|
||||
@@ -11,7 +141,10 @@ export default defineConfig({
|
||||
changefreq: 'weekly',
|
||||
priority: 0.7,
|
||||
serialize(item) {
|
||||
if (item.url === 'https://asger.xn--weirse-fya.dk/') item.priority = 1.0;
|
||||
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;
|
||||
},
|
||||
}),
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,99 @@
|
||||
// Pinger IndexNow (Bing/Yandex/Seznam m.fl.) med de sider dette push faktisk ændrede.
|
||||
//
|
||||
// Vi sender bevidst IKKE hele sitemappet hver gang: IndexNow er til "det her ændrede sig
|
||||
// nu", og gentagne fuld-lister ser ud som støj. Kan vi ikke regne diffen ud, springer vi
|
||||
// over frem for at gætte.
|
||||
//
|
||||
// Kør efter `astro build` — dist/sitemap-0.xml bruges som facitliste over gyldige URL'er.
|
||||
// Google understøtter ikke IndexNow; det her hjælper kun de øvrige motorer.
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const HOST = 'asger.xn--weirse-fya.dk';
|
||||
const KEY = 'b68b31655cb9c1a662d53905dd0b1b06';
|
||||
const ORIGIN = `https://${HOST}`;
|
||||
|
||||
const git = (args) => {
|
||||
try {
|
||||
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const sitemapUrls = () => {
|
||||
const xml = fs.readFileSync('dist/sitemap-0.xml', 'utf8');
|
||||
return new Set([...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]));
|
||||
};
|
||||
|
||||
// src/pages/foo.astro -> /foo/ ; src/content/blog/bar.md -> /blog/bar/
|
||||
const routeForFile = (f) => {
|
||||
let m = f.match(/^src\/pages\/(.+)\.astro$/);
|
||||
if (m) {
|
||||
if (m[1].includes('[')) return null; // dynamiske ruter kan vi ikke slå op
|
||||
if (m[1] === 'index') return '/';
|
||||
return `/${m[1].replace(/\/index$/, '')}/`;
|
||||
}
|
||||
m = f.match(/^src\/content\/blog\/(.+)\.md$/);
|
||||
if (m) return `/blog/${m[1]}/`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const changedFiles = () => {
|
||||
const { GITHUB_EVENT_BEFORE, GITHUB_SHA } = process.env;
|
||||
const zero = /^0*$/;
|
||||
if (GITHUB_EVENT_BEFORE && !zero.test(GITHUB_EVENT_BEFORE)) {
|
||||
const d = git(['diff', '--name-only', GITHUB_EVENT_BEFORE, GITHUB_SHA || 'HEAD']);
|
||||
if (d !== null) return d ? d.split('\n') : [];
|
||||
}
|
||||
const d = git(['diff', '--name-only', 'HEAD~1', 'HEAD']);
|
||||
return d ? d.split('\n') : null;
|
||||
};
|
||||
|
||||
const files = changedFiles();
|
||||
if (files === null) {
|
||||
console.log('IndexNow: kan ikke bestemme hvad der ændrede sig (shallow clone?) — springer over.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const valid = sitemapUrls();
|
||||
const urls = [...new Set(files.map(routeForFile).filter(Boolean).map((r) => ORIGIN + r))].filter((u) =>
|
||||
valid.has(u),
|
||||
);
|
||||
|
||||
// Layout/CSS/config-ændringer rammer hver side, men det er ikke en indholdsændring
|
||||
// værd at pinge om. Kun ruter med eget ændret kildefil ryger afsted.
|
||||
if (!urls.length) {
|
||||
console.log('IndexNow: ingen sider med ændret indhold i dette push — springer over.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const body = {
|
||||
host: HOST,
|
||||
key: KEY,
|
||||
keyLocation: `${ORIGIN}/${KEY}.txt`,
|
||||
urlList: urls,
|
||||
};
|
||||
|
||||
console.log(`IndexNow: indsender ${urls.length} URL(er):`);
|
||||
for (const u of urls) console.log(' ', u);
|
||||
|
||||
// `node scripts/indexnow.mjs --dry-run` viser hvad der ville blive sendt.
|
||||
if (process.argv.includes('--dry-run')) {
|
||||
console.log('IndexNow: --dry-run, sender ikke.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const res = await fetch('https://api.indexnow.org/indexnow', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// 200 = modtaget, 202 = modtaget men nøglen valideres stadig. Begge er fine.
|
||||
console.log(`IndexNow: HTTP ${res.status} ${res.statusText}`);
|
||||
if (![200, 202].includes(res.status)) {
|
||||
console.log(await res.text().catch(() => ''));
|
||||
console.log('IndexNow: afvist — men deployet er allerede ude, så vi fejler ikke buildet.');
|
||||
}
|
||||
Reference in New Issue
Block a user