docs/dev/design/foam-publish-module-plan.md
foam-publish Module Planfoam-publish should be a build-time package that translates a Foam workspace into publishable artifacts for a website.
It should not be the website itself. Its job is to understand Foam semantics and emit clean outputs for a site layer such as Astro plus Starlight.
In shorthand:
Foam workspace -> foam-publish -> Markdown + JSON + assets -> site builder
The website framework is not the hard part of publishing Foam. The hard part is preserving Foam semantics on the web:
foam-query blocks where safeIf those concerns are pushed into the site layer, Foam becomes tightly coupled to one renderer. foam-publish exists to keep that logic owned by Foam and reusable across targets.
foam-publish should do five things:
foam-publish should not own:
Those belong in the site package.
The long-term dependency should be a proper extracted foam-core package.
In the current repository shape, the nearest reusable foundation lives under:
That means the first iteration of foam-publish will likely require some extraction work so that publish logic does not depend on VS Code-specific modules.
The implementation should start at:
packages/foam-vscode/src/publishOnce the API and boundaries are stable, it should be extracted to:
packages/foam-publishThis is a pragmatic sequencing choice, not a change in architecture. The code should be written from day one as if extraction were expected.
That means:
vscode imports in src/publish/**src/core/** are allowedThe publishing layer should expose a small programmatic API and a thin CLI.
Suggested primary API:
export interface PublishConfig {
workspaceRoot: string;
outDir: string;
baseUrl?: string;
target: 'generic-static' | 'astro-starlight';
clean?: boolean;
visibility?: {
mode: 'public-by-default' | 'private-by-default' | 'folder-based';
publicGlobs?: string[];
privateGlobs?: string[];
};
features?: {
backlinks?: boolean;
graph?: boolean;
searchIndex?: boolean;
noteEmbeds?: boolean;
foamQuery?: 'off' | 'safe-only';
};
}
export interface PublishArtifactSet {
notes: PublishedNote[];
assets: PublishedAsset[];
routes: PublishedRoute[];
graph?: unknown;
searchIndex?: unknown;
nav?: unknown;
}
export async function buildSite(
config: PublishConfig
): Promise<PublishArtifactSet>;
The API should favor inspectable outputs over opaque HTML blobs.
The CLI should stay thin and call the publishing API.
Suggested commands:
foam publish build
foam publish build --target astro-starlight
foam publish watch
foam publish export --out-dir .foam-publish
Whether these commands land in an existing Foam CLI package or a new CLI wrapper is a separate packaging decision. The important part is to keep the core publishing logic in the publishing module rather than spreading it across CLI code.
The logical API should look like this. During the initial in-repo phase, it would be exported from packages/foam-vscode/src/publish. After extraction, the same API can become the public surface of packages/foam-publish.
import { buildSite, publishAssets } from './publish';
await buildSite({
workspaceRoot: '/notes/my-foam',
outDir: '/notes/my-foam/.foam-publish',
target: 'astro-starlight',
visibility: {
mode: 'folder-based',
publicGlobs: ['docs/**', 'garden/**'],
},
includeAsset: publishAssets.content(),
features: {
backlinks: true,
graph: true,
searchIndex: true,
noteEmbeds: true,
foamQuery: 'safe-only',
},
});
This should produce a publishable intermediate output that a site package can consume directly.
For an Astro plus Starlight target, the emitted output should be inspectable and structured.
Example:
.foam-publish/
content/
note-a.md
projects/note-b.md
assets/
image.png
pdfs/paper.pdf
data/
graph.json
backlinks.json
search-index.json
routes.json
nav.json
This keeps the publishing output debuggable and lets the site layer focus on rendering instead of resolution.
The initial in-repo structure should be:
packages/foam-vscode/
src/
publish/
index.ts
config.ts
types.ts
bootstrap/
create-context.ts
load-workspace.ts
collect/
collect-notes.ts
collect-assets.ts
collect-routes.ts
filters/
apply-publish-rules.ts
should-publish-note.ts
should-publish-asset.ts
transform/
transform-note.ts
rewrite-links.ts
resolve-wikilinks.ts
resolve-anchors.ts
render-embeds.ts
render-queries.ts
materialize-backlinks.ts
normalize-frontmatter.ts
derive/
build-search-index.ts
build-graph-data.ts
build-nav-tree.ts
build-backlink-index.ts
build-route-manifest.ts
emit/
emit-markdown.ts
emit-json.ts
emit-assets.ts
write-output.ts
targets/
generic-static.ts
astro-starlight.ts
cli/
build.ts
watch.ts
Once extracted, the structure should map almost directly to:
packages/foam-publish/
src/
index.ts
config.ts
types.ts
bootstrap/
create-context.ts
load-workspace.ts
collect/
collect-notes.ts
collect-assets.ts
collect-routes.ts
filters/
apply-publish-rules.ts
should-publish-note.ts
should-publish-asset.ts
transform/
transform-note.ts
rewrite-links.ts
resolve-wikilinks.ts
resolve-anchors.ts
render-embeds.ts
render-queries.ts
materialize-backlinks.ts
normalize-frontmatter.ts
derive/
build-search-index.ts
build-graph-data.ts
build-nav-tree.ts
build-backlink-index.ts
build-route-manifest.ts
emit/
emit-markdown.ts
emit-json.ts
emit-assets.ts
write-output.ts
targets/
generic-static.ts
astro-starlight.ts
cli/
build.ts
watch.ts
This structure separates the actual content work from the target-specific output details.
bootstrap/Loads the workspace and constructs the Foam model needed by the publishing pipeline.
Responsibilities:
This layer must remain independent of VS Code APIs.
collect/Finds the candidate notes and assets that might be published.
Responsibilities:
This stage does not decide final visibility yet. It gathers the raw candidates.
filters/Applies visibility and publish rules.
Responsibilities:
The output of this stage is the actual publish set.
transform/This is the heart of the package. It turns Foam semantics into site-ready content.
Responsibilities:
[[wikilinks]] into canonical routesfoam-query blocks where safeThe website should not be responsible for understanding how Foam link resolution works. That belongs here.
derive/Builds metadata that the site can consume.
Responsibilities:
This is where the published site gets the data needed for graph view, backlinks, and navigation helpers.
emit/Writes the final artifact set.
Responsibilities:
This layer should be intentionally dumb. It writes already-prepared data.
targets/Defines small target-specific policies without owning Foam semantics.
Responsibilities:
Target adapters should stay thin. They should not reimplement transformation logic.
The build pipeline should read naturally:
In pseudocode:
const ctx = await createPublishContext(config);
const candidates = await collectPublishableResources(ctx);
const filtered = applyPublishRules(candidates, ctx);
const notes = await transformNotes(filtered.notes, ctx);
const derived = await deriveArtifacts(notes, filtered.assets, ctx);
await emitArtifacts(notes, derived, ctx);
Each stage should be testable in isolation.
Specific feature boundaries should be explicit.
rewrite-links.ts should:
resolve-anchors.ts should:
render-embeds.ts should:
render-queries.ts should:
materialize-backlinks.ts should:
The first implementation should stay narrow.
Suggested v1:
That is enough to prove the architecture without prematurely building graph UI, advanced embed rendering, or full query support.
Before the publishing layer can be clean, some code will likely need to move out of VS Code-oriented areas into framework-agnostic utilities.
Likely candidates:
The first implementation should prefer extraction over duplication.
The companion site package should consume foam-publish output, not raw workspace notes.
That means the site package can remain thin:
This keeps the website replaceable without changing the publishing core.
foam-query-js be explicitly unsupported for published builds in v1?After agreeing on this plan, the next implementation task should be a small scaffold under packages/foam-vscode/src/publish plus an extraction pass for the minimum reusable core needed by v1.
The extraction to packages/foam-publish should happen only after the API and internal boundaries have proven stable.