How it works
Why "outside the site" works
The resolution rules for content.directory in riebeckite.config.ts:
| Name | Role | Default / resolution base |
|---|---|---|
appRoot |
The HonoX/Vite application. Owns app/, public/, routes, generated styles, and build output |
Vite's root |
configRoot |
The directory that contains riebeckite.config.ts |
appRoot |
contentRoot |
The absolute filesystem root of the configured content directory or Obsidian vault | path.resolve(appRoot, content.directory) |
The key point: a relative content.directory is always relative to appRoot, and does not change when configRoot or the working directory changes. The integration resolves all three roots before running content or plugins, and the CLI uses the same result. So from a nested directory, a CI working directory, or an editor task, the same vault is always read.
Diagram source
flowchart TD
App["appRoot"]
Config["content.directory<br/>../vault"]
Resolve["path.resolve()"]
Content["contentRoot"]
App --> Resolve
Config --> Resolve
Resolve --> Content
// site/riebeckite.config.ts
export default defineConfig({
content: {
directory: "../vault", // relative to appRoot
},
});
Do not assemble the value from process.cwd(), and do not point appRoot at the vault. The vault is source data; the Vite application root stays the site.
How the roots are determined
Walking through the order reveals the common pitfalls.
- The CLI walks up from the working directory looking for
riebeckite.config.ts/.js/.mjs. The directory of the first match becomesconfigRoot. If none is found it fails withCould not find riebeckite.config.*. - appRoot is found by scanning under
configRootforvite.config.ts/.js/.mjs(skippingnode_modules,.git, andtests). Finding none is an error, and finding more than one is also an error. - contentRoot resolves as
path.resolve(appRoot, content.directory). An absolutedirectorylands on the same value here.
Diagram source
flowchart TD
CLI["Run CLI"]
Config["Find riebeckite.config.*"]
ConfigRoot["configRoot"]
Vite["Find vite.config.*"]
AppRoot["appRoot"]
Directory["content.directory"]
ContentRoot["contentRoot"]
CLI --> Config
Config --> ConfigRoot
ConfigRoot --> Vite
Vite --> AppRoot
AppRoot --> ContentRoot
Directory --> ContentRoot
If you intentionally keep riebeckite.config.ts outside the Vite application, pass configRoot and appRoot to riebeckiteVite(). Either way, the base for a relative content.directory stays appRoot.
In other words, "the same vault from anywhere" holds as long as the config is inside (or above) the site and appRoot is unambiguous. Running from an unrelated directory finds no config at all, and a monorepo with multiple vite.config.* files fails with Found multiple Vite applications. Both mean the tool could not pin down which site's config applies.
Reading content from the application side
The integration exposes the resolved config and a ready ContentManager as
framework-owned modules, so application code imports them rather than
re-reading riebeckite.config.ts:
import { config } from "virtual:riebeckite/config";
import { content } from "virtual:riebeckite/content";
config.content.directory is already absolute, so resolving it again against
another base is a common source of errors. A script that runs outside Vite can
resolve the same values with resolveHonoxConfig from
@riebeckite/honox/runtime.
What not to do
- Build
content.directoryfromprocess.cwd(). The result changes with where you run. - Point
appRoot(Vite'sroot) at the vault. That moves the site'sapp/,public/, and build output. - Assume a relative
directoryis relative toconfigRoot. The base is alwaysappRoot.
The rule of thumb:
Do not use
→ process.cwd()
Base
→ appRoot
The three patterns
| Pattern | Layout | Choose it when | Deployment consideration |
|---|---|---|---|
| A. One repository | Site and articles in one Git repository (use content/) |
You want a single repository for a personal blog | No extra setup |
| B. Separate folders, one repository | site/ and vault/ side by side in one repository |
You share history but want locations and visibility apart | Set content.directory: "../vault" on the site side |
| C. Separate repositories (recommended) | Articles private, site public | You want articles private or updates decoupled | CI needs setup to fetch the articles repository |
Pattern A layout
blog/
├─ riebeckite.config.ts ← appRoot (directory: "content")
├─ app/
├─ public/
└─ content/ ← articles live here
└─ index.md
The simplest option, and exactly what create-riebeckite generates. Article visibility matches the repository.
Pattern B layout
notes-repo/
├─ site/ ← appRoot (contains riebeckite.config.ts)
│ └─ riebeckite.config.ts
└─ vault/ ← referenced by directory: "../vault"
One repository and one history, with the locations split. Visibility is still per repository, as in A. The only change from A is content.directory: "../vault".
Sharing one vault across several sites
An extension of C: keep the vault in its own place and have several sites reference it.
workspace/
├─ notes/ ← shared vault (its own private repository)
├─ blog/ ← site 1 (directory: "../notes")
└─ docs/ ← site 2 (directory: "../notes")
Treat the vault as read-only source data and vary exclude, publishStrategy, and plugins per site. No site writes back, so the same notes can be published in different shapes. Deploy each site independently, giving each CI the additional checkout described in Fetching content is separate from triggering a deployment.
Diagram source
flowchart TD
Vault["Private vault"]
Vault --> Blog["Blog"]
Vault --> Docs["Docs"]
Blog --> BlogRules["Blog settings<br/>exclude / plugins"]
Docs --> DocsRules["Docs settings<br/>exclude / plugins"]
Each site can set exclude, publishStrategy, plugins, theme, and deployment independently, so the same note can have a different publication range and presentation per site.
Decision flow
- You want articles private, or updates decoupled → C.
- Articles and site can share visibility → next.
- You only want locations and settings apart → B. You want one repository → A.
- You want one vault for several sites → C + shared vault.