Publish: site root serves a "Not Found" page to non-browser clients — .md appended twice, permalink ignored

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.md URL.
  • Published folder: Site/
  • Home note: Site/About.md, frontmatter permalink: home
  • Reproduced twice, 2026-08-02.

Steps to reproduce

  1. Publish a vault subfolder, e.g. Site/.
  2. Set Site/About.md as the site home page.
  3. Give that note permalink: home in its frontmatter.
  4. Request each URL with a non-browser client (curl, or any agent/LLM fetcher):
    • https://example.com/home
    • https://example.com/Site/About
    • https://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 FoundFile 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:description and og:title, and the generated og:image URL is built from the same strings. A link preview of the bare domain therefore renders the error message as the site’s description.
  • canonical and og:url point 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.md URL, or only on custom domains.

Update: I cross-checked this against the official Obsidian Help site (obsidian.md/help), which is also served by Publish. Two new findings — the first one narrows the bug considerably.

1. The root bug does not reproduce on the official Help site

https://obsidian.md/help/ requested by a non-browser client returns the Home note correctly: no doubled extension, no error page, and a single-hop canonical to /help/Home.

Comparing the two home-note configurations:

Official Help (works) My site (broken)
Home note location vault root published subfolder (Site/About.md)
Filename Home.md, no spaces contains a space
permalink property on home note apparently none — canonical is the vault path yes (home)
Root served to non-browser clients Home note, rendered correctly File Site/About.md.md does not exist.
Canonical chain from root one hop → /help/Home two hops → /Site/About/home

So the failure is conditional on how the home note is configured, not universal. The trigger is one of (or a combination of): the home note living in a nested folder, a space in its filename, or a permalink property on the home note.

I can isolate it by toggling one variable at a time — renaming the home note to a space-free name, moving it to the vault root, or removing its permalink — happy to run these and report back if that helps triage.

2. Refinement on the advertised “append .md” convention

Now tested on both sites: appending .md works only on vault-path URLs and fails on permalink URLs.

  • obsidian.md/help/Home.md (vault path) → returns the raw markdown file (the browser downloads it rather than rendering)
  • obsidian.md/help/install.md (permalink) → the SPA’s “This page does not exist” screen (routed to /help/404)
  • my site’s permalink URLs fail the same way with .md appended; I have not tested its vault-path form

Since a note’s canonical URL is its permalink, a client that reads the canonical and then follows the meta instruction lands on a 404 every time. The instruction is only valid for the URL form that canonicals point away from — which also connects back to the root bug: both come down to .md being a file-space suffix applied in URL space.

what’s your website?

I was able to reproduce the underlying issue against my actual Publish site, https://karney.eu.

There is currently a public 301 from https://karney.eu/ to O mnie - Mikita Karney , so simply opening the bare domain no longer exposes the bug. However, the underlying Obsidian Publish custom-domain handler still reproduces it.

Actual configuration:

  • Home note: Publish/O mnie.md

  • Home note is in a nested folder

  • Filename contains a space

  • Permalink: pl

Using the Publish custom-domain endpoint:

curl -i \
  -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36; compatible; OAI-SearchBot/1.4; +https://openai.com/searchbot' \
  'https://publish.obsidian.md/serve?url=karney.eu/'

The response is HTTP 200, but the generated page contains:

File Publish/O mnie.md.md does not exist.

The server-generated metadata also shows the same problem:

<title>O mnie.md - Mikita Karney
preloadPage → .../Publish/O%20mnie.md.md
canonical → https://karney.eu/Publish/O+mnie
description → Not Found File Publish/O mnie.md.md does not exist.

So the previously unverified HTTP status can now be confirmed: this is a soft 404 (200 OK with a Not Found body).

The normal routes resolve the same note correctly:

https://karney.eu/pl
→ 200
→ correct note
→ canonical https://karney.eu/pl

https://karney.eu/Publish/O%20mnie
→ 200
→ correct note
→ canonical https://karney.eu/pl

I also reproduced the bad root response with OAI-SearchBot, Claude-SearchBot, and PerplexityBot user agents.

One correction to my earlier post: my statement about the advertised “append .md” convention was too broad.

With a recognized crawler user-agent, permalink URLs currently do resolve correctly when .md is appended. For example, https://karney.eu/pl.md redirects to the underlying Markdown file and returns text/markdown. The same is true for permalink URLs on the official Obsidian Help site. A normal browser/curl user-agent follows a different code path, which is what caused the conflicting observation.

So I retract the claim that .md only works for vault-path URLs. That appears to be user-agent-dependent (or may have changed since my original test).

The root issue itself remains reproducible: the server-side home-page resolver turns the configured home file Publish/O mnie.md into Publish/O mnie.md.md, while the normal permalink and vault-path resolvers both resolve the note correctly.

I still don’t know whether the trigger is the nested home-note path, the space in the filename, the permalink on the home note, or a combination of them.

thanks you, I see the problem