PROJECT MAMBO
Change colour theme

TypeScript Output

Purpose

Rust emits typed content data, not HTML strings and not one handwritten-style React page per Markdown file. A stable TypeScript runtime converts this data into React components.

This boundary provides:

  • Compile-time validation with satisfies.

  • No Markdown parsing in Node.js or the browser.

  • A replaceable visual layer.

  • Static route discovery for Next.js.

  • Structured content for navigation, search, RSS, sitemaps, and future renderers.

  • Diagnostics before next build begins.

Generated directory

The current generated content structure is:

src/generated/mambo/
├── .mambosite-generated
├── manifest.ts
├── theme.ts
├── pages/
│   ├── p_01h....ts
│   ├── p_02a....ts
│   └── index.ts

Generated theme and content assets are separate from TypeScript:

public/mambo/
├── .mambosite-generated
├── theme.css
└── assets/
    └── ...

Navigation, search, and build information are planned extensions. Content assets are copied as bytes into the managed asset tree; rewritten public URLs are stored where those references already occur in page data. Each generated TypeScript content module begins with a clear “Generated by MamboSite; do not edit” comment. The manifest, page records, generated theme metadata, and ownership markers record their schema version; the aggregate pages/index.ts module does not repeat it. Output paths must be stable across operating systems.

Stable identifiers

Each page receives a stable PageId derived from its logical source path and physical-or-mounted route identity—not from an absolute path, title, content, or array position. Human-readable routes are stored separately.

Requirements:

  • The same source and mount configuration produce the same ID.

  • Renaming or remounting a source may change its ID and route.

  • Editing body content does not change its ID.

  • IDs contain only safe ASCII characters.

  • Content assets retain normalized relative paths in schema 1; hash-derived names and deduplication remain optional future work.

Generated filenames use IDs rather than titles to avoid Unicode, case, and path-length problems.

Runtime type contract

The runtime package owns the public TypeScript interfaces. The current page boundary is:

export type PageId = string;

export interface SourceSpan {
  readonly start: { readonly line: number; readonly column: number };
  readonly end: { readonly line: number; readonly column: number };
  readonly startByte?: number;
  readonly endByte?: number;
}

export interface PageRecord {
  readonly schemaVersion: number;
  readonly id: PageId;
  readonly route: string;
  readonly sourcePath: string;
  readonly title: string;
  readonly description?: string;
  readonly status: "published" | "draft";
  readonly listed: boolean;
  readonly date?: string;
  readonly updated?: string;
  readonly tags: readonly string[];
  readonly aliases: readonly string[];
  readonly order?: number;
  readonly cover?: string;
  readonly data: Readonly<Record<string, JsonValue>>;
  readonly extra: Readonly<Record<string, JsonValue>>;
  readonly headings: readonly HeadingRecord[];
  readonly blocks: readonly BlockRecord[];
  readonly directives: readonly ValidatedDirective[];
  readonly body: MarkdownNode;
  readonly children: readonly PageId[];
  readonly outgoingLinks: readonly ResolvedLink[];
  readonly embeds: readonly ResolvedEmbed[];
  readonly backlinks: readonly PageId[];
}

sourcePath is a normalized path relative to the configured content root. It is independent of the authoring tool and never exposes the author's absolute filesystem path.

The Markdown node union is explicit and closed for each schema version. It mirrors the owned parser AST and includes CommonMark/GFM blocks and inlines plus the supported optional dialect nodes. A shortened view is:

export type NodeKind =
  | DocumentNode
  | HeadingNode
  | ParagraphNode
  | CodeBlockNode
  | ListNode
  | ListItemNode
  | BlockQuoteNode
  | TableNode
  | ThematicBreakNode
  | TextNode
  | EmphasisNode
  | StrongNode
  | StrikethroughNode
  | HighlightNode
  | InlineCodeNode
  | LinkNode
  | ImageNode
  | WikiLinkNode
  | ObsidianEmbedNode
  | AlertNode
  | MathNode
  | DirectiveNode;

export type MarkdownNode = NodeKind & {
  readonly span?: SourceSpan;
  readonly children?: readonly MarkdownNode[];
  readonly blockId?: string;
};

The complete exported union also includes the remaining documented schema-1 variants; the abbreviated list above is not a second contract.

Nodes contain only serializable data, child nodes, and optional source spans. Authored note links and embeds sit beside their resolved graph edges; explicit assets/... values are emitted as validated public URLs directly in the tree and directive data. Generated records never contain functions, React elements, class names, or executable source.

Component nodes

Body directives lower into registry-validated directive nodes. The generated page also carries a normalized directive list for inspection:

export interface ValidatedDirective {
  readonly name: string;
  readonly form: "leaf" | "container";
  readonly properties: Readonly<Record<string, DirectiveValue>>;
  readonly span?: SourceSpan;
}

Properties remain serializable tagged values. Rust validates names, types, contexts, defaults, and enum choices before generation; the React registry converts those values into typed component models.

