fix(spa): prerender-build laeuft + meta/json-ld korrekt

Drei build-blocker beim ersten prerender-versuch identifiziert und gefixt:

1. svelte.config.js: handleHttpError + handleMissingId fuer den
   prerender-crawler. Der crawler folgt zur build-zeit allen hrefs/srcs
   im HTML — sieht dabei
   - __SITE_URL__-platzhalter in canonical/hreflang (werden im deploy
     per sed ersetzt, sind keine echten routes)
   - relative bild-paths in alten posts (z.B. h01-json-import.png)
   - anchor-links auf headings ohne id-attribute (#ACF-JSON-Export)
   Alle drei sind keine echten 404s — handlers ignorieren sie.

2. +page.svelte: <script type="application/ld+json">{jsonLd}</script>
   in <svelte:head> rendert {jsonLd} als literalen string, weil svelte
   den script-tag-inhalt nicht als expression evaluiert. Zurueck zu
   {@html ...} mit </script>-escape-hardening, damit titel oder
   beschreibungen mit </script> den output nicht aufbrechen koennen.

3. app.html behaelt seine homepage-defaults fuer og:title/og:url/
   og:description/canonical — der prerender-crawler rendert nur
   detail-routen (/<slug>/), die homepage bleibt SPA-only und braucht
   die defaults im app.html-template, weil dort kein svelte:head greift.
   Detail-routen ueberschreiben per <svelte:head>; last-wins greift bei
   LinkedIn/Mastodon/Browser. Facebook/Twitter (first-wins) haetten
   einen homepage-prerender-schritt noetig — folge-aufgabe.

Plus snapshot/deno.lock committed — deno empfiehlt lockfile-commit fuer
reproduzierbare CI-builds, analog package-lock.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-28 08:50:27 +02:00
parent 2ad27adf1f
commit 3e31caacef
4 changed files with 203 additions and 1 deletions

View File

@ -10,6 +10,13 @@
<meta property="og:description" content="Jörg Lohrer Blog (Nostr-basiert)" />
<link rel="canonical" href="__SITE_URL__/" />
<meta name="robots" content="index, follow" />
<!--
Detail-seiten (prerender=true) hängen via %sveltekit.head% ihre
eigenen og:title/description/url/canonical hinten an. Last-wins
gilt fuer LinkedIn/Mastodon/Browser; Facebook/Twitter nehmen
tendenziell first-wins — fuer perfekte OG-tags muesste die
homepage auch prerendered werden (separate aufgabe).
-->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />

View File

@ -112,7 +112,7 @@
<link rel="alternate" hreflang={alt.lang} href={`${siteUrl}/${alt.slug}/`} />
{/each}
<link rel="alternate" hreflang="x-default" href={canonical} />
<script type="application/ld+json">{jsonLd}</script>
{@html `<script type="application/ld+json">${jsonLd.replace(/<\/script>/gi, '<\\/script>')}</script>`}
{/if}
</svelte:head>

View File

@ -18,6 +18,29 @@ const config = {
}),
alias: {
$lib: 'src/lib'
},
prerender: {
// Der Crawler folgt zur Build-Zeit href/src-attributen im HTML. Zwei
// faelle, in denen 404er kein echter fehler sind:
//
// 1. canonical/hreflang enthalten den `__SITE_URL__`-platzhalter, der
// erst beim deploy per sed durch die echte SITE_URL ersetzt wird.
// Pfade wie `/<slug>/__SITE_URL__/` sind also pseudo-pfade.
// 2. Bild-references mit relativen pfaden (z.B. `h01-json-import.png`)
// in alten posts, die nicht zu Blossom-URLs migriert wurden — die
// sind im post-body als <img src="..."> und vom crawler verfolgte
// pseudo-routes. Die SPA selbst rendert die <img>-tags zwar, aber
// eine 404-route gibt es dafuer nicht.
handleHttpError: ({ path, message }) => {
if (path.includes('__SITE_URL__')) return;
if (/\.(png|jpe?g|gif|webp|svg|avif)\/?$/i.test(path)) return;
throw new Error(message);
},
// Markdown-headings bekommen ohne slugify-plugin keine id-attribute.
// Anchor-links in alten posts (z.B. [link](#ACF-JSON-Export)) sind
// damit zur build-zeit unauffindbar. Kein render-fehler — die SPA
// scrollt im browser entweder zum element oder garnicht.
handleMissingId: 'ignore'
}
}
};

172
snapshot/deno.lock Normal file
View File

@ -0,0 +1,172 @@
{
"version": "5",
"specifiers": {
"jsr:@std/assert@^1.0.6": "1.0.19",
"jsr:@std/cli@^1.0.6": "1.0.28",
"jsr:@std/fs@^1.0.4": "1.0.23",
"jsr:@std/internal@^1.0.12": "1.0.12",
"jsr:@std/path@^1.0.6": "1.1.4",
"jsr:@std/path@^1.1.4": "1.1.4",
"npm:applesauce-relay@2": "2.3.0",
"npm:nostr-tools@^2.10.4": "2.23.3",
"npm:rxjs@^7.8.1": "7.8.2"
},
"jsr": {
"@std/assert@1.0.19": {
"integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/cli@1.0.28": {
"integrity": "74ef9b976db59ca6b23a5283469c9072be6276853807a83ec6c7ce412135c70a",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/fs@1.0.23": {
"integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37",
"dependencies": [
"jsr:@std/internal",
"jsr:@std/path@^1.1.4"
]
},
"@std/internal@1.0.12": {
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
},
"@std/path@1.1.4": {
"integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
"dependencies": [
"jsr:@std/internal"
]
}
},
"npm": {
"@noble/ciphers@2.1.1": {
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="
},
"@noble/curves@2.0.1": {
"integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==",
"dependencies": [
"@noble/hashes@2.0.1"
]
},
"@noble/hashes@1.8.0": {
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="
},
"@noble/hashes@2.0.1": {
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="
},
"@scure/base@1.1.1": {
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
},
"@scure/base@1.2.6": {
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="
},
"@scure/base@2.0.0": {
"integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w=="
},
"@scure/bip32@2.0.1": {
"integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==",
"dependencies": [
"@noble/curves",
"@noble/hashes@2.0.1",
"@scure/base@2.0.0"
]
},
"@scure/bip39@2.0.1": {
"integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==",
"dependencies": [
"@noble/hashes@2.0.1",
"@scure/base@2.0.0"
]
},
"applesauce-core@2.3.0": {
"integrity": "sha512-rMVrwGMgHxXAHZfrq3ibtMjljAxeEfT95nl5VYLl5mSMmOHXnwjbiPTccJ2UDd6GP+INdHfkPgeB8AOUf5DFog==",
"dependencies": [
"@noble/hashes@1.8.0",
"@scure/base@1.2.6",
"debug",
"fast-deep-equal",
"hash-sum",
"light-bolt11-decoder",
"nanoid",
"nostr-tools",
"rxjs"
]
},
"applesauce-relay@2.3.0": {
"integrity": "sha512-tOijiN1yVyORS5jT5mXe8MTzqc1IVq/AdJXOzTe3uQgeDYhJzQ9lNYgqejDBXW1ahUThsRZgX2RybkOHVjBuHA==",
"dependencies": [
"@noble/hashes@1.8.0",
"applesauce-core",
"nanoid",
"nostr-tools",
"rxjs"
]
},
"debug@4.4.3": {
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": [
"ms"
]
},
"fast-deep-equal@3.1.3": {
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"hash-sum@2.0.0": {
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg=="
},
"light-bolt11-decoder@3.2.0": {
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
"dependencies": [
"@scure/base@1.1.1"
]
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"nanoid@5.1.7": {
"integrity": "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==",
"bin": true
},
"nostr-tools@2.23.3": {
"integrity": "sha512-AALyt9k8xPdF4UV2mlLJ2mgCn4kpTB0DZ8t2r6wjdUh6anfx2cTVBsHUlo9U0EY/cKC5wcNyiMAmRJV5OVEalA==",
"dependencies": [
"@noble/ciphers",
"@noble/curves",
"@noble/hashes@2.0.1",
"@scure/base@2.0.0",
"@scure/bip32",
"@scure/bip39",
"nostr-wasm"
]
},
"nostr-wasm@0.1.0": {
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="
},
"rxjs@7.8.2": {
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dependencies": [
"tslib"
]
},
"tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
}
},
"workspace": {
"dependencies": [
"jsr:@std/assert@^1.0.6",
"jsr:@std/cli@^1.0.6",
"jsr:@std/encoding@^1.0.5",
"jsr:@std/fs@^1.0.4",
"jsr:@std/path@^1.0.6",
"jsr:@std/testing@^1.0.3",
"jsr:@std/yaml@^1.0.5",
"npm:applesauce-relay@2",
"npm:nostr-tools@^2.10.4",
"npm:rxjs@^7.8.1"
]
}
}