Symptom
Origin logs show 404s for paths that no template could plausibly build:
Not found: /{user}/views/{real-slug}/null (x24)
Not found: /{user}/views/null (x1, this one cost 3 upstream API calls)
Not found: /null (x1)Every URL builder in the codebase interpolates a non-nullable field, and grepping the rendered HTML of every page in the sitemap finds no href="null". Both observations are consistent and both are red herrings.
Cause
The three shapes are one bad href, resolved three ways. <a href="null"> is a schemeless relative reference:
- RFC 3986 resolution against
/u/views/slugreplaces the last segment ->/u/views/null - a naive crawler doing
base + "/" + href->/u/views/slug/null - the same href on a single-segment page ->
/null
So one occurrence in shared or generated content explains an entire scattered 404 family. Rendered-HTML greps come up empty because the offending value is data, not code: it lives in a generated content field, and the field gets regenerated, so it is gone hours later while the 404s persist in the logs.
Two mechanics make the literal string "null" (not None, not undefined) the one that escapes:
- Framework null-omission gives false confidence. Svelte drops
hrefentirely fornull/undefined(<a href={x}>), and a component like<svelte:element this={href ? 'a' : 'button'}>degrades to a non-link. Only a stringified null survives — which is exactly what${x}interpolation,JSON.stringify(null), or an LLM writingnullinto a URL slot produces. - Template guards are usually on the label, not the URL. A generated attribution block of the form
{?title}[{title}]({url}){/title}emits a link whenever the title is present. Verified in an ashes/dust template: a PythonNoneurl renders[T]()(harmless, resolves to the current page), while the string'null'renders[T](null)(a real 404 generator). Andoembed.get('author_url', '')does not apply its default to an explicit JSONnull, so third-party nulls flow straight through.
Worse, markdown post-processors often adopt schemeless hrefs as external instead of rejecting them — e.g. html.replace(/<a href="(?!\/)/g, '<a target="_blank" href="') happily marks href="null" as an external link.
Why tracing it is hard, and the one place the referrer survives
SvelteKit's unmatched-route 404 logs Error: Not found: <path> with no request context. handleError is invoked for it — verified in @sveltejs/kit@2.69.1: src/runtime/server/respond.js:707-718 constructs new SvelteKitError(404, 'Not Found', ...) and calls respond_with_error, which calls handle_error_and_jsonify -> options.hooks.handleError (src/runtime/server/utils.js:102-120). But the Sentry SvelteKit SDK filters 404s, so no issue exists to inspect.
The referrer was recoverable only because the same bad URL also produced a non-404 event: the SSR upstream timeout captured from handleFetch. Pull that event's request entry (not its tags) and the headers are there:
URL: https://site/{user}/views/null
Referer: https://site/{user}/views/{real-slug} <- the emitting page
UA: meta-externalagent/1.1 <- non-JS crawler, so it read SSR HTMLFixes
- Reject schemeless hrefs at the markdown chokepoint, don't adopt them. Allow
https?:,mailto:,/-rooted and#fragments; strip thehrefattribute otherwise so the text stays and the link dies. With sanitize-html this is atransformTags.aentry, and it must run inside sanitization (which sees parsed attribute values) rather than as a later regex. One rule retroactively neutralizes every already-stored bad string, with no data migration. - Validate at generation time too, and degrade to plain text rather than dropping the credit:
f"[{name}]({url})" if url_is_ok else name. - Make the fallout free. In SvelteKit, a param matcher (
src/params/*.js) runs before anyload, so a rejection costs zero backend calls. Reject the stringified-empty family (null,undefined,none,nan) and mirror your writer's charset validator. Before this, each junk hit fanned out to three API calls. - Log 404s that carry a
Referer— one JSON line withpath,route(nullfor matcher rejections, set when a load threw),referer,ua. Requests without a referrer are scanner noise; requests with one are self-inflicted and name the page to fix.
Transferable rule
Any user- or model-authored string that reaches an href must be scheme-checked, because a relative href is a request against your own origin. Framework null-omission protects you from real nulls and not at all from the string "null" — which is the form every interpolation, JSON round-trip and LLM produces.