Rust applies defaults before generation. The TypeScript runtime receives normalized properties and does not repeat source-language validation.

Embed nodes

Resolved note embeds remain explicit graph edges while their authored AST nodes stay in the Markdown tree:

export interface ResolvedEmbed {
  readonly authoredDestination: string;
  readonly option?: string;
  readonly instanceId: string;
  readonly target: ResolvedLinkTarget;
  readonly span?: SourceSpan;
}

The runtime uses the compiler-resolved edge when rendering a whole-page Obsidian embed. Whole-page include directives currently use content-store lookup; compiler-resolved directive targets and fragment transclusion remain later schema work.

Manifest

manifest.ts contains site-wide information required before any page is rendered:

export interface SiteManifest {
  readonly schemaVersion: number;
  readonly generatedAt?: number;
  readonly site: {
    readonly title: string;
    readonly url?: string;
    readonly basePath: string;
    readonly language: string;
    readonly trailingSlash: boolean;
  };
  readonly entryPage: PageId;
  readonly routes: Readonly<Record<string, PageId>>;
  readonly pages: readonly PageSummary[];
}

generatedAt is Unix epoch seconds recorded once by each output-producing CLI build. It is omitted from direct core-compiler models and controlled by SOURCE_DATE_EPOCH when reproducible output is required. PageSummary contains identity, routing, display metadata, data, and direct child IDs. Full AST, headings, directives, links, embeds, and backlinks stay in page modules.

The route table includes / and every compiled route. Draft summaries remain in schema-1 output; the Next adapter omits them from static parameters.

Page modules

A generated page module follows this form:

import type { PageRecord } from "@mambosite/runtime";

const page = {
  schemaVersion: 1,
  id: "p_...",
  route: "/mambodot/commands/",
  sourcePath: "_mounts/mambodot/Commands.md",
  title: "Commands",
  listed: true,
  status: "published",
  tags: [],
  aliases: [],
  data: {},
  extra: {},
  headings: [],
  blocks: [],
  directives: [],
  body: { type: "document", children: [] },
  children: [],
  outgoingLinks: [],
  embeds: [],
  backlinks: [],
} as const satisfies PageRecord;

export default page;

The generator controls property order and formatting so diffs remain readable. Strings must be escaped as TypeScript literals safely. Generated source is formatted by the generator; running a general formatter must not be required for correctness.

Page index

pages/index.ts provides eager, statically analyzable imports and lookups for the web shell:

import pageA from "./p_a";
import pageB from "./p_b";

export const pagesById = {
  [pageA.id]: pageA,
  [pageB.id]: pageB,
} as const;

The current module also exports the ordered pages array used to create the runtime content store.

If the web runtime bundle becomes significant, code generation may switch to generated static import functions without changing Markdown or the page schema. Any lazy approach must remain compatible with static route generation.

The current runtime derives collections from compiler-generated children relationships. The default header reads an explicit data.navigation array from the entry page. A future navigation.ts may store a separately normalized hierarchy.

search.ts is not emitted in the current milestone. When added, it will contain pre-normalized records such as page ID, title, description, tags, headings, route, and plain searchable text. The compiler will exclude comments, code when configured, and hidden metadata. A client-side search index may be constructed from these records by the web shell.

Source locations

Schema 1 emits available source spans in every build; there is no production-stripping option yet. Page source paths are content-root relative, and absolute host paths are forbidden in generated output.

Schema versioning

The compiler, generated modules, and runtime share an integer schema version.

  • Compiler and runtime must fail clearly when their supported versions do not overlap.

  • Additive optional fields may remain within a schema version when defaults are defined.

  • Renamed fields, changed meanings, or node-union changes require a schema increment.

  • The additive generatedAt manifest field records the build instant; a future build-information field or module may also record the compiler version for diagnostics.

Deterministic and atomic generation

For the same normalized inputs, configuration, and build epoch, generated bytes must be identical.

This guarantee covers the TypeScript content writer described here. An ordinary CLI build records the current epoch in manifest.ts and rerolls the separately generated theme CSS collection-accent order. SOURCE_DATE_EPOCH fixes both values; page modules remain deterministic either way. See Theme and Components.

The current writer:

  1. Sorts pages/modules and serializes map keys deterministically while preserving authored array order.

  2. Writes into a temporary sibling directory.

  3. Completes one managed tree before publication.

  4. Replaces the previous generated directory atomically where supported and restores it when publication fails.

  5. Removes stale generated files only inside a marked managed output directory.

Compiler or theme-validation errors publish nothing. TypeScript and the combined theme/content-asset output are separate managed trees, so their replacement is not one cross-directory transaction.

Deliberate exclusions

The generated layer does not contain:

  • Raw Markdown requiring reparsing.

  • Prebuilt React elements.

  • Arbitrary HTML from authors.

  • Site-specific CSS classes.

  • Absolute filesystem paths.

  • Network-fetched content.

  • Build timestamps in page modules; the single build instant belongs in manifest.ts.

  • One generated page.tsx for every Markdown page.