@moku-labs/web 1.9.0 → 1.11.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/dist/browser.d.mts +69 -3
- package/dist/browser.mjs +84 -1
- package/dist/index.cjs +409 -30
- package/dist/index.d.cts +89 -3
- package/dist/index.d.mts +89 -3
- package/dist/index.mjs +408 -31
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EmitFn } from "@moku-labs/core";
|
|
2
|
-
import { ComponentChildren, VNode } from "preact";
|
|
2
|
+
import { ComponentChildren, FunctionComponent, VNode } from "preact";
|
|
3
3
|
import { Pluggable, Processor } from "unified";
|
|
4
4
|
import { BundledTheme, ThemeRegistrationAny } from "shiki";
|
|
5
5
|
|
|
@@ -2671,7 +2671,7 @@ type Api$1 = {
|
|
|
2671
2671
|
*/
|
|
2672
2672
|
declare const cliPlugin: import("@moku-labs/core").PluginInstance<"cli", Config$1, State$1, Api$1, {}> & Record<never, never>;
|
|
2673
2673
|
declare namespace types_d_exports$2 {
|
|
2674
|
-
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2674
|
+
export { Api, Article, ArticleCard, ComputedFields, Config, ContentApiContext, ContentEvents, ContentProvider, ContentProviderState, EmbedFacade, EmbedFacadeProps, EmbedOptions, FileSystemContentOptions, Frontmatter, LoadAllOptions, MermaidDiagramOptions, State };
|
|
2675
2675
|
}
|
|
2676
2676
|
/**
|
|
2677
2677
|
* YAML frontmatter parsed from each article file.
|
|
@@ -2815,6 +2815,55 @@ type MermaidDiagramOptions = {
|
|
|
2815
2815
|
*/
|
|
2816
2816
|
renderDiagrams?: (sources: readonly string[], mermaidConfig?: Record<string, unknown>) => Promise<readonly string[]>;
|
|
2817
2817
|
};
|
|
2818
|
+
/**
|
|
2819
|
+
* Props handed to an `::embed` facade component (the click-to-activate placeholder
|
|
2820
|
+
* the framework renders to static markup at build time). `width`/`height` are the
|
|
2821
|
+
* parsed pixel dimensions when the directive set them; `attributes` is the full raw
|
|
2822
|
+
* directive attribute bag, so a custom facade can read arbitrary extra options
|
|
2823
|
+
* (e.g. `::embed{… poster="/p.jpg" label="Play"}`).
|
|
2824
|
+
*
|
|
2825
|
+
* @example
|
|
2826
|
+
* ```tsx
|
|
2827
|
+
* const Facade = ({ title, attributes }: EmbedFacadeProps) => (
|
|
2828
|
+
* <button type="button" class="lazy-embed-button">
|
|
2829
|
+
* {attributes.poster ? <img src={attributes.poster} alt="" /> : null}
|
|
2830
|
+
* <span class="lazy-embed-title">{title}</span>
|
|
2831
|
+
* </button>
|
|
2832
|
+
* );
|
|
2833
|
+
* ```
|
|
2834
|
+
*/
|
|
2835
|
+
type EmbedFacadeProps = {
|
|
2836
|
+
/** The embed target exactly as written in the directive (the provider resolves it later). */src: string; /** The human-readable embed title (default label + iframe title). */
|
|
2837
|
+
title: string; /** Reserved-box width in pixels, when the directive set `width`/`height`. */
|
|
2838
|
+
width?: number; /** Reserved-box height in pixels, when the directive set `width`/`height`. */
|
|
2839
|
+
height?: number; /** The full raw directive attribute bag (custom options live here). */
|
|
2840
|
+
attributes: Readonly<Record<string, string>>;
|
|
2841
|
+
};
|
|
2842
|
+
/**
|
|
2843
|
+
* A consumer-supplied facade component: a Preact function component over
|
|
2844
|
+
* {@link EmbedFacadeProps}, rendered (at build time, to static markup) as the
|
|
2845
|
+
* facade's inner content — inside the framework-owned `<figure>` that carries the
|
|
2846
|
+
* island hooks + reserved-box sizing. Defaults to the built-in `EmbedFacadeButton`.
|
|
2847
|
+
*/
|
|
2848
|
+
type EmbedFacade = FunctionComponent<EmbedFacadeProps>;
|
|
2849
|
+
/**
|
|
2850
|
+
* Options for the `::embed` lazy-iframe feature (the `embed` key of
|
|
2851
|
+
* {@link FileSystemContentOptions}). `embed: true` uses the default facade;
|
|
2852
|
+
* `embed: { facade }` swaps in a consumer Preact component for the placeholder.
|
|
2853
|
+
*
|
|
2854
|
+
* @example
|
|
2855
|
+
* ```ts
|
|
2856
|
+
* fileSystemContent({ contentDir: "./content", trustedContent: true, embed: { facade: MyFacade } });
|
|
2857
|
+
* ```
|
|
2858
|
+
*/
|
|
2859
|
+
type EmbedOptions = {
|
|
2860
|
+
/**
|
|
2861
|
+
* Consumer Preact component rendering the facade's inner content (SSR'd to
|
|
2862
|
+
* static markup at build — no client JS). Receives {@link EmbedFacadeProps}.
|
|
2863
|
+
* Defaults to the built-in `EmbedFacadeButton`.
|
|
2864
|
+
*/
|
|
2865
|
+
facade?: EmbedFacade;
|
|
2866
|
+
};
|
|
2818
2867
|
/**
|
|
2819
2868
|
* Options for the node filesystem provider {@link ContentProvider} `fileSystemContent`.
|
|
2820
2869
|
* These are the markdown-pipeline + source concerns that used to live on the content
|
|
@@ -2849,6 +2898,16 @@ type FileSystemContentOptions = {
|
|
|
2849
2898
|
* (plus playwright with an installed browser). Defaults to disabled.
|
|
2850
2899
|
*/
|
|
2851
2900
|
mermaid?: boolean | MermaidDiagramOptions;
|
|
2901
|
+
/**
|
|
2902
|
+
* Lazy iframe embeds: rewrite `::embed{src="…" title="…"}` leaf directives
|
|
2903
|
+
* into static click-to-activate facades (no iframe — and none of the target's
|
|
2904
|
+
* network/JS cost — until the reader clicks). Pair with the `lazyEmbed` SPA
|
|
2905
|
+
* island, which swaps the facade for the real `<iframe loading="lazy">`.
|
|
2906
|
+
* `true` enables with the default facade; an object passes {@link EmbedOptions}
|
|
2907
|
+
* (e.g. a consumer `facade` Preact component). Requires `trustedContent: true`
|
|
2908
|
+
* (the facade is raw HTML the sanitize pass would strip). Defaults to disabled.
|
|
2909
|
+
*/
|
|
2910
|
+
embed?: boolean | EmbedOptions;
|
|
2852
2911
|
};
|
|
2853
2912
|
/**
|
|
2854
2913
|
* Internal mutable state of the filesystem provider: the lazy unified processor and
|
|
@@ -3486,6 +3545,13 @@ declare const sitePlugin: import("@moku-labs/core").PluginInstance<"site", Confi
|
|
|
3486
3545
|
*/
|
|
3487
3546
|
declare function createComponent(name: string, hooks: ComponentHooks): ComponentDef;
|
|
3488
3547
|
//#endregion
|
|
3548
|
+
//#region src/plugins/spa/lazy-embed.d.ts
|
|
3549
|
+
/**
|
|
3550
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
3551
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
3552
|
+
*/
|
|
3553
|
+
declare const lazyEmbed: ComponentDef;
|
|
3554
|
+
//#endregion
|
|
3489
3555
|
//#region src/plugins/spa/index.d.ts
|
|
3490
3556
|
/**
|
|
3491
3557
|
* SPA plugin — progressive client-side navigation layered over the static site:
|
|
@@ -3581,6 +3647,26 @@ declare function cloudflareBindings(): EnvProvider;
|
|
|
3581
3647
|
*/
|
|
3582
3648
|
declare function fileSystemContent(options: FileSystemContentOptions): ContentProvider;
|
|
3583
3649
|
//#endregion
|
|
3650
|
+
//#region src/plugins/content/pipeline/embed-facade.d.ts
|
|
3651
|
+
/**
|
|
3652
|
+
* Default `::embed` facade inner content: a single labelled `<button>` carrying
|
|
3653
|
+
* the embed title. The companion `lazyEmbed` island activates the embed on a
|
|
3654
|
+
* click anywhere in the facade, so the button is the keyboard-accessible
|
|
3655
|
+
* control. Provided as the default and as a composable building block for custom
|
|
3656
|
+
* facades.
|
|
3657
|
+
*
|
|
3658
|
+
* @param props - The embed facade props (only `title` is used by the default).
|
|
3659
|
+
* @returns The facade inner-content VNode.
|
|
3660
|
+
* @example
|
|
3661
|
+
* ```tsx
|
|
3662
|
+
* // Compose the default inside a richer custom facade:
|
|
3663
|
+
* const MyFacade = (p: EmbedFacadeProps) => (
|
|
3664
|
+
* <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
|
|
3665
|
+
* );
|
|
3666
|
+
* ```
|
|
3667
|
+
*/
|
|
3668
|
+
declare function EmbedFacadeButton(props: EmbedFacadeProps): VNode;
|
|
3669
|
+
//#endregion
|
|
3584
3670
|
//#region src/index.d.ts
|
|
3585
3671
|
/**
|
|
3586
3672
|
* Create and initialize a `@moku-labs/web` application — the Layer-3 entry point.
|
|
@@ -3687,4 +3773,4 @@ declare const createApp: <const ExtraPlugins extends readonly import("@moku-labs
|
|
|
3687
3773
|
*/
|
|
3688
3774
|
declare const createPlugin: import("@moku-labs/core").BoundCreatePluginFunction<Config$8, Events, import("@moku-labs/core").CoreApisFromTuple<[import("@moku-labs/core").CorePluginInstance<"log", LogConfig, LogState, LogApi>, import("@moku-labs/core").CorePluginInstance<"env", EnvConfig, EnvState, EnvApi>]>>;
|
|
3689
3775
|
//#endregion
|
|
3690
|
-
export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
3776
|
+
export { types_d_exports as Build, types_d_exports$1 as Cli, types_d_exports$2 as Content, types_d_exports$3 as Data, types_d_exports$4 as Deploy, type EmbedFacade, EmbedFacadeButton, type EmbedFacadeProps, type EmbedOptions, types_d_exports$5 as Env, types_d_exports$6 as Head, types_d_exports$7 as Log, types_d_exports$8 as Router, types_d_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
package/dist/index.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import remarkGfm from "remark-gfm";
|
|
|
24
24
|
import remarkParse from "remark-parse";
|
|
25
25
|
import remarkRehype from "remark-rehype";
|
|
26
26
|
import { visit } from "unist-util-visit";
|
|
27
|
+
import { jsx } from "preact/jsx-runtime";
|
|
27
28
|
import { defaultSchema } from "hast-util-sanitize";
|
|
28
29
|
import readingTime from "reading-time";
|
|
29
30
|
//#region src/plugins/env/api.ts
|
|
@@ -1569,13 +1570,14 @@ function validateContentConfig(config) {
|
|
|
1569
1570
|
}
|
|
1570
1571
|
/**
|
|
1571
1572
|
* Validates the `fileSystemContent` provider options (fail-fast at provider
|
|
1572
|
-
* construction). Throws when `mermaid` is enabled without
|
|
1573
|
-
*
|
|
1574
|
-
* XSS boundary) would strip — so
|
|
1575
|
-
* `[web]` prefix.
|
|
1573
|
+
* construction). Throws when `mermaid` or `embed` is enabled without
|
|
1574
|
+
* `trustedContent: true`: both emit raw HTML (inline SVG / the embed facade),
|
|
1575
|
+
* which the sanitize pass (the untrusted-content XSS boundary) would strip — so
|
|
1576
|
+
* the combination can never work. Errors use the `[web]` prefix.
|
|
1576
1577
|
*
|
|
1577
1578
|
* @param options - The provider options to validate.
|
|
1578
|
-
* @throws {Error} If `mermaid` is enabled while `trustedContent` is
|
|
1579
|
+
* @throws {Error} If `mermaid` or `embed` is enabled while `trustedContent` is
|
|
1580
|
+
* not `true`.
|
|
1579
1581
|
* @example
|
|
1580
1582
|
* ```ts
|
|
1581
1583
|
* validateFileSystemContentOptions({ contentDir: "./content", trustedContent: true, mermaid: true });
|
|
@@ -1583,6 +1585,7 @@ function validateContentConfig(config) {
|
|
|
1583
1585
|
*/
|
|
1584
1586
|
function validateFileSystemContentOptions(options) {
|
|
1585
1587
|
if (Boolean(options.mermaid) && options.trustedContent !== true) throw new Error("[web] content: `mermaid` requires `trustedContent: true`.\n Mermaid diagrams render to raw inline SVG, which the sanitize pass would strip.\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1588
|
+
if (Boolean(options.embed) && options.trustedContent !== true) throw new Error("[web] content: `embed` requires `trustedContent: true`.\n Embed directives render to a raw-HTML facade, which the sanitize pass would strip\n (and embedding third-party iframes is never safe for untrusted Markdown).\n Set trustedContent: true ONLY for fully author-controlled Markdown.");
|
|
1586
1589
|
}
|
|
1587
1590
|
//#endregion
|
|
1588
1591
|
//#region src/plugins/content/index.ts
|
|
@@ -4038,19 +4041,24 @@ function readCachedContent(ctx) {
|
|
|
4038
4041
|
//#endregion
|
|
4039
4042
|
//#region src/plugins/build/phases/content-images.ts
|
|
4040
4043
|
/**
|
|
4041
|
-
* @file build phase — content-images. Copies each article's co-located
|
|
4042
|
-
* (`<contentDir>/<slug
|
|
4043
|
-
*
|
|
4044
|
-
*
|
|
4044
|
+
* @file build phase — content-images. Copies each article's co-located asset
|
|
4045
|
+
* directories (`<contentDir>/<slug>/<dir>/`, e.g. `images/` or a pre-built
|
|
4046
|
+
* `game/` embed bundle) to a single shared output location
|
|
4047
|
+
* (`<outDir>/<slug>/<dir>/`) reused by every locale, matching the absolute
|
|
4048
|
+
* `/<slug>/<dir>/...` URLs the content renderer emits (image src + `::embed`
|
|
4049
|
+
* src). Dot- and underscore-prefixed dirs are treated as private and skipped.
|
|
4050
|
+
* Gated by `config.images`.
|
|
4045
4051
|
*/
|
|
4046
|
-
/** Conventional per-article image subdirectory name (alongside `<slug>/<locale>.md`). */
|
|
4047
|
-
const ARTICLE_IMAGE_DIR = "images";
|
|
4048
4052
|
/**
|
|
4049
|
-
* Copy every article's co-located
|
|
4050
|
-
*
|
|
4053
|
+
* Copy every article's co-located asset directories to `<outDir>/<slug>/<dir>/`.
|
|
4054
|
+
* Each direct subdirectory of `<contentDir>/<slug>/` rides along (the
|
|
4055
|
+
* conventional `images/` dir plus any other bundle, like an `::embed` game),
|
|
4056
|
+
* except `.`/`_`-prefixed dirs (private). The `.md` source files are never
|
|
4057
|
+
* copied (only directories are). No-op when `config.images` is false or the
|
|
4058
|
+
* content directory does not exist.
|
|
4051
4059
|
*
|
|
4052
4060
|
* @param ctx - Plugin context (provides `config`, `log`, `require`).
|
|
4053
|
-
* @returns The number of
|
|
4061
|
+
* @returns The number of articles that had at least one asset directory copied.
|
|
4054
4062
|
* @example
|
|
4055
4063
|
* ```ts
|
|
4056
4064
|
* const copied = await copyContentImages(ctx);
|
|
@@ -4069,14 +4077,20 @@ async function copyContentImages(ctx) {
|
|
|
4069
4077
|
});
|
|
4070
4078
|
return 0;
|
|
4071
4079
|
}
|
|
4072
|
-
const
|
|
4080
|
+
const articleDirectories = await readdir(contentDir, { withFileTypes: true });
|
|
4073
4081
|
let copied = 0;
|
|
4074
|
-
for (const
|
|
4075
|
-
if (!
|
|
4076
|
-
const
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4082
|
+
for (const article of articleDirectories) {
|
|
4083
|
+
if (!article.isDirectory()) continue;
|
|
4084
|
+
const articleDir = path.join(contentDir, article.name);
|
|
4085
|
+
const assetDirectories = await readdir(articleDir, { withFileTypes: true });
|
|
4086
|
+
let copiedAny = false;
|
|
4087
|
+
for (const asset of assetDirectories) {
|
|
4088
|
+
if (!asset.isDirectory()) continue;
|
|
4089
|
+
if (asset.name.startsWith(".") || asset.name.startsWith("_")) continue;
|
|
4090
|
+
await cp(path.join(articleDir, asset.name), path.join(ctx.config.outDir, article.name, asset.name), { recursive: true });
|
|
4091
|
+
copiedAny = true;
|
|
4092
|
+
}
|
|
4093
|
+
if (copiedAny) copied += 1;
|
|
4080
4094
|
}
|
|
4081
4095
|
ctx.log.debug("build:content-images", { copied });
|
|
4082
4096
|
return copied;
|
|
@@ -11299,6 +11313,89 @@ function disposeSpa() {
|
|
|
11299
11313
|
}
|
|
11300
11314
|
}
|
|
11301
11315
|
//#endregion
|
|
11316
|
+
//#region src/plugins/spa/lazy-embed.ts
|
|
11317
|
+
/**
|
|
11318
|
+
* @file `lazyEmbed` island — activates the static embed facades emitted by the
|
|
11319
|
+
* content pipeline's `::embed` directive (pipeline/embed.ts). Mounts on every
|
|
11320
|
+
* `[data-component="lazy-embed"]` figure; a click on the facade's button swaps
|
|
11321
|
+
* it for the real `<iframe loading="lazy">`. Until that click the embedded
|
|
11322
|
+
* document costs the page nothing — no request, no third-party JS, no
|
|
11323
|
+
* scroll-jacking. Register it in `pluginConfigs.spa.components`; all visual
|
|
11324
|
+
* chrome (`.lazy-embed*` classes) is consumer CSS.
|
|
11325
|
+
*/
|
|
11326
|
+
/** CSS class on the injected `<iframe>` (consumer CSS sizes it). */
|
|
11327
|
+
const EMBED_FRAME_CLASS = "lazy-embed-frame";
|
|
11328
|
+
/**
|
|
11329
|
+
* Swap a facade `<figure>`'s content for its real `<iframe>`. The iframe
|
|
11330
|
+
* carries `loading="lazy"` plus fullscreen permission, and the figure gains
|
|
11331
|
+
* `data-embed-active` so consumer CSS can restyle the activated state.
|
|
11332
|
+
*
|
|
11333
|
+
* @param figure - The facade element carrying `data-embed-src`/`data-embed-title`.
|
|
11334
|
+
* @example
|
|
11335
|
+
* ```ts
|
|
11336
|
+
* activateEmbed(figure);
|
|
11337
|
+
* ```
|
|
11338
|
+
*/
|
|
11339
|
+
function activateEmbed(figure) {
|
|
11340
|
+
const src = figure.dataset.embedSrc;
|
|
11341
|
+
if (!src) return;
|
|
11342
|
+
const iframe = document.createElement("iframe");
|
|
11343
|
+
iframe.src = src;
|
|
11344
|
+
iframe.title = figure.dataset.embedTitle ?? "";
|
|
11345
|
+
iframe.className = EMBED_FRAME_CLASS;
|
|
11346
|
+
iframe.setAttribute("loading", "lazy");
|
|
11347
|
+
iframe.allow = "fullscreen; autoplay; gamepad";
|
|
11348
|
+
iframe.allowFullscreen = true;
|
|
11349
|
+
figure.replaceChildren(iframe);
|
|
11350
|
+
figure.dataset.embedActive = "";
|
|
11351
|
+
}
|
|
11352
|
+
/**
|
|
11353
|
+
* Shared click handler (module-level so mount/unmount detach the same
|
|
11354
|
+
* reference): any click on the not-yet-active facade activates the embed. It
|
|
11355
|
+
* fires on the whole facade — not a specific button class — so a consumer's
|
|
11356
|
+
* custom facade markup (see content `embed.facade`) works without re-wiring;
|
|
11357
|
+
* the default facade's `<button>` keeps it keyboard-accessible. Once active
|
|
11358
|
+
* (`data-embed-active`), clicks fall through to the live iframe.
|
|
11359
|
+
*
|
|
11360
|
+
* @param event - The click event from the facade figure.
|
|
11361
|
+
* @example
|
|
11362
|
+
* ```ts
|
|
11363
|
+
* element.addEventListener("click", onFacadeClick);
|
|
11364
|
+
* ```
|
|
11365
|
+
*/
|
|
11366
|
+
function onFacadeClick(event) {
|
|
11367
|
+
const figure = event.currentTarget;
|
|
11368
|
+
if (!(figure instanceof HTMLElement)) return;
|
|
11369
|
+
if (figure.dataset.embedActive !== void 0) return;
|
|
11370
|
+
activateEmbed(figure);
|
|
11371
|
+
}
|
|
11372
|
+
/**
|
|
11373
|
+
* Lazy-embed island: facade button click → real `<iframe loading="lazy">`.
|
|
11374
|
+
* The companion of the content pipeline's `::embed` directive.
|
|
11375
|
+
*/
|
|
11376
|
+
const lazyEmbed = createComponent("lazy-embed", {
|
|
11377
|
+
/**
|
|
11378
|
+
* Bind the activation click handler when a facade mounts.
|
|
11379
|
+
*
|
|
11380
|
+
* @param ctx - The island lifecycle context.
|
|
11381
|
+
* @example
|
|
11382
|
+
* onMount(ctx);
|
|
11383
|
+
*/
|
|
11384
|
+
onMount(ctx) {
|
|
11385
|
+
ctx.el.addEventListener("click", onFacadeClick);
|
|
11386
|
+
},
|
|
11387
|
+
/**
|
|
11388
|
+
* Remove the activation click handler when the facade is destroyed.
|
|
11389
|
+
*
|
|
11390
|
+
* @param ctx - The island lifecycle context.
|
|
11391
|
+
* @example
|
|
11392
|
+
* onDestroy(ctx);
|
|
11393
|
+
*/
|
|
11394
|
+
onDestroy(ctx) {
|
|
11395
|
+
ctx.el.removeEventListener("click", onFacadeClick);
|
|
11396
|
+
}
|
|
11397
|
+
});
|
|
11398
|
+
//#endregion
|
|
11302
11399
|
//#region src/plugins/spa/index.ts
|
|
11303
11400
|
/**
|
|
11304
11401
|
* @file spa — Complex Plugin (WIRING ONLY, ≤30 lines). All logic lives in the
|
|
@@ -11573,6 +11670,230 @@ function parseFrontmatter(raw, config) {
|
|
|
11573
11670
|
};
|
|
11574
11671
|
}
|
|
11575
11672
|
//#endregion
|
|
11673
|
+
//#region src/plugins/content/pipeline/embed-facade.tsx
|
|
11674
|
+
/** CSS class on the facade's activation button. */
|
|
11675
|
+
const EMBED_BUTTON_CLASS = "lazy-embed-button";
|
|
11676
|
+
/** CSS class on the title span inside the activation button. */
|
|
11677
|
+
const EMBED_TITLE_CLASS = "lazy-embed-title";
|
|
11678
|
+
/**
|
|
11679
|
+
* Default `::embed` facade inner content: a single labelled `<button>` carrying
|
|
11680
|
+
* the embed title. The companion `lazyEmbed` island activates the embed on a
|
|
11681
|
+
* click anywhere in the facade, so the button is the keyboard-accessible
|
|
11682
|
+
* control. Provided as the default and as a composable building block for custom
|
|
11683
|
+
* facades.
|
|
11684
|
+
*
|
|
11685
|
+
* @param props - The embed facade props (only `title` is used by the default).
|
|
11686
|
+
* @returns The facade inner-content VNode.
|
|
11687
|
+
* @example
|
|
11688
|
+
* ```tsx
|
|
11689
|
+
* // Compose the default inside a richer custom facade:
|
|
11690
|
+
* const MyFacade = (p: EmbedFacadeProps) => (
|
|
11691
|
+
* <div class="poster"><img src={p.attributes.poster} alt="" /><EmbedFacadeButton {...p} /></div>
|
|
11692
|
+
* );
|
|
11693
|
+
* ```
|
|
11694
|
+
*/
|
|
11695
|
+
function EmbedFacadeButton(props) {
|
|
11696
|
+
return /* @__PURE__ */ jsx("button", {
|
|
11697
|
+
type: "button",
|
|
11698
|
+
class: EMBED_BUTTON_CLASS,
|
|
11699
|
+
"aria-label": `Load embed: ${props.title}`,
|
|
11700
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
11701
|
+
class: EMBED_TITLE_CLASS,
|
|
11702
|
+
children: props.title
|
|
11703
|
+
})
|
|
11704
|
+
});
|
|
11705
|
+
}
|
|
11706
|
+
//#endregion
|
|
11707
|
+
//#region src/plugins/content/pipeline/embed.ts
|
|
11708
|
+
/** CSS class on the `<figure>` facade wrapping each embed. */
|
|
11709
|
+
const EMBED_FIGURE_CLASS = "lazy-embed";
|
|
11710
|
+
/** `data-component` name binding the facade to the `lazyEmbed` SPA island. */
|
|
11711
|
+
const EMBED_COMPONENT_NAME = "lazy-embed";
|
|
11712
|
+
/**
|
|
11713
|
+
* Type guard for an `::embed` leaf directive.
|
|
11714
|
+
*
|
|
11715
|
+
* @param node - AST node to test.
|
|
11716
|
+
* @returns `true` when the node is an `::embed` leaf directive.
|
|
11717
|
+
* @example
|
|
11718
|
+
* ```ts
|
|
11719
|
+
* if (isEmbedDirective(node)) console.log(node.attributes?.src);
|
|
11720
|
+
* ```
|
|
11721
|
+
*/
|
|
11722
|
+
function isEmbedDirective(node) {
|
|
11723
|
+
return node.type === "leafDirective" && node.name === "embed";
|
|
11724
|
+
}
|
|
11725
|
+
/**
|
|
11726
|
+
* Escape a string for safe interpolation into a double-quoted HTML attribute.
|
|
11727
|
+
*
|
|
11728
|
+
* @param value - The raw attribute value.
|
|
11729
|
+
* @returns The escaped value.
|
|
11730
|
+
* @example
|
|
11731
|
+
* ```ts
|
|
11732
|
+
* escapeAttribute('He said "hi" & left'); // "He said "hi" & left"
|
|
11733
|
+
* ```
|
|
11734
|
+
*/
|
|
11735
|
+
function escapeAttribute(value) {
|
|
11736
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
11737
|
+
}
|
|
11738
|
+
/**
|
|
11739
|
+
* Validate an embed `src`. Three forms are embeddable: an `http(s)` URL, a
|
|
11740
|
+
* root-relative path (`/x`), or a co-located relative path (`./x`, `../x`,
|
|
11741
|
+
* `x/…`) resolved later against `/<slug>/`. Everything else — protocol-relative
|
|
11742
|
+
* (`//host`), `javascript:`, `data:`, any other scheme — is rejected.
|
|
11743
|
+
*
|
|
11744
|
+
* @param src - The raw `src` attribute value.
|
|
11745
|
+
* @returns `true` when the URL/path is embeddable.
|
|
11746
|
+
* @example
|
|
11747
|
+
* ```ts
|
|
11748
|
+
* isEmbeddableUrl("https://game.example.com/"); // true
|
|
11749
|
+
* isEmbeddableUrl("./game/index.html"); // true (co-located)
|
|
11750
|
+
* isEmbeddableUrl("javascript:alert(1)"); // false
|
|
11751
|
+
* ```
|
|
11752
|
+
*/
|
|
11753
|
+
function isEmbeddableUrl(src) {
|
|
11754
|
+
if (src === "") return false;
|
|
11755
|
+
if (src.startsWith("//")) return false;
|
|
11756
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src)) return /^https?:\/\//i.test(src);
|
|
11757
|
+
return true;
|
|
11758
|
+
}
|
|
11759
|
+
/**
|
|
11760
|
+
* Parse + validate the optional `width`/`height` directive attributes. Both
|
|
11761
|
+
* must be supplied together, each a positive integer count of pixels; the pair
|
|
11762
|
+
* is used to reserve the facade box at its true aspect ratio. Returns
|
|
11763
|
+
* `undefined` when neither is set.
|
|
11764
|
+
*
|
|
11765
|
+
* @param width - Raw `width` attribute (or undefined).
|
|
11766
|
+
* @param height - Raw `height` attribute (or undefined).
|
|
11767
|
+
* @returns The parsed dimensions, or `undefined` when both are absent.
|
|
11768
|
+
* @throws {Error} When only one of the pair is set, or a value is not a
|
|
11769
|
+
* positive integer.
|
|
11770
|
+
* @example
|
|
11771
|
+
* ```ts
|
|
11772
|
+
* parseDimensions("400", "711"); // { width: 400, height: 711 }
|
|
11773
|
+
* parseDimensions(undefined, undefined); // undefined
|
|
11774
|
+
* ```
|
|
11775
|
+
*/
|
|
11776
|
+
function parseDimensions(width, height) {
|
|
11777
|
+
const hasWidth = width !== void 0 && width !== null && width !== "";
|
|
11778
|
+
const hasHeight = height !== void 0 && height !== null && height !== "";
|
|
11779
|
+
if (!hasWidth && !hasHeight) return void 0;
|
|
11780
|
+
if (!hasWidth || !hasHeight) throw new Error("[web] content: `::embed` width and height must be set together (got only one).");
|
|
11781
|
+
if (!/^\d+$/.test(width) || !/^\d+$/.test(height) || width === "0" || height === "0") throw new Error(`[web] content: \`::embed\` width/height must be positive integers in pixels (got "${width}"×"${height}").`);
|
|
11782
|
+
return {
|
|
11783
|
+
width: Number(width),
|
|
11784
|
+
height: Number(height)
|
|
11785
|
+
};
|
|
11786
|
+
}
|
|
11787
|
+
/**
|
|
11788
|
+
* Collect the directive's raw attribute bag into a plain string record, dropping
|
|
11789
|
+
* `null`/`undefined` values (so a custom facade can read arbitrary extra options).
|
|
11790
|
+
*
|
|
11791
|
+
* @param attributes - The raw directive attributes (or undefined).
|
|
11792
|
+
* @returns A string-valued attribute record.
|
|
11793
|
+
* @example
|
|
11794
|
+
* ```ts
|
|
11795
|
+
* collectAttributes({ src: "x", poster: "/p.jpg", flag: null }); // { src: "x", poster: "/p.jpg" }
|
|
11796
|
+
* ```
|
|
11797
|
+
*/
|
|
11798
|
+
function collectAttributes(attributes) {
|
|
11799
|
+
const out = {};
|
|
11800
|
+
for (const [key, value] of Object.entries(attributes ?? {})) if (typeof value === "string") out[key] = value;
|
|
11801
|
+
return out;
|
|
11802
|
+
}
|
|
11803
|
+
/**
|
|
11804
|
+
* Build the static facade HTML for one embed: the framework-owned `<figure>`
|
|
11805
|
+
* (island hooks in data attributes; optional reserved-box `aspect-ratio`/`max-width`
|
|
11806
|
+
* inline style when dimensions are given) wrapping the facade component's inner
|
|
11807
|
+
* content, SSR'd to static markup. The wrapper carries `data-embed-src` (raw —
|
|
11808
|
+
* the provider resolves a relative src) so neither the island contract nor the
|
|
11809
|
+
* URL rewrite depend on the consumer's markup.
|
|
11810
|
+
*
|
|
11811
|
+
* @param facade - The facade component (default {@link EmbedFacadeButton}).
|
|
11812
|
+
* @param props - The facade props (`src`, `title`, optional `width`/`height`, raw `attributes`).
|
|
11813
|
+
* @param dimensions - Optional reserved-box pixel dimensions.
|
|
11814
|
+
* @returns The facade HTML string.
|
|
11815
|
+
* @example
|
|
11816
|
+
* ```ts
|
|
11817
|
+
* embedFacadeHtml(EmbedFacadeButton, { src: "https://g/", title: "G", attributes: {} });
|
|
11818
|
+
* ```
|
|
11819
|
+
*/
|
|
11820
|
+
function embedFacadeHtml(facade, props, dimensions) {
|
|
11821
|
+
return `<figure class="${EMBED_FIGURE_CLASS}" data-component="${EMBED_COMPONENT_NAME}" data-embed-src="${escapeAttribute(props.src)}" data-embed-title="${escapeAttribute(props.title)}"${dimensions ? ` data-embed-width="${dimensions.width}" data-embed-height="${dimensions.height}" style="aspect-ratio: ${dimensions.width} / ${dimensions.height}; max-width: ${dimensions.width}px;"` : ""}>${renderToString(h(facade, props))}</figure>`;
|
|
11822
|
+
}
|
|
11823
|
+
/**
|
|
11824
|
+
* Normalize the provider's `embed` config value (`boolean | options`) to a plain
|
|
11825
|
+
* {@link EmbedOptions} object for the transform factory.
|
|
11826
|
+
*
|
|
11827
|
+
* @param embed - The raw `FileSystemContentOptions.embed` value (truthy).
|
|
11828
|
+
* @returns The options object (`{}` for the bare `true` form).
|
|
11829
|
+
* @example
|
|
11830
|
+
* ```ts
|
|
11831
|
+
* normalizeEmbedOptions(true); // {}
|
|
11832
|
+
* normalizeEmbedOptions({ facade: MyFacade });
|
|
11833
|
+
* ```
|
|
11834
|
+
*/
|
|
11835
|
+
function normalizeEmbedOptions(embed) {
|
|
11836
|
+
return typeof embed === "boolean" ? {} : embed;
|
|
11837
|
+
}
|
|
11838
|
+
/**
|
|
11839
|
+
* Mdast transformer rewriting every `::embed` leaf directive to its facade
|
|
11840
|
+
* HTML node. A directive missing `src`/`title`, carrying a non-embeddable URL,
|
|
11841
|
+
* or carrying invalid `width`/`height`, fails the build with the offending
|
|
11842
|
+
* value quoted.
|
|
11843
|
+
*
|
|
11844
|
+
* @param facade - The facade component to render the inner content with.
|
|
11845
|
+
* @param tree - The mdast tree to mutate.
|
|
11846
|
+
* @throws {Error} When an `::embed` directive is missing `src` or `title`, its
|
|
11847
|
+
* `src` is not embeddable, or its dimensions are invalid.
|
|
11848
|
+
* @example
|
|
11849
|
+
* ```ts
|
|
11850
|
+
* embedTransform(EmbedFacadeButton, tree);
|
|
11851
|
+
* ```
|
|
11852
|
+
*/
|
|
11853
|
+
function embedTransform(facade, tree) {
|
|
11854
|
+
visit(tree, (node, index, parent) => {
|
|
11855
|
+
if (!isEmbedDirective(node)) return;
|
|
11856
|
+
if (parent === void 0 || index === void 0) return;
|
|
11857
|
+
const src = node.attributes?.src ?? "";
|
|
11858
|
+
const title = node.attributes?.title ?? "";
|
|
11859
|
+
if (src === "" || title === "") throw new Error("[web] content: `::embed` requires both `src` and `title` attributes, e.g. ::embed{src=\"https://…\" title=\"…\"}.");
|
|
11860
|
+
if (!isEmbeddableUrl(src)) throw new Error(`[web] content: \`::embed\` src must be an http(s) URL, a root-relative path, or a co-located relative path (got "${src}").`);
|
|
11861
|
+
const dimensions = parseDimensions(node.attributes?.width, node.attributes?.height);
|
|
11862
|
+
const html = {
|
|
11863
|
+
type: "html",
|
|
11864
|
+
value: embedFacadeHtml(facade, {
|
|
11865
|
+
src,
|
|
11866
|
+
title,
|
|
11867
|
+
...dimensions ? {
|
|
11868
|
+
width: dimensions.width,
|
|
11869
|
+
height: dimensions.height
|
|
11870
|
+
} : {},
|
|
11871
|
+
attributes: collectAttributes(node.attributes)
|
|
11872
|
+
}, dimensions)
|
|
11873
|
+
};
|
|
11874
|
+
parent.children[index] = html;
|
|
11875
|
+
});
|
|
11876
|
+
}
|
|
11877
|
+
/**
|
|
11878
|
+
* Remark transform factory: rewrites `::embed{src="…" title="…"}` leaf directives
|
|
11879
|
+
* into static click-to-activate facades (no iframe until the reader clicks — see
|
|
11880
|
+
* the file header). Opt-in via the provider's `embed` option; requires
|
|
11881
|
+
* `trustedContent: true` because the facade is raw HTML the sanitize pass would
|
|
11882
|
+
* strip. The facade's inner content is rendered by `options.facade` (a consumer
|
|
11883
|
+
* Preact component) or the built-in {@link EmbedFacadeButton}.
|
|
11884
|
+
*
|
|
11885
|
+
* @param options - Embed options (the optional `facade` component).
|
|
11886
|
+
* @returns An mdast tree transformer.
|
|
11887
|
+
* @example
|
|
11888
|
+
* ```ts
|
|
11889
|
+
* unified().use(embedPlugin, { facade: MyFacade });
|
|
11890
|
+
* ```
|
|
11891
|
+
*/
|
|
11892
|
+
function embedPlugin(options = {}) {
|
|
11893
|
+
const facade = options.facade ?? EmbedFacadeButton;
|
|
11894
|
+
return (tree) => embedTransform(facade, tree);
|
|
11895
|
+
}
|
|
11896
|
+
//#endregion
|
|
11576
11897
|
//#region src/plugins/content/pipeline/mermaid.ts
|
|
11577
11898
|
/** CSS class on the `<figure>` wrapper around each rendered diagram. */
|
|
11578
11899
|
const MERMAID_FIGURE_CLASS = "mermaid-diagram";
|
|
@@ -11856,14 +12177,17 @@ function sectionDividerPlugin() {
|
|
|
11856
12177
|
}
|
|
11857
12178
|
/**
|
|
11858
12179
|
* The hardcoded framework default remark (Markdown-AST) plugins, in order:
|
|
11859
|
-
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN mermaid
|
|
11860
|
-
* then the mdast→hast bridge (`remark-rehype` with
|
|
11861
|
-
* Pull-quote and mermaid run on the mdast before
|
|
11862
|
-
* directive carries its `hName`/`hProperties`,
|
|
11863
|
-
*
|
|
11864
|
-
*
|
|
11865
|
-
*
|
|
11866
|
-
*
|
|
12180
|
+
* parse, frontmatter, gfm, directive, pull-quote, the OPT-IN embed + mermaid
|
|
12181
|
+
* transforms, then the mdast→hast bridge (`remark-rehype` with
|
|
12182
|
+
* `allowDangerousHtml`). Pull-quote, embed and mermaid run on the mdast before
|
|
12183
|
+
* the bridge — pull-quote so the directive carries its `hName`/`hProperties`,
|
|
12184
|
+
* embed so the directive is replaced with its raw facade HTML, mermaid so the
|
|
12185
|
+
* fence is replaced with raw SVG HTML before Shiki could ever claim the code
|
|
12186
|
+
* block.
|
|
12187
|
+
*
|
|
12188
|
+
* @param config - Optional provider configuration; only the opt-in flags are
|
|
12189
|
+
* read here: `mermaid` and `embed` (each truthy value enables its transform at
|
|
12190
|
+
* a fixed mdast position).
|
|
11867
12191
|
* @returns The ordered default remark pluggables.
|
|
11868
12192
|
* @example
|
|
11869
12193
|
* ```ts
|
|
@@ -11878,6 +12202,7 @@ function defaultRemarkPlugins(config) {
|
|
|
11878
12202
|
remarkDirective,
|
|
11879
12203
|
pullQuotePlugin
|
|
11880
12204
|
];
|
|
12205
|
+
if (config?.embed) plugins.push([embedPlugin, normalizeEmbedOptions(config.embed)]);
|
|
11881
12206
|
if (config?.mermaid) plugins.push([remarkMermaidDiagrams, normalizeMermaidOptions(config.mermaid)]);
|
|
11882
12207
|
plugins.push([remarkRehype, { allowDangerousHtml: true }]);
|
|
11883
12208
|
return plugins;
|
|
@@ -12119,6 +12444,8 @@ function calculateReadingTime(text) {
|
|
|
12119
12444
|
*/
|
|
12120
12445
|
/** Matches an `<img>` `src` that points at the co-located `images/` dir (relative or root-relative). */
|
|
12121
12446
|
const RELATIVE_IMAGE_SRC = /(<img\b[^>]*?\bsrc=")(?:\.?\/)?images\//g;
|
|
12447
|
+
/** Matches the `data-embed-src` of an `::embed` facade (value captured for path resolution). */
|
|
12448
|
+
const EMBED_SRC_ATTR = /(\bdata-embed-src=")([^"]*)(")/g;
|
|
12122
12449
|
/**
|
|
12123
12450
|
* Build a canonical article URL for a locale + slug.
|
|
12124
12451
|
*
|
|
@@ -12149,6 +12476,56 @@ function rewriteImageUrls(html, slug) {
|
|
|
12149
12476
|
return html.replaceAll(RELATIVE_IMAGE_SRC, `$1/${slug}/images/`);
|
|
12150
12477
|
}
|
|
12151
12478
|
/**
|
|
12479
|
+
* Resolve an `::embed` `src` to the URL the iframe should load. Absolute targets
|
|
12480
|
+
* (`http(s)://…`, root-relative `/…`) pass through unchanged; a co-located
|
|
12481
|
+
* relative path (`./game/index.html`, `../x`, `game/x`) is resolved against the
|
|
12482
|
+
* article base `/<slug>/` into the single shared absolute path the content-assets
|
|
12483
|
+
* build phase copies the bundle to — so it loads identically from every locale
|
|
12484
|
+
* page (mirroring how co-located images resolve). Any `?query`/`#hash` is
|
|
12485
|
+
* preserved verbatim.
|
|
12486
|
+
*
|
|
12487
|
+
* @param value - The raw `data-embed-src` value.
|
|
12488
|
+
* @param slug - Article directory name.
|
|
12489
|
+
* @returns The resolved embed URL.
|
|
12490
|
+
* @example
|
|
12491
|
+
* ```ts
|
|
12492
|
+
* resolveEmbedSource("./game/index.html", "post"); // "/post/game/index.html"
|
|
12493
|
+
* resolveEmbedSource("https://x.dev/", "post"); // "https://x.dev/"
|
|
12494
|
+
* ```
|
|
12495
|
+
*/
|
|
12496
|
+
function resolveEmbedSource(value, slug) {
|
|
12497
|
+
if (/^https?:\/\//i.test(value) || value.startsWith("/")) return value;
|
|
12498
|
+
const tailIndex = value.search(/[?#]/);
|
|
12499
|
+
const rawPath = tailIndex === -1 ? value : value.slice(0, tailIndex);
|
|
12500
|
+
const tail = tailIndex === -1 ? "" : value.slice(tailIndex);
|
|
12501
|
+
const out = [];
|
|
12502
|
+
for (const segment of `${slug}/${rawPath}`.split("/")) {
|
|
12503
|
+
if (segment === "" || segment === ".") continue;
|
|
12504
|
+
if (segment === "..") {
|
|
12505
|
+
out.pop();
|
|
12506
|
+
continue;
|
|
12507
|
+
}
|
|
12508
|
+
out.push(segment);
|
|
12509
|
+
}
|
|
12510
|
+
const trailingSlash = rawPath === "" || rawPath.endsWith("/") ? "/" : "";
|
|
12511
|
+
return `/${out.join("/")}${trailingSlash}${tail}`;
|
|
12512
|
+
}
|
|
12513
|
+
/**
|
|
12514
|
+
* Rewrite every `::embed` facade's relative `data-embed-src` to its shared
|
|
12515
|
+
* absolute `/<slug>/…` path (no-op for already-absolute targets).
|
|
12516
|
+
*
|
|
12517
|
+
* @param html - The rendered article HTML.
|
|
12518
|
+
* @param slug - Article directory name.
|
|
12519
|
+
* @returns The HTML with embed `src`s resolved.
|
|
12520
|
+
* @example
|
|
12521
|
+
* ```ts
|
|
12522
|
+
* rewriteEmbedUrls('<figure data-embed-src="./g/">', "post"); // '… data-embed-src="/post/g/"'
|
|
12523
|
+
* ```
|
|
12524
|
+
*/
|
|
12525
|
+
function rewriteEmbedUrls(html, slug) {
|
|
12526
|
+
return html.replaceAll(EMBED_SRC_ATTR, (_match, prefix, value, suffix) => `${prefix}${resolveEmbedSource(value, slug)}${suffix}`);
|
|
12527
|
+
}
|
|
12528
|
+
/**
|
|
12152
12529
|
* Discover slug-like subdirectories of the content root (direct children not
|
|
12153
12530
|
* starting with `.` or `_`), sorted alphabetically for deterministic ordering.
|
|
12154
12531
|
*
|
|
@@ -12227,7 +12604,7 @@ function fileSystemContent(options) {
|
|
|
12227
12604
|
state.dirtyPaths.delete(filePath);
|
|
12228
12605
|
const { frontmatter, body } = parseFrontmatter(raw, options);
|
|
12229
12606
|
const processor = ensureProcessor(state, options);
|
|
12230
|
-
const html = rewriteImageUrls(String(await processor.process(body)), slug);
|
|
12607
|
+
const html = rewriteEmbedUrls(rewriteImageUrls(String(await processor.process(body)), slug), slug);
|
|
12231
12608
|
const { readingTime, wordCount } = calculateReadingTime(body);
|
|
12232
12609
|
return {
|
|
12233
12610
|
frontmatter,
|
|
@@ -12351,4 +12728,4 @@ const createApp = core.createApp;
|
|
|
12351
12728
|
*/
|
|
12352
12729
|
const createPlugin = core.createPlugin;
|
|
12353
12730
|
//#endregion
|
|
12354
|
-
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, types_exports$5 as Env, types_exports$6 as Head, types_exports$7 as Log, types_exports$8 as Router, types_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|
|
12731
|
+
export { types_exports as Build, types_exports$1 as Cli, types_exports$2 as Content, types_exports$3 as Data, types_exports$4 as Deploy, EmbedFacadeButton, types_exports$5 as Env, types_exports$6 as Head, types_exports$7 as Log, types_exports$8 as Router, types_exports$9 as Spa, browserEnv, buildArticleHead, buildPlugin, canonical, cliPlugin, cloudflareBindings, contentPlugin, createApp, createComponent, createPlugin, createUrls, dataPlugin, defineRoutes, deployPlugin, dotenv, envPlugin, feedLink, fileSystemContent, headPlugin, hreflang, i18nPlugin, jsonLd, lazyEmbed, logPlugin, meta, og, processEnv, route, routerPlugin, sitePlugin, spaPlugin, twitter };
|