@moku-labs/web 2.0.1 → 2.1.0

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.
package/README.md CHANGED
@@ -125,6 +125,51 @@ const app = createApp({
125
125
  await app.start(); // spa mounts islands onto the SSR'd DOM and intercepts navigation
126
126
  ```
127
127
 
128
+ ### Per-route transitions, scroll, and programmatic nav
129
+
130
+ The *behaviour* of a navigation is declared on the **route** (a typed framework directive, not free-form `.meta()`), with an app-wide default on the `spa` plugin. Both `.transition()` and `.scroll()` override the default for navigations **to** that route:
131
+
132
+ ```ts
133
+ spa: { islands, viewTransitions: "crossfade", scrollRestoration: "top" } // app defaults
134
+
135
+ route("/board/{id}").transition("slide") // board↔board slides
136
+ route("/board/{id}/issue/{issueId}")
137
+ .transition("morph") // the card morphs into the panel
138
+ .scroll("preserve") // overlay: don't move the board behind it
139
+ ```
140
+
141
+ - **`viewTransitions`** accepts `boolean` (`true` → `"crossfade"`, `false` → `"none"`) **or** a named `TransitionMode` (`"none" | "crossfade" | "slide" | "morph"`). A named transition tags the swap so your CSS can target it two ways — `:root[data-view-transition~="slide"]` (every View-Transitions engine) and the standards-track `:active-view-transition-type(slide)` — and a shared `view-transition-name` left across the swap morphs one element into another. Reduced-motion and unsupported browsers fall back to an instant swap.
142
+ - **`scrollRestoration`** is `"top"` (reset, the default) or `"preserve"` (keep the current scroll — e.g. opening an overlay route over content that must stay still). Back/forward always restores its saved position.
143
+ - **`ctx.navigate(path, options?)`** — every island can navigate programmatically with no `app` handle (an always-present context member, like `ctx.set`/`ctx.url`). `app.spa.navigate` takes the same optional `{ scroll }`.
144
+
145
+ ```ts
146
+ createIsland("issue", {
147
+ // `null` renders nothing but stays mountable (Preact unmount, not innerHTML="") —
148
+ // the correct "empty" for a persistent overlay panel, so it re-opens cleanly:
149
+ render: (s) => (s.open ? h(IssuePanel, { issue: s.issue }) : null),
150
+ events: { "click [data-close]": (ctx) => ctx.navigate(ctx.url("board", { id: ctx.params.id })) },
151
+ });
152
+ ```
153
+
154
+ ### Live realtime channels
155
+
156
+ `createChannel` is a self-healing client WebSocket primitive for islands that consume a server push stream (e.g. a Durable Object). It owns reconnect-with-backoff, an optional keepalive, a pre-seed buffer that closes the connect→load race, refcounted `subscribe` (first in connects, last out disconnects), and an optimistic `deliverLocal` echo — browser-only and tree-shaken away unless used:
157
+
158
+ ```ts
159
+ import { createChannel } from "@moku-labs/web/browser";
160
+
161
+ const board = createChannel<BoardPatch>({
162
+ url: (id) => `${location.origin.replace(/^http/, "ws")}/ws/board/${id}`,
163
+ keepAlive: { send: "ping", ignore: "pong" },
164
+ bufferUntilSeed: true,
165
+ });
166
+ // island onMount:
167
+ const off = board.subscribe(boardId, (patch) => applyPatch(ctx, patch));
168
+ ctx.cleanup(off);
169
+ const snapshot = await getBoard(boardId);
170
+ board.seed(boardId); // flush anything that arrived during the load
171
+ ```
172
+
128
173
  ## How it works
129
174
 
130
175
  ### Three layers, one `createApp`
@@ -278,7 +278,7 @@ interface PathMatcher {
278
278
  } | null;
279
279
  }
280
280
  declare namespace types_d_exports$3 {
281
- 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 };
281
+ 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, ScrollMode, State$2 as State, TransitionMode, TypedRoute, Urls };
282
282
  }
283
283
  /**
284
284
  * Param contribution of a single path segment. `{name:?}` / `:name?` → optional;
@@ -426,6 +426,27 @@ interface HeadConfig$1 {
426
426
  /** Arbitrary extra head fields. */
427
427
  readonly [key: string]: unknown;
428
428
  }
429
+ /**
430
+ * Named view-transition behaviour for a navigation TO a route — a closed, typed
431
+ * vocabulary the SPA kernel interprets (NOT free-form app data, so it earns a
432
+ * first-class `.transition()` method instead of a `.meta()` key):
433
+ * - `"none"` — swap with no View Transition (instant);
434
+ * - `"crossfade"` — the default root crossfade (`startViewTransition(swap)`);
435
+ * - `"slide"` / `"morph"` — a named transition; the kernel tags the transition with
436
+ * `types: ["slide"|"morph"]` so consumer CSS can target `:active-view-transition-type(slide)`
437
+ * (and shared `view-transition-name`s morph one element into another).
438
+ *
439
+ * A route's `.transition()` OVERRIDES the app-wide default (`spa.viewTransitions`).
440
+ */
441
+ type TransitionMode = "none" | "crossfade" | "slide" | "morph";
442
+ /**
443
+ * Scroll behaviour for a navigation TO a route — a closed, typed vocabulary the SPA
444
+ * kernel interprets (a first-class `.scroll()` method, not a `.meta()` key):
445
+ * - `"top"` — reset to the top of the page on the swap (the default for forward navs);
446
+ * - `"preserve"` — keep the current scroll position (e.g. opening an overlay/issue route
447
+ * over a board that must stay still). A route's `.scroll()` OVERRIDES `spa.scrollRestoration`.
448
+ */
449
+ type ScrollMode = "top" | "preserve";
429
450
  /**
430
451
  * Fluent route builder. Each chain method returns the same builder with a
431
452
  * (possibly widened) state generic. Only `.load()` widens the data type `D`.
@@ -458,6 +479,24 @@ interface RouteBuilder<S extends RouteState> extends RouteDefinition {
458
479
  * projected verbatim into `clientManifest()` and shipped to the browser.
459
480
  */
460
481
  meta(meta: Record<string, unknown>): RouteBuilder<S>;
482
+ /**
483
+ * Declare the view-transition behaviour for navigations TO this route — a typed
484
+ * framework directive (the SPA kernel reads it), overriding the app-wide
485
+ * `spa.viewTransitions` default. See {@link TransitionMode}.
486
+ *
487
+ * @example
488
+ * route("/board/{id}/issue/{issueId}").transition("morph"); // card → panel
489
+ */
490
+ transition(mode: TransitionMode): RouteBuilder<S>;
491
+ /**
492
+ * Declare the scroll behaviour for navigations TO this route — a typed framework
493
+ * directive (the SPA kernel reads it), overriding the app-wide
494
+ * `spa.scrollRestoration` default. See {@link ScrollMode}.
495
+ *
496
+ * @example
497
+ * route("/board/{id}/issue/{issueId}").scroll("preserve"); // overlay: don't move the board
498
+ */
499
+ scroll(mode: ScrollMode): RouteBuilder<S>;
461
500
  /** Attach a JSON serializer for the route's data. */
462
501
  toJson(handler: (ctx: RouteContext<S>) => unknown): RouteBuilder<S>;
463
502
  /** Override the output file-path producer. */
@@ -490,6 +529,10 @@ interface RouteDefinition {
490
529
  readonly pattern: string;
491
530
  /** Metadata bag accumulated from `.meta()` (named `_meta` to avoid clashing with the `.meta()` builder method). */
492
531
  readonly _meta: Record<string, unknown>;
532
+ /** Named view-transition behaviour from `.transition()` (read by the SPA kernel); undefined → app default. */
533
+ readonly _transition?: TransitionMode;
534
+ /** Scroll behaviour from `.scroll()` (read by the SPA kernel); undefined → app default. */
535
+ readonly _scroll?: ScrollMode;
493
536
  /** Build-time handler bag (load/render/head/generate/toJson/toFile). */
494
537
  readonly _handlers: RouteHandlers;
495
538
  }
@@ -871,7 +914,12 @@ type Api$1 = {
871
914
  composeTitle(head: HeadConfig | undefined): string;
872
915
  };
873
916
  declare namespace types_d_exports$4 {
874
- export { AnyVNode, ExtractApi, ISLAND_HOOK_NAMES, IslandContext, IslandDef, IslandEventHandler, IslandEvents, IslandHooks, IslandInstance, IslandRender, IslandRouteSlice, IslandSpec, IslandSpecExtras, IslandStateFactory, PageData, RenderResult, ResolvedSpaConfig, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState };
917
+ export { AnyVNode, ExtractApi, ISLAND_HOOK_NAMES, IslandContext, IslandDef, IslandEventHandler, IslandEvents, IslandHooks, IslandInstance, IslandRender, IslandRouteSlice, IslandSpec, IslandSpecExtras, IslandStateFactory, NavigateOptions, PageData, RenderResult, ResolvedSpaConfig, ScrollMode, SpaApi, SpaConfig, SpaContext, SpaDataReader, SpaEmitFunction, SpaEvents, SpaKernel, SpaKernelDeps, SpaRequire, SpaState, TransitionMode };
918
+ }
919
+ /** Per-navigation options for the programmatic navigators (`ctx.navigate` / `app.spa.navigate`). */
920
+ interface NavigateOptions {
921
+ /** Override the scroll behaviour for this single navigation (else the route/app default). */
922
+ scroll?: ScrollMode;
875
923
  }
876
924
  /** Payload map for the events `spa` emits, used to type the kernel's `emit` closure. */
877
925
  type SpaEvents = {
@@ -946,10 +994,21 @@ type SpaConfig = {
946
994
  */
947
995
  swapSelector?: string;
948
996
  /**
949
- * Use the View Transitions API for cross-fade swaps when available.
950
- * Falls back to an instant swap when unsupported. Defaults to `false`.
997
+ * The app-wide default View-Transition behaviour for navigations. Accepts a
998
+ * boolean (legacy: `true` → `"crossfade"`, `false` → `"none"`) OR a named
999
+ * {@link TransitionMode} (`"none" | "crossfade" | "slide" | "morph"`). A route's
1000
+ * `.transition()` overrides this per-route. Falls back to an instant swap when the
1001
+ * View Transitions API is unsupported or under `prefers-reduced-motion`. Defaults
1002
+ * to `false` (`"none"`).
1003
+ */
1004
+ viewTransitions?: boolean | TransitionMode;
1005
+ /**
1006
+ * The app-wide default scroll behaviour for forward navigations: `"top"` (reset to
1007
+ * the top, the default) or `"preserve"` (keep the current scroll). A route's
1008
+ * `.scroll()` overrides this per-route. Traverse (back/forward) always restores its
1009
+ * saved position regardless. Defaults to `"top"`.
951
1010
  */
952
- viewTransitions?: boolean;
1011
+ scrollRestoration?: ScrollMode;
953
1012
  /**
954
1013
  * Show the in-house top progress bar during navigation. Defaults to `true`.
955
1014
  */
@@ -964,8 +1023,12 @@ type SpaConfig = {
964
1023
  interface ResolvedSpaConfig {
965
1024
  /** CSS selector for the swapped page region. */
966
1025
  swapSelector: string;
967
- /** Whether View Transitions are enabled. */
1026
+ /** Whether View Transitions are enabled at all (false ⇒ always instant swap). */
968
1027
  viewTransitions: boolean;
1028
+ /** The app-wide default transition mode applied when a route declares none. */
1029
+ defaultTransition: TransitionMode;
1030
+ /** The app-wide default scroll behaviour applied when a route declares none. */
1031
+ scrollRestoration: ScrollMode;
969
1032
  /** Whether the progress bar is enabled. */
970
1033
  progressBar: boolean;
971
1034
  /** Pre-registered islands. */
@@ -976,9 +1039,14 @@ interface ResolvedSpaConfig {
976
1039
  * - a Preact `VNode` — committed into the host through the lazy Preact gate (`commitVNode`);
977
1040
  * - a `Node` — replaces the host's children;
978
1041
  * - a `string` — set as the host's `innerHTML`;
1042
+ * - `null` — render NOTHING but stay mountable: the host's Preact tree is unmounted via
1043
+ * `render(null, host)` (NOT `innerHTML = ""`), so a later non-empty render re-commits
1044
+ * cleanly. This is the correct "empty" for a persistent render-island (e.g. a closed
1045
+ * overlay panel) — returning `""` instead corrupts Preact's retained vdom and the panel
1046
+ * never re-opens;
979
1047
  * - `void`/`undefined` — the render mutated the DOM itself (DOM-only islands → no Preact loaded).
980
1048
  */
981
- type RenderResult = AnyVNode | Node | string | void;
1049
+ type RenderResult = AnyVNode | Node | string | null | void;
982
1050
  /**
983
1051
  * A Preact `VNode` of ANY props shape. A render returns `h(Component, props)`, i.e. a
984
1052
  * `VNode<SomeProps>`; the props generic is invariant under `exactOptionalPropertyTypes`,
@@ -1064,6 +1132,21 @@ interface IslandContext<S = undefined> {
1064
1132
  readonly locale: string;
1065
1133
  /** Build a link to a named route by pattern substitution (same output as `app.router.toUrl`). */
1066
1134
  readonly url: (name: string, params?: Record<string, string>) => string;
1135
+ /**
1136
+ * Programmatically navigate the SPA to an internal path (same swap pipeline as a
1137
+ * link click; no-op without a DOM). Build `path` from the route map's `urls`/`ctx.url`,
1138
+ * never a literal. Pass `{ scroll: "preserve" }` to keep the current scroll for this nav.
1139
+ *
1140
+ * An ALWAYS-PRESENT member (like `set`/`flush`/`cleanup`) so every island can navigate
1141
+ * without an `app` handle — no synthesized anchor click.
1142
+ *
1143
+ * @param path - The internal destination path (e.g. `ctx.url("board", { id })`).
1144
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
1145
+ * @returns void
1146
+ * @example
1147
+ * ctx.navigate(ctx.url("issue", { id, issueId }));
1148
+ */
1149
+ readonly navigate: (path: string, options?: NavigateOptions) => void;
1067
1150
  /** The live per-instance state (the object returned by `spec.state`). `undefined` for legacy hooks-only islands. */
1068
1151
  readonly state: S;
1069
1152
  /**
@@ -1306,11 +1389,12 @@ interface SpaKernel {
1306
1389
  * Process a navigation to `path`: fetch then swap then head-sync then emit.
1307
1390
  *
1308
1391
  * @param path - The target path to navigate to.
1392
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
1309
1393
  * @returns void
1310
1394
  * @example
1311
1395
  * kernel.processNav("/about");
1312
1396
  */
1313
- processNav(path: string): void;
1397
+ processNav(path: string, options?: NavigateOptions): void;
1314
1398
  /**
1315
1399
  * Query the swap region and mount islands for matching elements.
1316
1400
  *
@@ -1342,6 +1426,12 @@ interface SpaState {
1342
1426
  destroyRouter: (() => void) | null;
1343
1427
  /** Whether the browser runtime has been booted. */
1344
1428
  started: boolean;
1429
+ /**
1430
+ * The kernel's programmatic navigator, bound at init (null until then). The island
1431
+ * context's `ctx.navigate` reads through this — the same decoupled seam `islandApis`
1432
+ * uses — so `islands.ts` never imports the kernel.
1433
+ */
1434
+ navigate: ((path: string, options?: NavigateOptions) => void) | null;
1345
1435
  /** The single shared SPA kernel instance (null until onInit builds it). */
1346
1436
  kernel: SpaKernel | null;
1347
1437
  }
@@ -1360,11 +1450,12 @@ type SpaApi = {
1360
1450
  * Programmatically navigate to a path (client runtime; no-op without a DOM).
1361
1451
  *
1362
1452
  * @param path - Target path (pathname, optionally with search/hash).
1453
+ * @param options - Optional per-navigation overrides (e.g. `{ scroll: "preserve" }`).
1363
1454
  * @returns void
1364
1455
  * @example
1365
1456
  * app.spa.navigate("/about");
1366
1457
  */
1367
- navigate(path: string): void;
1458
+ navigate(path: string, options?: NavigateOptions): void;
1368
1459
  /**
1369
1460
  * Read the current resolved URL.
1370
1461
  *
@@ -1600,6 +1691,112 @@ declare const headPlugin: import("@moku-labs/core").PluginInstance<"head", Confi
1600
1691
  buildArticleHead: typeof buildArticleHead;
1601
1692
  };
1602
1693
  //#endregion
1694
+ //#region src/plugins/spa/channel.d.ts
1695
+ /**
1696
+ * @file spa plugin — `createChannel`, a client realtime WebSocket primitive.
1697
+ *
1698
+ * A live, self-healing message channel for islands that consume a server push stream
1699
+ * (e.g. a Durable Object broadcasting board patches). It owns the parts every real-time
1700
+ * app otherwise hand-rolls: a single active socket bound to one channel `id`, refcounted
1701
+ * `subscribe` (first subscriber connects, last disconnects — so cross-island mount order
1702
+ * stops being load-bearing), exponential-backoff auto-reconnect, an optional keepalive
1703
+ * ping, a pre-seed buffer (frames that arrive before the island's snapshot loads are
1704
+ * queued and flushed on `seed`, so the connect→load race drops nothing), and an
1705
+ * optimistic `deliverLocal` echo.
1706
+ *
1707
+ * Browser-only (uses `WebSocket`); reached ONLY through an explicit `createChannel(...)`
1708
+ * call, so an app that never opens a channel never pulls this module into its graph.
1709
+ * @see {@link createChannel} for the usage example.
1710
+ */
1711
+ /** Keepalive configuration: a frame sent on an interval, and the reply to ignore. */
1712
+ interface ChannelKeepAlive {
1713
+ /** The keepalive frame to send (e.g. `"ping"`). */
1714
+ send: string;
1715
+ /** A reply frame to ignore rather than parse (e.g. `"pong"`). */
1716
+ ignore?: string;
1717
+ /** Interval between keepalive sends, in ms. Defaults to `30000`. */
1718
+ everyMs?: number;
1719
+ }
1720
+ /** Auto-reconnect backoff configuration (exponential, capped). */
1721
+ interface ChannelReconnect {
1722
+ /** First retry delay in ms (doubles each attempt). Defaults to `500`. */
1723
+ baseMs?: number;
1724
+ /** Maximum retry delay in ms (the backoff ceiling). Defaults to `8000`. */
1725
+ maxMs?: number;
1726
+ }
1727
+ /** Options for {@link createChannel}. */
1728
+ interface ChannelOptions<T> {
1729
+ /** Build the `ws(s)://` URL for a channel `id`. */
1730
+ url: (id: string) => string;
1731
+ /**
1732
+ * Parse a raw text frame into a message, or return `undefined` to drop it (malformed
1733
+ * frame / keepalive reply). Defaults to `JSON.parse` (dropping frames that fail to parse).
1734
+ */
1735
+ parse?: (raw: string) => T | undefined;
1736
+ /** Keepalive ping configuration (off when omitted). */
1737
+ keepAlive?: ChannelKeepAlive;
1738
+ /** Auto-reconnect backoff (defaults applied when omitted; pass `false` to disable). */
1739
+ reconnect?: ChannelReconnect | false;
1740
+ /**
1741
+ * Buffer frames that arrive before {@link Channel.seed} and flush them (in arrival
1742
+ * order) on seed, closing the connect→load race. Defaults to `false` (deliver live).
1743
+ */
1744
+ bufferUntilSeed?: boolean;
1745
+ }
1746
+ /** A live realtime channel (the surface returned by {@link createChannel}). */
1747
+ interface Channel<T> {
1748
+ /**
1749
+ * Subscribe to messages for channel `id`, connecting (or switching) the single
1750
+ * active socket to it. Refcounted: the first subscriber connects, the last to
1751
+ * unsubscribe disconnects. Returns an unsubscribe function.
1752
+ *
1753
+ * @param id - The channel id to subscribe to.
1754
+ * @param handler - Called with each delivered message.
1755
+ * @returns A function that removes this handler (and disconnects when it was the last).
1756
+ */
1757
+ subscribe(id: string, handler: (message: T) => void): () => void;
1758
+ /**
1759
+ * Mark the channel seeded and flush the pre-seed buffer (no-op unless
1760
+ * `bufferUntilSeed` and `id` is the active channel). Idempotent.
1761
+ *
1762
+ * @param id - The channel id whose buffer to flush (must be the active id).
1763
+ */
1764
+ seed(id: string): void;
1765
+ /**
1766
+ * Deliver a message to the local handlers immediately (the optimistic-update path —
1767
+ * the acting client reflects its own mutation without waiting for the server echo).
1768
+ *
1769
+ * @param id - The channel id (must be the active id; ignored otherwise).
1770
+ * @param message - The message to deliver locally now.
1771
+ */
1772
+ deliverLocal(id: string, message: T): void;
1773
+ /**
1774
+ * Send a raw text frame over the live socket (no-op when not open).
1775
+ *
1776
+ * @param raw - The frame to send.
1777
+ */
1778
+ send(raw: string): void;
1779
+ /**
1780
+ * The currently-subscribed channel id, or `undefined` when disconnected.
1781
+ *
1782
+ * @returns The active id, or undefined.
1783
+ */
1784
+ current(): string | undefined;
1785
+ /** Close the socket, cancel reconnect, and clear all handlers. */
1786
+ disconnect(): void;
1787
+ }
1788
+ /**
1789
+ * Create a live realtime channel — a single self-healing WebSocket bound to one channel
1790
+ * `id` at a time, with refcounted subscription, auto-reconnect, an optional keepalive, a
1791
+ * pre-seed buffer, and an optimistic local-echo path. See the file header for the model.
1792
+ *
1793
+ * @param options - The channel configuration (see {@link ChannelOptions}).
1794
+ * @returns The {@link Channel} control surface.
1795
+ * @example
1796
+ * const board = createChannel<BoardPatch>({ url: (id) => `wss://host/ws/board/${id}` });
1797
+ */
1798
+ declare function createChannel<T>(options: ChannelOptions<T>): Channel<T>;
1799
+ //#endregion
1603
1800
  //#region src/plugins/spa/islands.d.ts
1604
1801
  /**
1605
1802
  * Create a validated island definition. Accepts either the legacy hooks-only form
@@ -2417,4 +2614,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
2417
2614
  */
2418
2615
  declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$5, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", import("@moku-labs/common").LogConfig, import("@moku-labs/common").LogState, import("@moku-labs/common").LogApi>, import("@moku-labs/core").CorePluginInstance<"env", import("@moku-labs/common").EnvConfig, import("@moku-labs/common").EnvState, import("@moku-labs/common").EnvApi>]>>;
2419
2616
  //#endregion
2420
- export { types_d_exports as Content, types_d_exports$1 as Data, type Env, types_d_exports$2 as Head, type Log, types_d_exports$3 as Router, types_d_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };
2617
+ export { type Channel, type ChannelKeepAlive, type ChannelOptions, type ChannelReconnect, types_d_exports as Content, types_d_exports$1 as Data, type Env, types_d_exports$2 as Head, type Log, types_d_exports$3 as Router, types_d_exports$4 as Spa, browserEnv, buildArticleHead, canonical, contentPlugin, createApp, createChannel, createIsland, createPlugin, createUrls, dataPlugin, defineRoutes, envPlugin, feedLink, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, route, routerPlugin, sitePlugin, spaPlugin, twitter };