@moku-labs/web 0.6.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,10 @@
1
1
  import { EmitFn } from "@moku-labs/core";
2
2
  import { ComponentChildren, VNode } from "preact";
3
+ import { BundledTheme, ThemeRegistrationAny } from "shiki";
4
+ import { Pluggable, Processor } from "unified";
3
5
 
4
6
  //#region src/plugins/log/types.d.ts
5
- declare namespace types_d_exports$3 {
7
+ declare namespace types_d_exports$4 {
6
8
  export { ExpectChain, LogApi, LogConfig, LogEntry, LogLevel, LogSink, LogState };
7
9
  }
8
10
  /**
@@ -142,7 +144,7 @@ type LogApi = {
142
144
  addSink(sink: LogSink): void; /** Clear all recorded entries while keeping registered sinks. */
143
145
  reset(): void;
144
146
  };
145
- declare namespace types_d_exports$1 {
147
+ declare namespace types_d_exports$2 {
146
148
  export { EnvApi, EnvConfig, EnvProvider, EnvState, EnvVarSpec };
147
149
  }
148
150
  /**
@@ -153,8 +155,9 @@ declare namespace types_d_exports$1 {
153
155
  *
154
156
  * Providers are walked in array order during resolution; the first provider to
155
157
  * return a non-`undefined` (and non-empty-string) value for a key wins. `load()`
156
- * is called once per resolution at `onInit` time except for live, per-request
157
- * sources (e.g. {@link cloudflareBindings}) which read fresh on every call.
158
+ * is called exactly once per resolution at `onInit` time, after which both env
159
+ * maps are frozen. A provider like {@link cloudflareBindings} reads `globalThis`
160
+ * at that single `onInit` call (not per request).
158
161
  *
159
162
  * @example
160
163
  * ```ts
@@ -225,7 +228,8 @@ type EnvConfig = {
225
228
  /**
226
229
  * Ordered list of value sources. The first provider yielding a non-`undefined`
227
230
  * (and non-empty-string) value for a key wins. The plugin's own spec default is
228
- * `[]`; the framework layer supplies the working default `[dotenv(), processEnv()]`.
231
+ * `[]`; the consumer supplies the providers per target (`[dotenv(), processEnv()]`
232
+ * on Node) — only the `/browser` entry pre-wires `browserEnv()` out of the box.
229
233
  */
230
234
  providers: EnvProvider[];
231
235
  /**
@@ -334,12 +338,25 @@ declare function browserEnv(options?: {
334
338
  declare const envPlugin: import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>;
335
339
  //#endregion
336
340
  //#region src/config.d.ts
341
+ /**
342
+ * Deployment stage. Drives content draft visibility — drafts are suppressed
343
+ * only in `"production"`; `"development"` and `"test"` both surface them.
344
+ */
345
+ type Stage = "production" | "development" | "test";
337
346
  /**
338
347
  * Global framework configuration. Minimal by design — per-plugin config is
339
348
  * resolved via `pluginConfigs`, not merged here.
340
349
  */
341
- type Config$4 = {
342
- /** Runtime mode. Drives log sink defaults, content draft filtering, build minify. */mode: "production" | "development";
350
+ type Config$5 = {
351
+ /** Deployment stage. Drives content draft visibility (drafts hidden only in `"production"`). */stage: Stage;
352
+ /**
353
+ * Render mode — the single SSG/DATA/SPA switch, read by the router (`ctx.global`)
354
+ * and consumed by `build`/`spa` via `router.mode()`.
355
+ * - `"ssg"` static generation only (no client router emitted).
356
+ * - `"spa"` client-side routing only.
357
+ * - `"hybrid"` static HTML + client navigation overlay (default).
358
+ */
359
+ mode: "ssg" | "spa" | "hybrid";
343
360
  };
344
361
  /**
345
362
  * Framework event contract. Empty base — each plugin declares its own events
@@ -373,7 +390,7 @@ type Events = {};
373
390
  * });
374
391
  * ```
375
392
  */
376
- type Config$3 = {
393
+ type Config$4 = {
377
394
  /** Human-readable site name. Used in feeds, og:site_name, and titles. MUST be non-empty. */name: string; /** Absolute base URL of the site, e.g. "https://blog.dev". MUST be a valid absolute URL (http/https). */
378
395
  url: string; /** Default author/byline for the site. Used in feeds and article author meta. */
379
396
  author: string; /** Short site description. Used in feeds, the default meta description, and og:description fallbacks. */
@@ -383,7 +400,7 @@ type Config$3 = {
383
400
  * Public API of the site plugin — read-only accessors over frozen global
384
401
  * site metadata, plus canonical URL construction.
385
402
  */
386
- type Api$3 = {
403
+ type Api$4 = {
387
404
  /**
388
405
  * Returns the configured site name.
389
406
  *
@@ -444,7 +461,7 @@ type Api$3 = {
444
461
  * @file i18n plugin — public type definitions (Config + Api).
445
462
  */
446
463
  /**
447
- * i18n plugin configuration. Mirrors the legacy `I18nConfig` shape.
464
+ * i18n plugin configuration.
448
465
  *
449
466
  * `locales` and `defaultLocale` are required and validated in `onInit`. The
450
467
  * optional maps default to empty objects so every lookup method is total —
@@ -461,7 +478,7 @@ type Api$3 = {
461
478
  * }
462
479
  * ```
463
480
  */
464
- type Config$2 = {
481
+ type Config$3 = {
465
482
  readonly locales: readonly string[];
466
483
  readonly defaultLocale: string;
467
484
  readonly localeNames?: Record<string, string>;
@@ -472,7 +489,7 @@ type Config$2 = {
472
489
  * Public API of the i18n plugin. Injected as `app.i18n` and reachable from
473
490
  * other plugins via `ctx.require(i18nPlugin)`.
474
491
  */
475
- type Api$2 = {
492
+ type Api$3 = {
476
493
  /**
477
494
  * Returns the configured supported locales in declared order.
478
495
  *
@@ -540,8 +557,8 @@ type Api$2 = {
540
557
  */
541
558
  t(locale: string, key: string): string;
542
559
  };
543
- declare namespace types_d_exports$4 {
544
- export { Api$1 as Api, ClientRoute, CompileInput, CompiledRoute, Config$1 as Config, ExtractRouteParams, ExtractSegmentParameter, HeadConfig$1 as HeadConfig, LayoutContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteState, RouterApi, RouterConfig, RouterState, State$1 as State, TypedRoute };
560
+ declare namespace types_d_exports$5 {
561
+ export { Api$2 as Api, ClientRoute, CompileInput, CompiledRoute, Config$2 as Config, ExtractApi$1 as ExtractApi, ExtractRouteParams, ExtractSegmentParameter, GenerateContext, HeadConfig$1 as HeadConfig, LayoutContext, LoadContext, MatcherTable, Prettify, RouteBuilder, RouteContext, RouteDefinition, RouteHandlers, RouteMap, RouteRequire, RouteState, RouterApi, RouterConfig, RouterState, State$2 as State, TypedRoute, Urls };
545
562
  }
546
563
  /**
547
564
  * Param contribution of a single path segment. `{name:?}` / `:name?` → optional;
@@ -580,6 +597,84 @@ interface RouteContext<S extends RouteState> {
580
597
  readonly data: S["data"];
581
598
  /** Active locale for this render. */
582
599
  readonly locale: string;
600
+ /**
601
+ * Build a link to a named route by pattern substitution — the framework delivers
602
+ * this on the context (same output as `app.router.toUrl`), so render/head build
603
+ * links with no `app`/`createUrls` reference. Works identically at build and on
604
+ * client navigation.
605
+ */
606
+ readonly url: (name: string, params?: Record<string, string>) => string;
607
+ }
608
+ /**
609
+ * Structural extraction of a plugin instance's public API from its `_phantom`
610
+ * carrier (mirrors the kernel's `ExtractApi` / spec/09 §3). Lets the loader/generator
611
+ * `require` resolve a plugin instance to its typed public API.
612
+ *
613
+ * @example
614
+ * type ContentApi = ExtractApi<typeof contentPlugin>;
615
+ */
616
+ type ExtractApi$1<PluginCandidate> = PluginCandidate extends {
617
+ readonly _phantom: {
618
+ readonly api: infer PluginApi;
619
+ };
620
+ } ? PluginApi : never;
621
+ /**
622
+ * Generic, instance-only `require` handed to a route's `.load()` / `.generate()` —
623
+ * the SAME shape as the kernel's `RequireFunction` (spec/08 §7) and the build's
624
+ * `PhaseRequire`, so the build forwards its own `ctx.require` straight through.
625
+ * Resolves a plugin INSTANCE to its public API; the consumer supplies the instance
626
+ * (e.g. `ctx.require(contentPlugin)`), so the router never names a sibling plugin.
627
+ *
628
+ * @example
629
+ * const content = ctx.require(contentPlugin); // ContentApi
630
+ */
631
+ type RouteRequire = <PluginCandidate extends {
632
+ readonly name: string;
633
+ readonly spec: unknown;
634
+ readonly _phantom: {
635
+ readonly config: unknown;
636
+ readonly state: unknown;
637
+ readonly api: unknown;
638
+ readonly events: Record<string, unknown>;
639
+ };
640
+ }>(plugin: PluginCandidate) => ExtractApi$1<PluginCandidate>;
641
+ /**
642
+ * Build-time context handed to a route's `.load()`. Carries the resolved path
643
+ * `params` and active `locale`, plus the spec's `require`/`has` so a loader pulls
644
+ * sibling plugin APIs the canonical way — `ctx.require(contentPlugin)` — with no
645
+ * module global and no router→content coupling. Loaders run ONLY at build time
646
+ * (never on the client), inside the build plugin's context, so `require`/`has` are
647
+ * always live here.
648
+ *
649
+ * @example
650
+ * route("/{slug}/").load((ctx) => ctx.require(contentPlugin).load(ctx.params.slug, ctx.locale));
651
+ */
652
+ interface LoadContext<S extends RouteState> {
653
+ /** Resolved path params for this page instance. */
654
+ readonly params: S["params"];
655
+ /** Active locale this page instance is built for. */
656
+ readonly locale: string;
657
+ /** Resolve a sibling plugin instance to its public API (spec/08 §7). */
658
+ readonly require: RouteRequire;
659
+ /** Whether a plugin is registered (by name) — branch on OPTIONAL plugins. */
660
+ readonly has: (name: string) => boolean;
661
+ }
662
+ /**
663
+ * Build-time context handed to a route's `.generate()` — the static-param producer.
664
+ * Carries the active `locale` plus `require`/`has` (no `params` yet — `.generate()`
665
+ * PRODUCES the param sets). Same build-only guarantee as {@link LoadContext}.
666
+ *
667
+ * @example
668
+ * route("/{slug}/").generate(async (ctx) =>
669
+ * [...(await ctx.require(contentPlugin).loadAll()).get(ctx.locale) ?? []].map((a) => ({ slug: a.computed.slug })));
670
+ */
671
+ interface GenerateContext {
672
+ /** Active locale to enumerate param sets for. */
673
+ readonly locale: string;
674
+ /** Resolve a sibling plugin instance to its public API (spec/08 §7). */
675
+ readonly require: RouteRequire;
676
+ /** Whether a plugin is registered (by name). */
677
+ readonly has: (name: string) => boolean;
583
678
  }
584
679
  /**
585
680
  * Context handed to a route's `.layout()` wrapper: the render-time
@@ -615,7 +710,7 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
615
710
  * Attach a data loader; widens the data generic (and ONLY the data generic) so
616
711
  * `.render()`/`.head()` see its return. Path params are preserved unchanged.
617
712
  */
618
- load<D>(loader: (params: S["params"], locale: string) => D | Promise<D>): RouteBuilder<{
713
+ load<D>(loader: (ctx: LoadContext<S>) => D | Promise<D>): RouteBuilder<{
619
714
  readonly params: S["params"];
620
715
  readonly data: Awaited<D>;
621
716
  }>;
@@ -629,19 +724,10 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
629
724
  layout(component: (ctx: LayoutContext<S>, children: ComponentChildren) => VNode): RouteBuilder<S>;
630
725
  /** Attach the page render handler. */
631
726
  render(handler: (ctx: RouteContext<S>) => VNode): RouteBuilder<S>;
632
- /**
633
- * Attach the client-side validation gate: parse the raw `unknown` fetched from
634
- * the persisted data file back into this route's data type `S["data"]`. Runs at
635
- * the trust boundary before `render` on the client (and MUST return `S["data"]`,
636
- * so a mismatched schema is a compile error). Throw inside it to reject malformed
637
- * data — `spa` then falls back to HTML-over-fetch. Use a hand guard or any
638
- * Standard-Schema validator (zod/valibot/arktype).
639
- */
640
- parse(handler: (raw: unknown) => S["data"]): RouteBuilder<S>;
641
727
  /** Attach the head/SEO handler. */
642
728
  head(handler: (ctx: RouteContext<S>) => HeadConfig$1): RouteBuilder<S>;
643
- /** Attach a static-generation param producer. */
644
- generate(handler: (locale: string) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
729
+ /** Attach a static-generation param producer (receives a {@link GenerateContext}). */
730
+ generate(handler: (ctx: GenerateContext) => S["params"][] | Promise<S["params"][]>): RouteBuilder<S>;
645
731
  /**
646
732
  * Attach an arbitrary metadata bag. The bag MUST be JSON-serializable: it is
647
733
  * projected verbatim into `clientManifest()` and shipped to the browser.
@@ -654,18 +740,16 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
654
740
  }
655
741
  /** Build-only handler bag captured by a `RouteBuilder` (consumed by `build` via `manifest()`). */
656
742
  interface RouteHandlers {
657
- /** Data loader. */
658
- readonly load?: (params: Record<string, string>, locale: string) => unknown;
743
+ /** Data loader (receives a {@link LoadContext}: params + locale + require/has). */
744
+ readonly load?: (ctx: LoadContext<RouteState>) => unknown;
659
745
  /** Layout wrapper (ctx-aware): frames the page in persistent chrome. SSG-only. */
660
746
  readonly layout?: (ctx: LayoutContext<RouteState>, children: ComponentChildren) => VNode;
661
747
  /** Page renderer. */
662
748
  readonly render?: (ctx: RouteContext<RouteState>) => VNode;
663
- /** Client-side validation gate: `unknown` (fetched JSON) → the route's data type, or throw. */
664
- readonly parse?: (raw: unknown) => unknown;
665
749
  /** Head/SEO producer. */
666
750
  readonly head?: (ctx: RouteContext<RouteState>) => HeadConfig$1;
667
- /** Static-generation param producer. */
668
- readonly generate?: (locale: string) => unknown[] | Promise<unknown[]>;
751
+ /** Static-generation param producer (receives a {@link GenerateContext}). */
752
+ readonly generate?: (ctx: GenerateContext) => unknown[] | Promise<unknown[]>;
669
753
  /** JSON serializer. */
670
754
  readonly toJson?: (ctx: RouteContext<RouteState>) => unknown;
671
755
  /** Output file-path producer. */
@@ -689,28 +773,45 @@ interface RouteDefinition {
689
773
  * base (erased) `RouteDefinition`; this is the documented generic-erasure boundary.
690
774
  */
691
775
  type RouteMap = Record<string, RouteDefinition>;
776
+ /**
777
+ * A pure, app-free URL builder over a route map (the return type of `createUrls`).
778
+ * `toUrl` builds a route's path by name + params via pattern substitution — it needs
779
+ * NO running app, router instance, base URL, or i18n: just the route map the consumer
780
+ * already holds at module scope. Works identically at build, on client navigation,
781
+ * and inside hydrated islands. Reuses the SAME `buildUrl` as the runtime `RouterApi`,
782
+ * so the helper and the API can never diverge.
783
+ *
784
+ * @example
785
+ * const url = createUrls(routes);
786
+ * url.toUrl("article", { lang: "en", slug: "hello" }); // "/en/hello/"
787
+ */
788
+ interface Urls<T extends RouteMap> {
789
+ /**
790
+ * Build a route's URL path from its name and params. The name is typed to the
791
+ * route map's keys — only declared routes are accepted.
792
+ *
793
+ * @param name - Route name key from the map (e.g. `"home"`, `"article"`).
794
+ * @param params - Path params to substitute into the pattern. Defaults to `{}`.
795
+ * @returns The resolved relative URL path.
796
+ * @throws {Error} If `name` is not present in the route map.
797
+ * @example
798
+ * url.toUrl("home", { lang: "en" }); // "/en/"
799
+ */
800
+ toUrl<K extends keyof T & string>(name: K, params?: Record<string, string>): string;
801
+ }
692
802
  /**
693
803
  * Configuration for the router plugin.
694
804
  *
695
805
  * @remarks
696
- * `routes` is an OPAQUE carrier at the config boundary the framework `Config`
697
- * generic erases the per-route element types (spec/05 §8, spec/09 §4). Downstream
698
- * plugins read the typed route set via `ctx.require(routerPlugin).manifest()`.
806
+ * `routes` is the declarative route map registered the normal config way via
807
+ * `createApp({ pluginConfigs: { router: { routes } } })` and compiled into the matcher
808
+ * table in the router's `onInit`. An `import * as routes` namespace is a valid value. It is
809
+ * the SOLE registration path: omitting it leaves the matcher table empty, so every read
810
+ * (`match`/`toUrl`/`entries`/…) throws. The render `mode` is NOT here — it is a GLOBAL
811
+ * framework option (`createApp({ config: { mode } })`), read by the router via `ctx.global`.
699
812
  */
700
813
  type RouterConfig = {
701
- /**
702
- * Named route definitions. Element type erases to the base `RouteDefinition`
703
- * at this config boundary; per-route call-site types are preserved only through
704
- * `defineRoutes()` + `route()` at the consumer and re-exposed via `manifest()`.
705
- */
706
- routes: RouteMap;
707
- /**
708
- * Render mode for URL/file resolution. Defaults to `"hybrid"`.
709
- * - `"ssg"` static generation only (no client router emitted).
710
- * - `"spa"` client-side routing only.
711
- * - `"hybrid"` static HTML + client navigation overlay.
712
- */
713
- mode?: "ssg" | "spa" | "hybrid";
814
+ /** Declarative route map (route name → `route(...)`); compiled at init. An `import * as` namespace works. */readonly routes?: RouteMap;
714
815
  };
715
816
  /** A resolved route exposing URL utilities with typed params (port of legacy TypedRoute). */
716
817
  interface TypedRoute<TParams = Record<string, string>> {
@@ -759,15 +860,14 @@ interface MatcherTable {
759
860
  readonly byName: ReadonlyMap<string, CompiledRoute>;
760
861
  }
761
862
  /**
762
- * Router plugin state. `createState` runs with minimal context and returns a
763
- * mutable holder whose `table` is `null` until `onInit` (which has full context)
764
- * compiles and assigns it. Keeps all mutable state in `ctx.state` (no singletons).
863
+ * Router plugin state — a mutable holder whose `table` is `null` until the router's
864
+ * `onInit` compiles `config.routes`. The render `mode` is NOT stored here; it is read
865
+ * from the global framework config via the API context. Keeps all mutable state in
866
+ * `ctx.state` (no singletons).
765
867
  */
766
868
  interface RouterState {
767
- /** Compiled matcher table; `null` until `onInit` assigns it. */
869
+ /** Compiled matcher table; `null` until `onInit` compiles `config.routes`. */
768
870
  table: MatcherTable | null;
769
- /** Resolved render mode (single source of truth; set in `onInit`). Defaults `"hybrid"`. */
770
- mode: "ssg" | "spa" | "hybrid";
771
871
  }
772
872
  /** Plain-data input to `compileRoutes` — resolved DATA only, never the plugin ctx. */
773
873
  interface CompileInput {
@@ -800,7 +900,7 @@ interface ClientRoute {
800
900
  /** Route metadata bag from `.meta()`. MUST be JSON-serializable. */
801
901
  readonly meta: Record<string, unknown>;
802
902
  }
803
- /** Public API exposed via `ctx.require(routerPlugin)`. */
903
+ /** Public API exposed via `ctx.require(routerPlugin)` and `app.router`. */
804
904
  type RouterApi = {
805
905
  /**
806
906
  * Match a pathname against the compiled route table (specificity-sorted).
@@ -839,7 +939,7 @@ type RouterApi = {
839
939
  *
840
940
  * @returns Read-only array of the typed route definitions, in declaration order.
841
941
  * @example
842
- * for (const def of ctx.require(routerPlugin).manifest()) { def._handlers.load?.({}, "en"); }
942
+ * for (const def of ctx.require(routerPlugin).manifest()) def._handlers.render?.(routeContext);
843
943
  */
844
944
  manifest(): readonly RouteDefinition[];
845
945
  /**
@@ -865,13 +965,13 @@ type RouterApi = {
865
965
  mode(): "ssg" | "spa" | "hybrid";
866
966
  };
867
967
  /** Re-export under the canonical `Config` name for the plugin-types barrel. */
868
- type Config$1 = RouterConfig;
968
+ type Config$2 = RouterConfig;
869
969
  /** Re-export under the canonical `State` name for the plugin-types barrel. */
870
- type State$1 = RouterState;
970
+ type State$2 = RouterState;
871
971
  /** Re-export under the canonical `Api` name for the plugin-types barrel. */
872
- type Api$1 = RouterApi;
873
- declare namespace types_d_exports$2 {
874
- export { Api, ArticleMeta, Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State };
972
+ type Api$2 = RouterApi;
973
+ declare namespace types_d_exports$3 {
974
+ export { Api$1 as Api, ArticleMeta, Config$1 as Config, HeadConfig, HeadDefaults, HeadElement, ResolvedRoute, State$1 as State };
875
975
  }
876
976
  /**
877
977
  * @file head plugin — type definitions skeleton
@@ -887,7 +987,7 @@ declare namespace types_d_exports$2 {
887
987
  * const config: Config = { titleTemplate: "%s — Moku" };
888
988
  * ```
889
989
  */
890
- type Config = {
990
+ type Config$1 = {
891
991
  /** Title template applied to per-route titles. `%s` is replaced by the route title. */titleTemplate?: string; /** Default Open Graph image URL used when a route does not supply one. */
892
992
  defaultOgImage?: string; /** Default Twitter card type emitted when og/twitter content is present. */
893
993
  twitterCard?: "summary" | "summary_large_image"; /** Default Twitter site handle (e.g. `"@moku_labs"`) emitted as `twitter:site`. */
@@ -905,7 +1005,7 @@ type Config = {
905
1005
  * const state: State = { defaults: null };
906
1006
  * ```
907
1007
  */
908
- type State = {
1008
+ type State$1 = {
909
1009
  /** Normalized head defaults, assigned once in `onInit` (initially `null`). */defaults: HeadDefaults | null;
910
1010
  };
911
1011
  /**
@@ -999,7 +1099,7 @@ type ResolvedRoute = {
999
1099
  * const html: string = api.render(route, data);
1000
1100
  * ```
1001
1101
  */
1002
- type Api = {
1102
+ type Api$1 = {
1003
1103
  /**
1004
1104
  * Compose the final `<head>` inner HTML for a route. Pulled synchronously by `build`.
1005
1105
  *
@@ -1013,7 +1113,7 @@ type Api = {
1013
1113
  */
1014
1114
  render(route: ResolvedRoute, data: unknown): string;
1015
1115
  };
1016
- declare namespace types_d_exports$5 {
1116
+ declare namespace types_d_exports$6 {
1017
1117
  export { COMPONENT_HOOK_NAMES, ComponentContext, ComponentDef, ComponentHooks, ComponentInstance, ExtractApi, PageData, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
1018
1118
  }
1019
1119
  /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
@@ -1214,11 +1314,11 @@ interface SpaKernelDeps {
1214
1314
  /** Router plugin API — used for client-side route matching (`match`) + the resolved `mode`. */
1215
1315
  router: RouterApi;
1216
1316
  /** Head plugin API — its pure compose is reused for client head-sync. */
1217
- head: Api;
1317
+ head: Api$1;
1218
1318
  /**
1219
1319
  * The OPTIONAL `data` reader. Present only when the `data` plugin is composed.
1220
1320
  * When present (and `router.mode() !== "ssg"`), navigation first tries the client
1221
- * DATA path (match → `dataAt(path)` → `route.parse` → `route.render`); otherwise
1321
+ * DATA path (match → `dataAt(path)` → `route.render`); otherwise
1222
1322
  * it always uses HTML-over-fetch.
1223
1323
  */
1224
1324
  dataAt?: SpaDataReader;
@@ -1341,7 +1441,7 @@ type SpaApi = {
1341
1441
  * });
1342
1442
  * ```
1343
1443
  */
1344
- declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>;
1444
+ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Config$4, Record<string, never>, Api$4, {}> & Record<never, never>;
1345
1445
  //#endregion
1346
1446
  //#region src/plugins/i18n/index.d.ts
1347
1447
  /**
@@ -1363,7 +1463,7 @@ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Confi
1363
1463
  * });
1364
1464
  * ```
1365
1465
  */
1366
- declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$2, Record<string, never>, Api$2, {}> & Record<never, never>;
1466
+ declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>;
1367
1467
  //#endregion
1368
1468
  //#region src/plugins/router/builders/route-builder.d.ts
1369
1469
  /**
@@ -1378,7 +1478,7 @@ declare const i18nPlugin: import("@moku-labs/core").PluginInstance<"i18n", Confi
1378
1478
  * @example
1379
1479
  * ```ts
1380
1480
  * route("/{lang:?}/{slug}/")
1381
- * .load(({ slug }) => loadArticle(slug))
1481
+ * .load((ctx) => loadArticle(ctx.params.slug))
1382
1482
  * .render((ctx) => <Article a={ctx.data} />)
1383
1483
  * .head((ctx) => ({ title: ctx.data.title }));
1384
1484
  * ```
@@ -1396,31 +1496,46 @@ declare function route<P extends string>(pattern: P): RouteBuilder<RouteState<P>
1396
1496
  * ```
1397
1497
  */
1398
1498
  declare function defineRoutes<T extends RouteMap>(routes: T): T;
1499
+ /**
1500
+ * Build a pure, app-free URL builder from a route map. `toUrl(name, params)` resolves
1501
+ * a route's path by pattern substitution using the SAME `buildUrl` as the runtime
1502
+ * `RouterApi.toUrl`, so the helper and the API can never diverge. It needs no running
1503
+ * app, router instance, base URL, or i18n — just the route map the consumer already
1504
+ * holds at module scope. So components, layouts, and hydrated islands import it
1505
+ * directly: no `app.router` reference, no manual "bind", no module global, no
1506
+ * "not bound" guard, and no createApp ↔ routes cycle.
1507
+ *
1508
+ * @param routes - The route map (typically the value returned by {@link defineRoutes}).
1509
+ * @returns A {@link Urls} builder whose `toUrl` accepts only this map's route names.
1510
+ * @example
1511
+ * ```ts
1512
+ * const url = createUrls(routes);
1513
+ * url.toUrl("article", { lang: "en", slug: "hello" }); // "/en/hello/"
1514
+ * ```
1515
+ */
1516
+ declare function createUrls<T extends RouteMap>(routes: T): Urls<T>;
1399
1517
  //#endregion
1400
1518
  //#region src/plugins/router/index.d.ts
1401
1519
  /**
1402
1520
  * Router plugin — typed, named route definitions with locale-aware URL generation
1403
- * and matching. Author routes with {@link route} + {@link defineRoutes}. Depends
1404
- * on site (base URL) and i18n (locales).
1521
+ * and matching. Author routes with {@link route}, then register them the normal config
1522
+ * way via `pluginConfigs.router.routes` (compiled at init). Depends on site (base URL)
1523
+ * and i18n (locales).
1405
1524
  *
1406
- * @example Define routes and choose a render mode
1525
+ * @example Register routes via config, then start/build
1407
1526
  * ```ts
1527
+ * import * as routes from "./routes";
1408
1528
  * const app = createApp({
1409
- * pluginConfigs: {
1410
- * router: {
1411
- * routes: defineRoutes({
1412
- * home: route("/"),
1413
- * article: route("/blog/{slug}/")
1414
- * }),
1415
- * mode: "hybrid" // "ssg" | "spa" | "hybrid" (default)
1416
- * }
1417
- * }
1529
+ * config: { mode: "hybrid" }, // render mode is GLOBAL config
1530
+ * pluginConfigs: { router: { routes } } // declarative route map (a namespace works)
1418
1531
  * });
1532
+ * await app.build.run(); // or: await app.start(); — routes compiled at init
1419
1533
  * ```
1420
1534
  */
1421
1535
  declare const routerPlugin: import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1422
1536
  route: typeof route;
1423
1537
  defineRoutes: typeof defineRoutes;
1538
+ createUrls: typeof createUrls;
1424
1539
  };
1425
1540
  //#endregion
1426
1541
  //#region src/plugins/head/primitives.d.ts
@@ -1495,6 +1610,8 @@ declare function feedLink(title: string, url: string, type?: string): HeadElemen
1495
1610
  * modified times, author, section, tags, plus a JSON-LD `Article` block and canonical.
1496
1611
  *
1497
1612
  * @param articleMeta - Article metadata (title, description, author, dates, tags, image…).
1613
+ * `image`, when present, is pushed to `og:image` verbatim and must therefore be
1614
+ * an absolute URL (this helper does not resolve relative paths against the site).
1498
1615
  * @param canonicalUrl - The article's canonical absolute URL.
1499
1616
  * @returns An ordered array of serializable head elements.
1500
1617
  * @example buildArticleHead({ title: "Hi", author: "A", published: "2026-01-01" }, "https://x/p")
@@ -1521,7 +1638,7 @@ declare function buildArticleHead(articleMeta: ArticleMeta, canonicalUrl: string
1521
1638
  * });
1522
1639
  * ```
1523
1640
  */
1524
- declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config, State, Api, {}> & {
1641
+ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Config$1, State$1, Api$1, {}> & {
1525
1642
  meta: typeof meta;
1526
1643
  og: typeof og;
1527
1644
  twitter: typeof twitter;
@@ -1588,7 +1705,7 @@ declare const spaPlugin: import("@moku-labs/core").PluginInstance<"spa", SpaConf
1588
1705
  el: Element;
1589
1706
  };
1590
1707
  }> & Record<never, never>;
1591
- declare namespace types_d_exports {
1708
+ declare namespace types_d_exports$1 {
1592
1709
  export { DataConfig, DataEntry, DataProvider, DataState, DataWriteSummary };
1593
1710
  }
1594
1711
  /**
@@ -1597,13 +1714,13 @@ declare namespace types_d_exports {
1597
1714
  * The `data` plugin is the **agnostic data provider** for the SSG→DATA→SPA pattern.
1598
1715
  * It owns ONE thing: the contract `page path → persisted JSON file`. It knows
1599
1716
  * NOTHING about what the data *is* — no domain types appear here. A route decides
1600
- * its own data shape (`load`'s return) and its own validation (`route.parse`).
1717
+ * its own data shape (`load`'s return).
1601
1718
  *
1602
1719
  * - **Node (build):** `write(entries)` persists one JSON file per page, keyed by
1603
1720
  * the page's URL via {@link DataProvider.fileFor}. `build` supplies the entries
1604
1721
  * (it already expanded the routes), so there is no duplicate expansion here.
1605
- * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`; the
1606
- * route's `parse` validates it into the route's data type before `render`.
1722
+ * - **Browser (runtime):** `at(path)` fetches + caches that file as `unknown`, which
1723
+ * the route uses directly as `ctx.data` in `render`.
1607
1724
  *
1608
1725
  * The Node-only file-writing code (`node:fs`) is isolated behind a lazy `import()`
1609
1726
  * inside `write()`, so composing `data` in a browser app keeps the bundle free of
@@ -1625,8 +1742,8 @@ type DataConfig = {
1625
1742
  outputDir: string;
1626
1743
  /**
1627
1744
  * READ side (browser): site-root-relative URL the client fetches the per-page
1628
- * JSON from. A different domain from {@link DataConfig.outputDir} (a filesystem
1629
- * path); keep consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
1745
+ * JSON from. The URL-space mirror of {@link DataConfig.outputDir} (a filesystem
1746
+ * path); keep them consistent (`"/" + trim(outputDir) + "/"`). Default `"/_data/"`.
1630
1747
  */
1631
1748
  baseUrl: string;
1632
1749
  };
@@ -1668,15 +1785,15 @@ interface DataState {
1668
1785
  * // Node build (build supplies the entries it already expanded):
1669
1786
  * await app.data.write([{ path: "/en/hello/", data: article }]);
1670
1787
  *
1671
- * // Browser (inside spa nav): fetch the page's data, then route.parse validates it:
1672
- * const raw = await app.data.at("/en/hello/"); // unknown | null
1788
+ * // Browser (inside spa nav): fetch the page's data, used directly as ctx.data:
1789
+ * const raw = await app.data.at("/en/hello/"); // unknown | null (null on failure)
1673
1790
  * ```
1674
1791
  */
1675
1792
  type DataProvider = {
1676
1793
  /**
1677
1794
  * READ (browser) — fetch (and cache) the persisted data for a page path from
1678
- * `config.baseUrl`. Returns the raw parsed JSON as `unknown` (the caller's
1679
- * `route.parse` validates it), or `null` if the fetch/parse fails.
1795
+ * `config.baseUrl`. Returns the raw parsed JSON as `unknown`, used directly as
1796
+ * the route's `ctx.data`; returns `null` if the fetch or JSON parse fails.
1680
1797
  *
1681
1798
  * @param path - The page URL path (e.g. `/en/hello/`).
1682
1799
  * @returns The page's raw data, or `null` on failure.
@@ -1722,18 +1839,316 @@ type DataProvider = {
1722
1839
  * @example
1723
1840
  * ```ts
1724
1841
  * // Node build: `build` calls app.data.write(...) during its pages phase when
1725
- * // router.mode !== "ssg". Just compose the plugin:
1842
+ * // router.mode() !== "ssg". Compose the plugin + set the global render mode:
1843
+ * import * as routes from "./routes";
1726
1844
  * const app = createApp({
1727
1845
  * plugins: [dataPlugin, contentPlugin, buildPlugin],
1728
- * pluginConfigs: { content: { contentDir: "./content" }, router: { routes, mode: "hybrid" } }
1846
+ * config: { mode: "hybrid" },
1847
+ * pluginConfigs: { content: { providers: [fileSystemContent({ contentDir: "./content" })] }, router: { routes } }
1729
1848
  * });
1730
- * await app.start();
1731
- * await app.build.run(); // writes HTML + per-page data sidecars
1849
+ * await app.build.run(); // writes HTML + per-page data sidecars (routes compiled at init)
1732
1850
  *
1733
1851
  * // Browser app: compose `dataPlugin` too; spa fetches via app.data.at(path) on nav.
1734
1852
  * ```
1735
1853
  */
1736
1854
  declare const dataPlugin: import("@moku-labs/core").PluginInstance<"data", DataConfig, DataState, DataProvider, {}> & Record<never, never>;
1855
+ declare namespace types_d_exports {
1856
+ export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, State };
1857
+ }
1858
+ /**
1859
+ * YAML frontmatter parsed from each article file.
1860
+ *
1861
+ * @example
1862
+ * ```ts
1863
+ * { title: "Hello", date: "2026-01-15", description: "Intro", tags: [], language: "en" }
1864
+ * ```
1865
+ */
1866
+ type Frontmatter = {
1867
+ /** Article title. Required. */title: string; /** ISO 8601 date string, e.g. "2026-01-15". Required. */
1868
+ date: string; /** Short summary used in cards, feeds, and meta description. Required. */
1869
+ description: string; /** Topic tags. Required (may be empty array). */
1870
+ tags: string[]; /** Source language code of this file. Required. */
1871
+ language: string; /** Draft flag. Excluded from output in production mode. Defaults to false. */
1872
+ draft?: boolean; /** Author name. Falls back to the provider's defaultAuthor when omitted. */
1873
+ author?: string;
1874
+ };
1875
+ /**
1876
+ * Fields computed by the pipeline (not authored in frontmatter).
1877
+ *
1878
+ * @example
1879
+ * ```ts
1880
+ * { slug: "hello", readingTime: 1, contentId: "hello", status: "published", wordCount: 42 }
1881
+ * ```
1882
+ */
1883
+ type ComputedFields = {
1884
+ /** Article directory name. */slug: string; /** Reading time in minutes (ceiling, minimum 1). */
1885
+ readingTime: number; /** Stable content identifier (slug by default). */
1886
+ contentId: string; /** Derived publication status. */
1887
+ status: "published" | "draft"; /** Word count from the source body. */
1888
+ wordCount: number;
1889
+ };
1890
+ /**
1891
+ * A fully processed, render-ready article.
1892
+ *
1893
+ * @example
1894
+ * ```ts
1895
+ * { frontmatter, computed, html: "<p>…</p>", locale: "en", isFallback: false, url: "/en/hello/" }
1896
+ * ```
1897
+ */
1898
+ type Article = {
1899
+ /** Parsed frontmatter. */frontmatter: Frontmatter; /** Pipeline-computed metadata. */
1900
+ computed: ComputedFields; /** Sanitized rendered HTML body. */
1901
+ html: string; /** Locale this Article instance represents (the requested locale, even on fallback). */
1902
+ locale: string; /** True when the default-locale file was used because the requested locale was missing. */
1903
+ isFallback: boolean; /** Canonical URL for this article in this locale. */
1904
+ url: string;
1905
+ };
1906
+ /**
1907
+ * Lightweight projection of Article for cards/lists.
1908
+ *
1909
+ * @example
1910
+ * ```ts
1911
+ * { contentId: "hello", status: "published", title: "Hello", date: "2026-01-15", description: "Intro", tags: [], readingTime: 1, url: "/en/hello/" }
1912
+ * ```
1913
+ */
1914
+ type ArticleCard = {
1915
+ /** Stable content identifier. */contentId: string; /** Derived publication status. */
1916
+ status: "published" | "draft"; /** Article title. */
1917
+ title: string; /** ISO 8601 date string. */
1918
+ date: string; /** Short summary. */
1919
+ description: string; /** Topic tags. */
1920
+ tags: string[]; /** Reading time in minutes. */
1921
+ readingTime: number; /** Canonical URL for this article in this locale. */
1922
+ url: string;
1923
+ };
1924
+ /**
1925
+ * A pluggable content SOURCE. The shell calls these to read articles; whether content
1926
+ * is read from the filesystem (Node) or some other source is chosen by which provider
1927
+ * you compose — exactly like `env` providers (`dotenv`/`processEnv` vs `browserEnv`).
1928
+ * The shell adds locale fallback, draft filtering, sorting, caching, and events on top.
1929
+ *
1930
+ * @example
1931
+ * ```ts
1932
+ * const provider = fileSystemContent({ contentDir: "./content" });
1933
+ * ```
1934
+ */
1935
+ interface ContentProvider {
1936
+ /** Human-readable provider name, used in diagnostics. */
1937
+ readonly name: string;
1938
+ /** Source directory surfaced via `api.contentDir()` (filesystem providers; "" otherwise). */
1939
+ readonly contentDir: string;
1940
+ /**
1941
+ * Discover the article slugs this provider can supply.
1942
+ *
1943
+ * @returns The provider's slug list.
1944
+ */
1945
+ slugs(): Promise<readonly string[]>;
1946
+ /**
1947
+ * Read + render ONE article for a file-locale; `null` if this provider has no such file.
1948
+ *
1949
+ * @param slug - Article directory name.
1950
+ * @param fileLocale - Locale whose source file is read.
1951
+ * @param outLocale - Locale the resulting Article represents (the requested locale).
1952
+ * @param isFallback - Whether this resolution used the default-locale fallback.
1953
+ * @returns The constructed Article, or `null` when absent.
1954
+ */
1955
+ readArticle(slug: string, fileLocale: string, outLocale: string, isFallback: boolean): Promise<Article | null>;
1956
+ /**
1957
+ * Render a standalone Markdown string to HTML through the provider's pipeline.
1958
+ *
1959
+ * @param markdown - Raw Markdown source.
1960
+ * @returns The rendered HTML.
1961
+ */
1962
+ render(markdown: string): Promise<string>;
1963
+ /**
1964
+ * Optional dev hook: drop cached discovery so stale paths are re-read next time.
1965
+ *
1966
+ * @param paths - Stale file paths.
1967
+ */
1968
+ invalidate?(paths: readonly string[]): void;
1969
+ }
1970
+ /**
1971
+ * Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
1972
+ * These are the markdown-pipeline + source concerns that used to live on the content
1973
+ * plugin config; they now belong to the provider you compose.
1974
+ *
1975
+ * @example
1976
+ * ```ts
1977
+ * fileSystemContent({ contentDir: "./content", shikiTheme: "github-dark", defaultAuthor: "Ada" });
1978
+ * ```
1979
+ */
1980
+ type FileSystemContentOptions = {
1981
+ /** Absolute or project-relative path to the content directory. */contentDir: string;
1982
+ /**
1983
+ * SECURITY GATE. When false (the default), rehype-sanitize runs as the final
1984
+ * pipeline step. Set true ONLY for fully author-controlled Markdown.
1985
+ */
1986
+ trustedContent?: boolean; /** Additional remark plugins, concatenated AFTER framework defaults. Defaults to []. */
1987
+ extraRemarkPlugins?: readonly Pluggable[]; /** Additional rehype plugins, concatenated after custom transforms, before Shiki + sanitize. Defaults to []. */
1988
+ extraRehypePlugins?: readonly Pluggable[];
1989
+ /**
1990
+ * Shiki theme for syntax highlighting: a bundled theme NAME (default "github-dark")
1991
+ * or a custom `ThemeRegistration` object. Passed straight through to `@shikijs/rehype`.
1992
+ */
1993
+ shikiTheme?: BundledTheme | ThemeRegistrationAny; /** Author applied to articles whose frontmatter omits author. Defaults to undefined. */
1994
+ defaultAuthor?: string;
1995
+ };
1996
+ /**
1997
+ * Internal mutable state of the filesystem provider: the lazy unified processor and
1998
+ * the discovery caches. Owned by the provider closure, never by the plugin shell.
1999
+ *
2000
+ * @example
2001
+ * ```ts
2002
+ * { processor: null, slugs: null, dirtyPaths: new Set() }
2003
+ * ```
2004
+ */
2005
+ type ContentProviderState = {
2006
+ /** Lazily-created unified processor singleton. null until first render()/readArticle(). */processor: Processor | null; /** Discovered, sorted slug list cached after first disk scan. null until first discovery. */
2007
+ slugs: string[] | null; /** Paths marked stale by invalidate(); next discovery re-reads only these. Starts empty. */
2008
+ dirtyPaths: Set<string>;
2009
+ };
2010
+ /**
2011
+ * Configuration for the content plugin (shell).
2012
+ *
2013
+ * @example
2014
+ * ```ts
2015
+ * { providers: [fileSystemContent({ contentDir: "./content" })] }
2016
+ * ```
2017
+ */
2018
+ type Config = {
2019
+ /**
2020
+ * Ordered content sources. Compose at least one (e.g. `fileSystemContent(...)` on
2021
+ * Node). The first provider that supplies an article for a slug+locale wins;
2022
+ * `slugs()` are unioned. The plugin's own spec default is `[]` (a build must supply one).
2023
+ */
2024
+ providers: ContentProvider[];
2025
+ };
2026
+ /**
2027
+ * Internal mutable state for the content plugin shell: the locale-keyed article cache.
2028
+ *
2029
+ * @example
2030
+ * ```ts
2031
+ * { articles: new Map() }
2032
+ * ```
2033
+ */
2034
+ type State = {
2035
+ /** Article cache keyed locale -> (slug -> Article). Starts empty. */articles: Map<string, Map<string, Article>>;
2036
+ };
2037
+ /**
2038
+ * Notification-only events emitted by the content plugin.
2039
+ *
2040
+ * @example
2041
+ * ```ts
2042
+ * emit("content:ready", { locales: ["en"], articleCount: 3 });
2043
+ * ```
2044
+ */
2045
+ type ContentEvents = {
2046
+ /** All articles loaded across locales. */"content:ready": {
2047
+ locales: readonly string[];
2048
+ articleCount: number;
2049
+ }; /** Article paths marked stale for dev rebuild. */
2050
+ "content:invalidated": {
2051
+ paths: readonly string[];
2052
+ };
2053
+ };
2054
+ /**
2055
+ * Kernel-free domain context handed to createContentApi by the wiring harness.
2056
+ * Carries the shell state (article cache), global flag, emit, the i18n-derived
2057
+ * locale helpers, and the resolved content {@link ContentProvider} — so api.ts stays
2058
+ * free of createPlugin/ctx AND of any node/pipeline import.
2059
+ *
2060
+ * @example
2061
+ * ```ts
2062
+ * const apiContext: ContentApiContext = { state, global, emit, locales, defaultLocale, provider };
2063
+ * ```
2064
+ */
2065
+ type ContentApiContext = {
2066
+ /** Mutable shell state (article cache). */state: State; /** Global framework configuration (deployment stage). */
2067
+ global: {
2068
+ stage: Stage;
2069
+ }; /** Emit a registered content event. */
2070
+ emit: <K extends keyof ContentEvents>(event: K, payload: ContentEvents[K]) => void; /** Active locale codes from i18n. */
2071
+ locales: () => readonly string[]; /** Default locale code from i18n (fallback source). */
2072
+ defaultLocale: () => string; /** The resolved content source (merged from `config.providers`). */
2073
+ provider: ContentProvider;
2074
+ };
2075
+ /**
2076
+ * Public API for the content plugin.
2077
+ *
2078
+ * @example
2079
+ * ```ts
2080
+ * const map = await app.content.loadAll();
2081
+ * ```
2082
+ */
2083
+ type Api = {
2084
+ /**
2085
+ * Load every article across every active locale, returning a locale-keyed
2086
+ * map of date-descending Article arrays. Emits content:ready.
2087
+ */
2088
+ loadAll(): Promise<Map<string, Article[]>>;
2089
+ /**
2090
+ * Resolve and render a single article for one locale, with locale fallback.
2091
+ *
2092
+ * @param slug - Article directory name.
2093
+ * @param locale - Requested locale code.
2094
+ */
2095
+ load(slug: string, locale: string): Promise<Article>;
2096
+ /**
2097
+ * Render a raw Markdown string to HTML through the full pipeline.
2098
+ *
2099
+ * @param md - Raw Markdown source.
2100
+ */
2101
+ renderMarkdown(md: string): Promise<string>;
2102
+ /**
2103
+ * Mark file paths stale for incremental dev rebuilds. Emits content:invalidated.
2104
+ *
2105
+ * @param paths - File paths to invalidate.
2106
+ */
2107
+ invalidate(paths: readonly string[]): void;
2108
+ /**
2109
+ * Project a full Article to a lightweight ArticleCard for list/grid rendering.
2110
+ *
2111
+ * @param article - The source article.
2112
+ */
2113
+ articleToCard(article: Article): ArticleCard;
2114
+ /**
2115
+ * The configured content source directory (e.g. `"./content"`), from the first
2116
+ * provider. Lets the build copy each article's co-located assets
2117
+ * (`<contentDir>/<slug>/images/`) into the output.
2118
+ */
2119
+ contentDir(): string;
2120
+ };
2121
+ //#endregion
2122
+ //#region src/plugins/content/index.d.ts
2123
+ /**
2124
+ * Content plugin (shell) — provider-driven locale-keyed Article model. Orchestration
2125
+ * (locale fallback, draft filtering, sort, caching, events) lives here; source I/O +
2126
+ * the Markdown pipeline live in a {@link ContentProvider} you compose (like `env`
2127
+ * providers). The shell imports zero node code, so `contentPlugin` is browser-safe.
2128
+ * Depends on i18n; emits `content:ready` and `content:invalidated`.
2129
+ *
2130
+ * @example Compose the node filesystem provider with a content dir + Shiki theme
2131
+ * ```ts
2132
+ * import { contentPlugin, fileSystemContent } from "@moku-labs/web";
2133
+ * const app = createApp({
2134
+ * plugins: [contentPlugin],
2135
+ * pluginConfigs: {
2136
+ * content: {
2137
+ * providers: [fileSystemContent({ contentDir: "./content", shikiTheme: "github-dark", defaultAuthor: "Ada" })]
2138
+ * }
2139
+ * }
2140
+ * });
2141
+ * ```
2142
+ */
2143
+ declare const contentPlugin: import("@moku-labs/core").PluginInstance<"content", Config, State, Api, {
2144
+ "content:ready": {
2145
+ locales: readonly string[];
2146
+ articleCount: number;
2147
+ };
2148
+ "content:invalidated": {
2149
+ paths: readonly string[];
2150
+ };
2151
+ }> & Record<never, never>;
1737
2152
  //#endregion
1738
2153
  //#region src/plugins/log/index.d.ts
1739
2154
  /**
@@ -1758,27 +2173,24 @@ declare const logPlugin: import("@moku-labs/core").CorePluginInstance<"log", Log
1758
2173
  *
1759
2174
  * @param options - Optional configuration:
1760
2175
  * - `pluginConfigs` — per-plugin overrides, keyed by plugin name.
1761
- * - `config` — global framework config (e.g. `{ mode: "development" }`).
2176
+ * - `config` — global framework config (e.g. `{ mode: "spa" }`).
1762
2177
  * - `plugins` — extra plugins (e.g. `dataPlugin` or your own) merged into the app and its type.
1763
2178
  * - `onReady` / `onError` / `onStart` / `onStop` — lifecycle callbacks.
1764
2179
  * @returns The initialized app: `start()`, `stop()`, every plugin's API, and `log`.
1765
2180
  * @example
1766
2181
  * ```ts
1767
2182
  * // Client SPA — env works with no wiring (browserEnv is the default provider):
1768
- * const app = createApp({
1769
- * plugins: [dataPlugin],
1770
- * pluginConfigs: {
1771
- * router: { mode: "spa", routes: defineRoutes({ home: route("/"), post: route("/blog/{slug}/") }) }
1772
- * }
1773
- * });
1774
- * await app.start();
2183
+ * import * as routes from "./routes";
2184
+ * const app = createApp({ config: { mode: "spa" }, pluginConfigs: { router: { routes } } });
2185
+ * await app.start(); // routes compiled at init from config
1775
2186
  * app.env.get("PUBLIC_API_URL"); // resolved from import.meta.env
1776
2187
  * ```
1777
2188
  */
1778
- declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs/core").AnyPluginInstance[] = readonly []>(options?: import("@moku-labs/core").CreateAppOptions<Config$4, Events, (import("@moku-labs/core").PluginInstance<"site", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$2, Record<string, never>, Api$2, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2189
+ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs/core").AnyPluginInstance[] = readonly []>(options?: import("@moku-labs/core").CreateAppOptions<Config$5, Events, (import("@moku-labs/core").PluginInstance<"site", Config$4, Record<string, never>, Api$4, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1779
2190
  route: typeof route;
1780
2191
  defineRoutes: typeof defineRoutes;
1781
- }) | (import("@moku-labs/core").PluginInstance<"head", Config, State, Api, {}> & {
2192
+ createUrls: typeof createUrls;
2193
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$1, State$1, Api$1, {}> & {
1782
2194
  meta: typeof meta;
1783
2195
  og: typeof og;
1784
2196
  twitter: typeof twitter;
@@ -1803,10 +2215,11 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
1803
2215
  name: string;
1804
2216
  el: Element;
1805
2217
  };
1806
- }> & Record<never, never>) | ExtraPlugins[number], [...ExtraPlugins], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>> | undefined) => import("@moku-labs/core").App<Config$4, Events, (import("@moku-labs/core").PluginInstance<"site", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$2, Record<string, never>, Api$2, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
2218
+ }> & Record<never, never>) | ExtraPlugins[number], [...ExtraPlugins], import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>> | undefined) => import("@moku-labs/core").App<Config$5, Events, (import("@moku-labs/core").PluginInstance<"site", Config$4, Record<string, never>, Api$4, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"i18n", Config$3, Record<string, never>, Api$3, {}> & Record<never, never>) | (import("@moku-labs/core").PluginInstance<"router", RouterConfig, RouterState, RouterApi, {}> & {
1807
2219
  route: typeof route;
1808
2220
  defineRoutes: typeof defineRoutes;
1809
- }) | (import("@moku-labs/core").PluginInstance<"head", Config, State, Api, {}> & {
2221
+ createUrls: typeof createUrls;
2222
+ }) | (import("@moku-labs/core").PluginInstance<"head", Config$1, State$1, Api$1, {}> & {
1810
2223
  meta: typeof meta;
1811
2224
  og: typeof og;
1812
2225
  twitter: typeof twitter;
@@ -1847,6 +2260,6 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
1847
2260
  * const app = createApp({ plugins: [analytics] });
1848
2261
  * ```
1849
2262
  */
1850
- declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$4, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
2263
+ declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$5, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
1851
2264
  //#endregion
1852
- export { types_d_exports as Data, types_d_exports$1 as Env, types_d_exports$2 as Head, types_d_exports$3 as Log, types_d_exports$4 as Router, types_d_exports$5 as Spa, browserEnv, buildArticleHead, canonical, createApp, createComponent, createPlugin, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
2265
+ export { types_d_exports as Content, types_d_exports$1 as Data, types_d_exports$2 as Env, types_d_exports$3 as Head, types_d_exports$4 as Log, types_d_exports$5 as Router, types_d_exports$6 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };