@junejs/server 1.0.0-dev.25 → 1.0.0-dev.27

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/pipeline.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { iconLetter } from "./icon-letter.js";
1
2
  import { SEGMENT_HEADER, TITLE_HEADER, encodeTitle, negotiate } from "./negotiate.js";
2
3
  import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
3
4
  import { LoaderDataContext } from "@junejs/core/route";
@@ -34,8 +35,9 @@ function StreamedView({ loadPromise, def, ctx }) {
34
35
  return provideLoaderData(data, def.view ? def.view(data, ctx) : null);
35
36
  }
36
37
  function letterFavicon(siteName) {
37
- return text(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#1d1d1f"/><text x="32" y="32" dy=".36em" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif" font-size="34" font-weight="600" fill="#fbfbf8">${((siteName ?? "").trim().charAt(0) || "•").toUpperCase().replace(/[<>&"']/g, "") || "•"}</text></svg>\n`, "image/svg+xml", { headers: { "cache-control": "public, max-age=86400" } });
38
+ return text(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#1d1d1f"/><text x="32" y="32" dy=".36em" text-anchor="middle" font-family="ui-sans-serif, system-ui, sans-serif" font-size="34" font-weight="600" fill="#fbfbf8">${(iconLetter(siteName) || "•").replace(/[<>&"']/g, "") || "•"}</text></svg>\n`, "image/svg+xml", { headers: { "cache-control": "public, max-age=86400" } });
38
39
  }
40
+ const routePathOf = /* @__PURE__ */ new WeakMap();
39
41
  function createPipeline(cfg) {
40
42
  const { docConfig, agent } = cfg;
41
43
  const NotFound = cfg.notFoundComponent ?? DefaultNotFound;
@@ -62,7 +64,15 @@ function createPipeline(cfg) {
62
64
  protocol: ctx.url.protocol.replace(":", "")
63
65
  });
64
66
  }
65
- async function renderDocument(node, metadata, status, chain, locale, boundaryKey, alternates) {
67
+ function pageProps(ctx) {
68
+ const host = ctx.url.hostname.toLowerCase();
69
+ return {
70
+ pageUrl: ctx.url.href,
71
+ isHome: routePathOf.get(ctx) === "/",
72
+ onLocaleDomain: cfg.i18n ? Object.values(cfg.i18n.locales).some((l) => l.domain?.toLowerCase() === host) : false
73
+ };
74
+ }
75
+ async function renderDocument(node, metadata, status, chain, locale, boundaryKey, alternates, page) {
66
76
  const wrapped = chain.reduceRight((acc, L) => React.createElement(L, null, acc), node);
67
77
  const stream = await renderToReadableStream(React.createElement(Document, {
68
78
  config: docConfigForRender(),
@@ -70,6 +80,7 @@ function createPipeline(cfg) {
70
80
  children: wrapped,
71
81
  shellKey: boundaryKey,
72
82
  alternates,
83
+ ...page,
73
84
  ...langDir(locale)
74
85
  }));
75
86
  await stream.allReady;
@@ -115,6 +126,7 @@ function createPipeline(cfg) {
115
126
  children: wrapped,
116
127
  shellKey: boundaryKey,
117
128
  alternates: alternatesFor(ctx),
129
+ ...pageProps(ctx),
118
130
  ...langDir(ctx.locale)
119
131
  }), { onError: (e) => console.error("[june] streaming render error:", e) });
120
132
  return new Response(stream, {
@@ -155,7 +167,7 @@ function createPipeline(cfg) {
155
167
  const node = provideLoaderData(data, def.view ? def.view(data, ctx) : null);
156
168
  const meta = resolveMeta(def, data, ctx);
157
169
  if (target === "fragment") return renderFragment(node, meta, chain, boundaryIndex, boundaryKey);
158
- return renderDocument(node, meta, 200, chain, ctx.locale, boundaryKey, alternatesFor(ctx));
170
+ return renderDocument(node, meta, 200, chain, ctx.locale, boundaryKey, alternatesFor(ctx), pageProps(ctx));
159
171
  }
160
172
  async function discovery(url) {
161
173
  switch (url.pathname) {
@@ -219,6 +231,7 @@ function createPipeline(cfg) {
219
231
  locale,
220
232
  speculative
221
233
  };
234
+ routePathOf.set(ctx, pathname);
222
235
  if ("handler" in resolved) return resolved.handler(request, ctx);
223
236
  if (target === "view" && resolved.loading && typeof resolved.def.metadata !== "function") return renderStreamingDocument(resolved, Promise.resolve(resolved.def.load ? resolved.def.load(ctx) : void 0), ctx);
224
237
  let data;
@@ -21,24 +21,27 @@ async function streamToString(stream) {
21
21
  out += dec.decode();
22
22
  return out;
23
23
  }
24
- async function renderDocument(path) {
24
+ async function renderDocument(url) {
25
+ const path = url.pathname;
25
26
  const flight = await (await import(new URL("./server.js", import.meta.url).href)).renderFlight(path);
26
27
  if (flight === null) return null;
27
28
  const flightStream = new Response(flight).body;
29
+ const tree = createFromReadableStream(flightStream, { serverConsumerManifest: {
30
+ moduleMap: MODULE_MAP,
31
+ serverModuleMap: null,
32
+ moduleLoading: null
33
+ } });
28
34
  const html = await renderToReadableStream(/* @__PURE__ */ jsx(Document, {
29
35
  config: DOC_CONFIG,
30
- children: /* @__PURE__ */ jsx(Root, { tree: createFromReadableStream(flightStream, { serverConsumerManifest: {
31
- moduleMap: MODULE_MAP,
32
- serverModuleMap: null,
33
- moduleLoading: null
34
- } }) })
36
+ pageUrl: url.href,
37
+ isHome: path === "/",
38
+ children: /* @__PURE__ */ jsx(Root, { tree })
35
39
  }));
36
40
  if (typeof html.allReady?.then === "function") await html.allReady;
37
41
  return "<!DOCTYPE html>" + await streamToString(html);
38
42
  }
39
43
  var rsc_worker_default = { async fetch(request) {
40
- const path = new URL(request.url).pathname;
41
- const body = await renderDocument(path);
44
+ const body = await renderDocument(new URL(request.url));
42
45
  if (body === null) return new Response("Not Found", { status: 404 });
43
46
  return new Response(body, { headers: { "content-type": "text/html; charset=utf-8" } });
44
47
  } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "1.0.0-dev.25",
3
+ "version": "1.0.0-dev.27",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -123,7 +123,8 @@
123
123
  },
124
124
  "files": [
125
125
  "src",
126
- "dist"
126
+ "dist",
127
+ "assets"
127
128
  ],
128
129
  "scripts": {
129
130
  "test": "bun test",
@@ -133,12 +134,13 @@
133
134
  "dependencies": {
134
135
  "@junejs/db": ">=0.1.0-dev.2",
135
136
  "@momiji-rs/sparkdown": "^0.0.4",
137
+ "@resvg/resvg-wasm": "^2.6.2",
136
138
  "lightningcss": "^1.27.0",
137
139
  "oxc-parser": "^0.137.0",
138
140
  "rolldown": "^1.1.0"
139
141
  },
140
142
  "peerDependencies": {
141
- "@junejs/core": ">=0.2.0-dev.44",
143
+ "@junejs/core": ">=0.2.0-dev.47",
142
144
  "react": "^19.2.0",
143
145
  "react-dom": "^19.2.0",
144
146
  "react-server-dom-webpack": "19.2.7",
package/src/app.ts CHANGED
@@ -18,6 +18,7 @@ import type { ComponentType } from "react";
18
18
  import { routeFromModule } from "@junejs/core/route";
19
19
  import {
20
20
  resolveAgent,
21
+ deployOrigin,
21
22
  resolveClientRouter,
22
23
  resolveSpeculationRules,
23
24
  type JuneConfig,
@@ -42,6 +43,15 @@ import { generateIslandRegistry } from "./island-registry";
42
43
  import { findGlobalCss, globalCssUsesTailwind, processCssCached, STYLES_URL } from "./css";
43
44
  import { buildModuleCss, registerCssModules, MODULE_STYLES_URL, type ModuleMaps } from "./css-modules";
44
45
  import { contentTypeFor, RESERVED_PREFIX, safeRelativePath } from "./static-files";
46
+ import {
47
+ GENERATED_ICONS,
48
+ googleFontFetcher,
49
+ publicFileCheck,
50
+ renderIcons,
51
+ resolveIcons,
52
+ type GeneratedIconFile,
53
+ type GeneratedIcons,
54
+ } from "./favicon";
45
55
 
46
56
  export type CreateAppOptions = {
47
57
  appDir: string;
@@ -132,6 +142,19 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
132
142
  } catch {
133
143
  /* no public/ dir → nothing to serve */
134
144
  }
145
+ // Default PNG icons: linked by the document, rendered once on first request
146
+ // (a failed render is retried next time rather than memoized).
147
+ const iconPlan = resolveIcons(config.site ?? {}, publicFileCheck(publicDir));
148
+ let iconsPromise: Promise<GeneratedIcons> | undefined;
149
+ const getIcons = () =>
150
+ (iconsPromise ??= renderIcons({
151
+ site: config.site ?? {},
152
+ fetchFont: googleFontFetcher(join(dirname(appDir), "node_modules", ".cache", "june", "fonts")),
153
+ warn: (msg) => console.warn(msg),
154
+ }).catch((err) => {
155
+ iconsPromise = undefined;
156
+ throw err;
157
+ }));
135
158
  const agent = resolveAgent(config.agent);
136
159
  const speculation = config.speculation;
137
160
  // app/_client.* present → the dev document loads /client.js and we serve it
@@ -143,6 +166,7 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
143
166
  const cssEntry = findGlobalCss(appDir);
144
167
  const docConfig: DocumentConfig = {
145
168
  site: config.site ?? {},
169
+ deployOrigin: deployOrigin(config),
146
170
  speculationRules: resolveSpeculationRules(speculation ?? undefined),
147
171
  speculationDelivery: speculation ? speculation.delivery ?? "inline" : "inline",
148
172
  viewTransitions: config.viewTransitions ?? true,
@@ -151,6 +175,7 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
151
175
  clientRouter: resolveClientRouter(config.clientRouter),
152
176
  clientScript: clientEntry ? CLIENT_SCRIPT_URL : null,
153
177
  styles: cssEntry ? STYLES_URL : null,
178
+ icons: iconPlan.icons,
154
179
  };
155
180
 
156
181
  // CSS Modules: glob + transform app/**/*.module.css ONCE (memoized) → the maps
@@ -401,6 +426,20 @@ export function createApp({ appDir: appDirInput, config = {} }: CreateAppOptions
401
426
  }
402
427
  }
403
428
  }
429
+ // June's default PNG icons (the app has none of its own): rendered on first
430
+ // request with the same code `june build` writes them with.
431
+ if (iconPlan.generate && (request.method === "GET" || request.method === "HEAD")) {
432
+ const name = new URL(request.url).pathname.slice(1);
433
+ if ((GENERATED_ICONS as readonly string[]).includes(name)) {
434
+ return getIcons().then(
435
+ (icons) =>
436
+ new Response(request.method === "HEAD" ? null : icons[name as GeneratedIconFile], {
437
+ headers: { "content-type": contentTypeFor(name), "cache-control": "public, max-age=0, must-revalidate" },
438
+ }),
439
+ () => toPipeline(),
440
+ );
441
+ }
442
+ }
404
443
  return toPipeline();
405
444
  },
406
445
  async warmup() {
package/src/build.ts CHANGED
@@ -27,12 +27,14 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
27
27
 
28
28
  import { loadJuneConfig } from "./config-loader";
29
29
  import { buildLinkHeader } from "@junejs/core/discovery";
30
+ import { PRERENDER_ORIGIN } from "@junejs/core/document";
30
31
  import { localeHref } from "@junejs/core/i18n";
31
32
  import type { BrandedRoute } from "@junejs/core/route";
32
33
  import type { ResourcePlan } from "./adapter";
33
34
  import { generateAgentModule } from "./agent-compile";
34
35
  import { freezeConfig, resolveDeployAdapter, workerName } from "./config-freeze";
35
36
  import { generateContent } from "./content-freeze";
37
+ import { GENERATED_ICONS, googleFontFetcher, publicFileCheck, renderIcons, resolveIcons } from "./favicon";
36
38
  import { createWorker } from "./worker";
37
39
  import { buildManifest, importLayout, type ImportedLayout } from "./manifest";
38
40
  import { findMiddlewareFile } from "./router";
@@ -542,6 +544,25 @@ ${doClass}`;
542
544
  if (publicFiles.length) hasAssets = true;
543
545
  }
544
546
 
547
+ // ---- default PNG icons: apple-touch-icon.png, icon.png, favicon.ico -----
548
+ // Only when the app brings no icon of its own (the frozen document already
549
+ // links them — same resolveIcons call). Listed with publicFiles so adapters
550
+ // with a separate static tier (vercel) place them like any public/ file.
551
+ const site = frozen.document.site;
552
+ if (resolveIcons(site, publicFileCheck(publicDir)).generate) {
553
+ const icons = await renderIcons({
554
+ site,
555
+ fetchFont: googleFontFetcher(join(appRoot, "node_modules", ".cache", "june", "fonts")),
556
+ warn: (msg) => console.warn(msg),
557
+ });
558
+ await mkdir(assetsDir, { recursive: true }); // an app with no public/ has none yet
559
+ for (const file of GENERATED_ICONS) {
560
+ await writeFile(join(assetsDir, file), icons[file]);
561
+ publicFiles.push(file);
562
+ }
563
+ hasAssets = true;
564
+ }
565
+
545
566
  // static() target: prerender EVERY route (not just opted-in ones) + enumerate
546
567
  // dynamic routes via their staticPaths, and write <stem>/index.html so clean URLs
547
568
  // resolve on a dumb file host with no rewrite server. Other targets keep the
@@ -571,7 +592,7 @@ ${doClass}`;
571
592
  if (def.md !== false) targets.push([mdReq, mdFile]); // .md/.json stay flat (exact-path negotiation)
572
593
  if (typeof def.json === "function") targets.push([jsonReq, jsonFile]);
573
594
  for (const [rp, file] of targets) {
574
- const res = await worker.fetch(new Request(`https://prerender.june${rp}`));
595
+ const res = await worker.fetch(new Request(`${PRERENDER_ORIGIN}${rp}`));
575
596
  if (!res.ok) throw new Error(`prerender ${rp} → ${res.status}`);
576
597
  const dest = join(assetsDir, file);
577
598
  await mkdir(dirname(dest), { recursive: true });
@@ -614,14 +635,14 @@ ${doClass}`;
614
635
  ["/sitemap.xml", "sitemap.xml"],
615
636
  ];
616
637
  for (const [reqPath, file] of extra) {
617
- const res = await worker.fetch(new Request(`https://prerender.june${reqPath}`));
638
+ const res = await worker.fetch(new Request(`${PRERENDER_ORIGIN}${reqPath}`));
618
639
  if (!res.ok) continue;
619
640
  await writeFile(join(assetsDir, file), Buffer.from(await res.arrayBuffer()));
620
641
  hasAssets = true;
621
642
  }
622
643
  // 404.html — GitHub Pages serves it for any unmatched URL. A deliberately-missing
623
644
  // path renders June's not-found HTML (body is written regardless of the 404 status).
624
- const nf = await worker.fetch(new Request("https://prerender.june/__june_not_found__"));
645
+ const nf = await worker.fetch(new Request(`${PRERENDER_ORIGIN}/__june_not_found__`));
625
646
  await writeFile(join(assetsDir, "404.html"), Buffer.from(await nf.arrayBuffer()));
626
647
  hasAssets = true;
627
648
  }
@@ -6,12 +6,13 @@
6
6
  import { existsSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
 
9
- import { resolveAgent, resolveClientRouter, resolveSpeculationRules } from "@junejs/core/config";
9
+ import { deployOrigin, resolveAgent, resolveClientRouter, resolveSpeculationRules } from "@junejs/core/config";
10
10
  import type { JuneConfig } from "@junejs/core/config";
11
11
  import type { DocumentConfig } from "@junejs/core/document";
12
12
  import { workers, vercel, deno, staticSite, type JuneAdapter } from "./adapter";
13
13
  import { findClientEntry, CLIENT_SCRIPT_URL } from "./client-bundle";
14
14
  import { loadJuneConfig } from "./config-loader";
15
+ import { publicFileCheck, resolveIcons } from "./favicon";
15
16
  import { findGlobalCss, globalCssUsesTailwind, STYLES_URL } from "./css";
16
17
  import type { WorkerManifest } from "./worker";
17
18
 
@@ -56,6 +57,7 @@ export async function freezeConfig(appRoot: string): Promise<{
56
57
  return {
57
58
  document: {
58
59
  site: cfg.site ?? {},
60
+ deployOrigin: deployOrigin(cfg),
59
61
  speculationRules: resolveSpeculationRules(cfg.speculation ?? undefined),
60
62
  speculationDelivery: "inline",
61
63
  viewTransitions: cfg.viewTransitions ?? true,
@@ -68,6 +70,9 @@ export async function freezeConfig(appRoot: string): Promise<{
68
70
  // asset URLs; the prerender path re-freezes through buildManifest and gets the
69
71
  // same value, keeping static pages' asset links correct under the subpath.
70
72
  basePath: normalizeBase(cfg.basePath),
73
+ // PNG icon links: June's generated set unless public/ or site.icon brings one
74
+ // (juneBuild writes the files; resolveIcons makes the same call there).
75
+ icons: resolveIcons(cfg.site ?? {}, publicFileCheck(join(appRoot, "public"))).icons,
71
76
  },
72
77
  agent: resolveAgent(cfg.agent),
73
78
  // Pass i18n through as-is: the in-process buildManifest keeps a resolveLocale
package/src/content.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { toHtmlSync, initSync } from "@momiji-rs/sparkdown/gfm";
15
+ import { createSlugger } from "@junejs/core/slug";
15
16
 
16
17
  import type { ContentSource } from "@junejs/core/config";
17
18
 
@@ -29,6 +30,48 @@ const ensureWasm = () => {
29
30
  }
30
31
  };
31
32
 
33
+ export type Heading = { depth: number; text: string; id: string };
34
+
35
+ // &-entities the renderer emits in heading text, decoded so the text (and so the
36
+ // slug) is what a reader sees: "Q&amp;A" slugs as "Q&A" → "qa", as on GitHub.
37
+ const NAMED: Record<string, string> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
38
+ const decodeEntities = (s: string) =>
39
+ s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e: string) =>
40
+ e[0] === "#"
41
+ ? String.fromCodePoint(e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : Number(e.slice(1)))
42
+ : (NAMED[e.toLowerCase()] ?? m),
43
+ );
44
+
45
+ // Give every heading in rendered HTML a GitHub-compatible id — one slugger per
46
+ // document, in order, so repeats de-duplicate exactly as GitHub's do — and collect
47
+ // them. Markdown headings render bare (`<h2>…</h2>`) and get only an id. Raw HTML an
48
+ // author wrote is preserved by CommonMark, so a heading may arrive with attributes:
49
+ // its attributes are kept, an authored id is REUSED (the author's explicit anchor),
50
+ // and a generated id never repeats any id already in the document — authored ones
51
+ // included — so every heading's anchor is unique.
52
+ const ID_ATTR = /\sid\s*=\s*(?:"([^"]*)"|'([^']*)')/i;
53
+ export function headingIds(html: string): { html: string; headings: Heading[] } {
54
+ const slug = createSlugger();
55
+ const taken = new Set<string>();
56
+ for (const m of html.matchAll(new RegExp(ID_ATTR.source, "gi"))) taken.add((m[1] ?? m[2])!);
57
+ const headings: Heading[] = [];
58
+ const out = html.replace(/<h([1-6])(\s[^>]*)?>([\s\S]*?)<\/h\1>/gi, (_m, depth: string, attrs: string | undefined, inner: string) => {
59
+ const text = decodeEntities(inner.replace(/<[^>]*>/g, "")).trim();
60
+ const own = attrs ? ID_ATTR.exec(attrs) : null;
61
+ const authored = own ? (own[1] ?? own[2]) : undefined;
62
+ let id = authored;
63
+ if (!id) {
64
+ do id = slug(text);
65
+ while (taken.has(id)); // the slugger suffixes each repeat: -1, -2, …
66
+ taken.add(id);
67
+ }
68
+ headings.push({ depth: Number(depth), text, id });
69
+ // an authored-but-empty id="" is no anchor: drop it, so the heading carries exactly one id
70
+ return authored ? _m : `<h${depth}${(attrs ?? "").replace(ID_ATTR, "")} id="${id}">${inner}</h${depth}>`;
71
+ });
72
+ return { html: out, headings };
73
+ }
74
+
32
75
  export type ContentEntry = {
33
76
  slug: string;
34
77
  file: string;
@@ -38,8 +81,13 @@ export type ContentEntry = {
38
81
  body: string;
39
82
  /** The authored file, verbatim — the agent-facing .md projection. */
40
83
  original: string;
41
- /** The body rendered to HTML (CommonMark + GFM, via @momiji-rs/sparkdown/gfm wasm). */
84
+ /** The body rendered to HTML (CommonMark + GFM, via @momiji-rs/sparkdown/gfm wasm).
85
+ * Every heading carries a GitHub-compatible `id` (see @junejs/core/slug) and nothing
86
+ * else — no injected links or classes; how an anchor looks is the app's call. */
42
87
  html: string;
88
+ /** The headings of `html`, in document order: their depth (1–6), plain text, and the
89
+ * `id` each carries in `html` — for a table of contents, or a structured projection. */
90
+ headings: Heading[];
43
91
  /** The locale this entry was authored in — set for files under a `<locale>/`
44
92
  * subdir, undefined for the flat (default-locale) files. */
45
93
  locale?: string;
@@ -96,13 +144,15 @@ function loadEntry(file: string, slug: string, locale?: string): ContentEntry {
96
144
  if (h1?.[1]) data.title = h1[1];
97
145
  }
98
146
  ensureWasm(); // sync, idempotent — instantiate the renderer wasm on first use only
147
+ const { html, headings } = headingIds(toHtmlSync(body));
99
148
  const entry: ContentEntry = {
100
149
  slug,
101
150
  file,
102
151
  data,
103
152
  body,
104
153
  original,
105
- html: toHtmlSync(body),
154
+ html,
155
+ headings,
106
156
  ...(locale ? { locale } : {}),
107
157
  };
108
158
  memo.set(key, { mtime, entry });
@@ -287,6 +337,7 @@ export function generateContentModule(
287
337
  body: e.body,
288
338
  original: e.original,
289
339
  html: e.html,
340
+ headings: e.headings,
290
341
  ...(e.locale ? { locale: e.locale } : {}),
291
342
  });
292
343
  // Default scan: each `content/<name>/` is a collection. Guarded — a sources-only app
@@ -340,8 +391,8 @@ export function generateContentModule(
340
391
  names.push(name);
341
392
  }
342
393
  const entryType = anyLocale
343
- ? "export type ContentEntry = { slug: string; data: Record<string, string | string[]>; body: string; original: string; html: string; locale?: string };\n"
344
- : "export type ContentEntry = { slug: string; data: Record<string, string | string[]>; body: string; original: string; html: string };\n";
394
+ ? "export type ContentEntry = { slug: string; data: Record<string, string | string[]>; body: string; original: string; html: string; headings: Array<{ depth: number; text: string; id: string }>; locale?: string };\n"
395
+ : "export type ContentEntry = { slug: string; data: Record<string, string | string[]>; body: string; original: string; html: string; headings: Array<{ depth: number; text: string; id: string }> };\n";
345
396
  const code =
346
397
  "// AUTO-GENERATED by `june build` — edit content/**/*.md, not this file.\n" + entryType + body;
347
398
  return { code, names };