Publication and verification
Publication rules and assets
How publication is decided
content.filters.publishStrategy takes two values.
| Value | Published when | What it means |
|---|---|---|
explicit (default) |
The publish frontmatter is true |
Only notes that opt in |
selective |
Neither private nor draft is true |
Everything except notes that opt out |
The decision is centralized in isPublished / isPublishable, and note rendering, diagnostics, and the prebuild asset collection all use the same rule. Do not reimplement the publication decision on the site side; that is how "visible in preview, missing in production" disagreements start.
Diagram source
flowchart LR
Vault["Private vault"]
Publish{"publish: true?"}
Vault --> Publish
Publish -->|"Yes"| Public["Published"]
Publish -->|"No"| NotPublic["Not published"]
For a private vault, the default explicit is the easier model to reason about.
How to write exclude
exclude matches glob patterns against the path relative to contentRoot (normalized to / separators). Note that patterns are anchored to the whole path.
| Pattern | Matches | Does not match |
|---|---|---|
.obsidian/** |
.obsidian/app.json |
notes/.obsidian/app.json |
**/.obsidian/** |
both of the above | — |
Templates/** |
Templates/daily.md |
notes/Templates/daily.md |
**/Templates/** |
both of the above | — |
private/** |
private/secret.md |
notes/private/secret.md |
* stays within one segment; ** crosses segments. If a folder of the same name can appear in subfolders, prefix **/ to be safe. That is why the scaffold defaults use **/templates/** and **/private/**.
exclude filters before loading, so excluded notes never appear in link resolution or the graph. For notes you want private, in addition to leaving off publish: true, exclude the whole folder where possible.
Content images and attachments are published differently
Vault files fall into three kinds, and each one is published by a different owner:
| Kind | Subject | Public URL | Published by |
|---|---|---|---|
| Content image | An image (png, jpg, svg, …) |
/<relative logical path from the vault> |
The build, as generated output |
| Attachment / Media | A file that is neither Markdown nor an image | /assets/attachments/<relative logical path from the vault> |
A prebuild step on the site side |
| Static asset | A file the site application owns | anywhere under / |
Vite's public/ directory |
Content images need nothing from the site side: obsidianMarkdown() writes every image referenced from a published page into the build output, keeping its logical path, and the development server serves the same logical path straight from the content. Images that nothing references, or that only non-public pages reference, are not written.
Diagram source
flowchart LR
Image["Content image<br/>assets/logo.png"]
Attach["![[attachments/report.pdf]]"]
Image -->|"Build writes it"| Public["Public asset"]
Attach -->|"URL only"| Copy["Prebuild copy"]
Copy --> Public
Attachments and media are different. An embed such as:
![[attachments/report.pdf]]
produces a URL, but the file itself is not copied into the public directory. Add a prebuild step on the site side that copies only the files you publish. Reference implementation: website/scripts/build_images.ts (called from the prebuild script, tsx scripts/build_images.ts, copying into public/assets/attachments/).
That implementation works like this:
- Walk contentRoot to enumerate images (
IMAGE_EXTENSIONS) and attachments (isAttachmentPath). - Build the content with
ContentManagerand collect only the assets referenced by published notes — from links and fromsrc/hrefin the rendered HTML. - Copy only referenced assets into
public/assets/attachments/<relative path from the vault>(images intopublic/<relative path>, so the dev server can serve them before the first build), skipping the copy when size and mtime are unchanged. - Delete orphaned attachments that exist in public but not in content.
Diagram source
flowchart TD
Vault["Vault assets"]
PublicNotes["Published notes"]
Referenced["Referenced assets"]
Copy["Copy"]
Public["Public assets"]
Vault --> Referenced
PublicNotes --> Referenced
Referenced --> Copy
Copy --> Public
In short, the rule is "copy because a published note references it", not "copy because it exists in the vault". Do not take the shortcut of copying the entire vault. It risks leaking private notes, images referenced only from non-public pages, unreferenced attachments, and .obsidian metadata. Content images are already filtered and emitted by the build; attachment and media publishing stays the site application's responsibility, so copy only the files a published note references. To detect a published note that crosses the boundary by linking to an unpublished one, run riebeckite-diagnostics from @riebeckite/plugin-diagnostics; it reports those references as publish-boundary warnings, and runDiagnostics() produces the same result programmatically. See Diagnostics.
The generated URL
A content image keeps its logical path from the vault:
Vault:
assets/logo.png
Public URL:
/assets/logo.png
Attachments and media use a dedicated prefix so that a file and an image with the same name cannot collide:
/assets/attachments/<relative logical path from the vault>
For example:
Vault:
attachments/report.pdf
Public URL:
/assets/attachments/attachments/report.pdf
attachment()reads the embedded file's size from the resolved vault root and rejects paths outside the root.media()renders the matching audio/video for the same logical path.
Keeping the physical location under public/ consistent with the visible URL reduces post-build 404s.
Repository visibility vs. publication boundary
Making a repository private and deciding what Riebeckite publishes are separate concerns:
Repository visibility
→ who can read the Git repository
Riebeckite publication
→ what ends up in the site
Even with a private vault, a build that copies non-public information into the public output publishes it. Keep three things aligned:
exclude
+
publishStrategy
+
asset copy that targets only published notes
In particular, do not copy the whole vault into public/. The prebuild copy selects only referenced assets, as described above.
Verification
You can confirm roots and publication boundaries before building, with the CLI. Run it from a nested directory to prove the working directory does not matter.
cd site/app
npm exec riebeckite check
npm exec riebeckite doctor
npm exec riebeckite inspect config
npm exec -- riebeckite inspect content --list
npm exec riebeckite inspect graph
npm exec riebeckite build
Check the results in this order:
checkvalidates the configuration and plugin contracts.doctorreports unreadable or invalid filesystem content sources.- In
inspect config,Directoryis the resolved absolute path,PublishingispublishStrategy, andExcludeis the pattern count. Settle where the vault points here first. - Before investigating WikiLinks or embeds, use
inspect content --listto confirm the expected logical paths are loaded. - Use
inspect graphto confirm notes you excluded are not appearing as nodes. buildvalidates the integration and route rendering.
Diagram source
flowchart LR
Check["check"]
Doctor["doctor"]
Config["inspect config"]
Content["inspect content"]
Graph["inspect graph"]
Build["build"]
Check --> Doctor
Doctor --> Config
Config --> Content
Content --> Graph
Graph --> Build
Confirming the working directory does not matter
To investigate root-resolution problems, run the CLI not only from the site root but also from a nested directory:
cd site/app
npm exec riebeckite inspect config
npm exec -- riebeckite inspect content --list
Both should resolve the same application and vault. Running from an unrelated directory can fail to find the config at all.