On a Publish site with a custom domain, the site root returns a “Not Found” page to any non-browser client. The error text shows the file extension appended twice:
File Site/About.md.md does not exist.
The same note requested over its permalink or over its vault path is served correctly. Browsers are not affected in the end — the SPA router rewrites the root to the permalink URL a moment after load — but everything that is not a browser (crawlers, link-preview bots, agent fetchers) receives the error page, including its meta tags.
Environment
- Obsidian Publish, custom domain. Not tested on the default
*.publish.obsidian.mdURL. - Published folder:
Site/ - Home note:
Site/About.md, frontmatterpermalink: home - Reproduced twice, 2026-08-02.
Steps to reproduce
- Publish a vault subfolder, e.g.
Site/. - Set
Site/About.mdas the site home page. - Give that note
permalink: homein its frontmatter. - Request each URL with a non-browser client (curl, or any agent/LLM fetcher):
https://example.com/homehttps://example.com/Site/Abouthttps://example.com/
Actual result
| URL | Response | <title> |
canonical |
|---|---|---|---|
/home |
note served correctly | About |
https://example.com/home |
/Site/About |
note served correctly | About |
https://example.com/home |
/ |
Not Found — File Site/About.md.md does not exist. |
About.md |
https://example.com/Site/About |
Additional symptoms on the root response:
- The error string is written into
description,og:descriptionandog:title, and the generatedog:imageURL is built from the same strings. A link preview of the bare domain therefore renders the error message as the site’s description. canonicalandog:urlpoint at the vault-path URL (/Site/About) rather than at the permalink URL (/home), which every other page uses.
Browser behaviour (for contrast)
Loading the root in a browser ends up on the right page: the client-side router replaces the URL with /home shortly after load and the title corrects itself to About. During the first paint the title is About.md, but that is transient.
This is the interesting part: the client-side router resolves root → permalink correctly, while the server-side path does not. The two disagree about what the root is.
Expected result
/serves the home note to every client, not just to browsers./reports the same title and the same canonical URL as/home.- One canonical URL per note.
Hypothesis
Path resolution looks like file = resolve(urlPath) + ".md".
The permalink branch and the vault-path branch resolve to a name without an extension, so appending .md yields a valid path. The root falls back to a stored “home file” value that already contains .md, and the extension is appended a second time.
The root title (About.md) supports this: it is derived from a value that still carries the extension. The root branch also appears to skip permalink resolution entirely, which would explain the canonical pointing at the vault path.
More generally, .md is a file extension being appended to a permalink, which is a URL slug and not a path to a file. Mixing URL space and file space is what makes a collision like this possible at all; content negotiation (Accept: text/markdown) or a query parameter would not have this failure mode.
Related: three URLs, two-hop canonical chain
The same note is reachable at /, /home and /Site/About. The root canonicalizes to /Site/About, which canonicalizes to /home. Search engines follow canonical chains unreliably; the root should point straight at the permalink URL, or redirect to it server-side.
Related: the advertised .md convention
The server-rendered response carries a meta tag instructing the reader to append .md to the URL to get raw markdown. (I read it through a client that normalises meta tags, so the exact attribute spelling should be checked against the raw HTML — it appeared as llm:instruction. It is present only in the server-rendered variant; it does not exist in the browser DOM, where the head carries only viewport, description, og:site_name and og:description.)
Two observations about it:
- Non-browser clients already receive markdown for every URL without appending anything, so following the instruction is redundant for its intended audience.
- A browser that follows it literally gets the SPA’s “This page does not exist” screen, because routing is client-side and no such route exists.
Impact
The bare domain is the URL people put in profiles, signatures and messages, and the first URL a crawler reaches from any backlink. Every non-browser client currently lands on an error page whose meta description is the error text, and no links out of it — so a crawler arriving at the site has no path into the rest of it.
Not verified
- Whether the root returns HTTP 404 or HTTP 200 with a Not Found body (soft 404). I could not capture the status code, and it matters for triage.
- Whether the same happens on the default
*.publish.obsidian.mdURL, or only on custom domains.