@lupinum/ginko 0.4.0 → 0.6.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +2 -2
  3. package/dist/basics.d.mts +492 -0
  4. package/dist/basics.d.ts +492 -0
  5. package/dist/basics.mjs +416 -0
  6. package/dist/cli/index.mjs +59 -56
  7. package/dist/index.d.mts +5 -4
  8. package/dist/index.d.ts +5 -4
  9. package/dist/index.mjs +3 -3
  10. package/dist/internal/search.mjs +6 -3
  11. package/dist/internal.d.mts +387 -41
  12. package/dist/internal.d.ts +387 -41
  13. package/dist/internal.mjs +267 -101
  14. package/dist/nuxt/module.d.mts +2 -2
  15. package/dist/nuxt/module.d.ts +2 -2
  16. package/dist/nuxt/module.mjs +108 -15
  17. package/dist/nuxt/runtime/GinkoImage.d.mts +1 -1
  18. package/dist/nuxt/runtime/GinkoMarkdown.mjs +2 -2
  19. package/dist/nuxt/runtime/composables.d.mts +4 -3
  20. package/dist/nuxt/runtime/composables.mjs +19 -15
  21. package/dist/nuxt/runtime/fetch.d.mts +3 -1
  22. package/dist/nuxt/runtime/fetch.mjs +12 -6
  23. package/dist/nuxt/runtime/markdown.d.mts +3 -0
  24. package/dist/nuxt/runtime/markdown.mjs +15 -3
  25. package/dist/nuxt/runtime/plugin.mjs +6 -35
  26. package/dist/nuxt/runtime/preview-listener.d.mts +13 -0
  27. package/dist/nuxt/runtime/preview-listener.mjs +37 -0
  28. package/dist/nuxt/runtime/preview.vue +3 -1
  29. package/dist/nuxt/runtime/schema.d.ts +6 -0
  30. package/dist/nuxt/runtime/server/bundles.d.ts +4 -0
  31. package/dist/nuxt/runtime/server/files-md.d.ts +2 -0
  32. package/dist/nuxt/runtime/server/files.d.ts +2 -0
  33. package/dist/nuxt/runtime/server/headers.d.ts +2 -0
  34. package/dist/nuxt/runtime/server/items.d.mts +12 -0
  35. package/dist/nuxt/runtime/server/items.d.ts +12 -0
  36. package/dist/nuxt/runtime/server/items.mjs +17 -0
  37. package/dist/nuxt/runtime/server/llms-full.d.ts +2 -0
  38. package/dist/nuxt/runtime/server/llms.d.ts +2 -0
  39. package/dist/nuxt/runtime/server/markdown-trees.d.ts +2 -0
  40. package/dist/nuxt/runtime/server/markdown.d.ts +2 -0
  41. package/dist/nuxt/runtime/server/markdown.mjs +1 -1
  42. package/dist/nuxt/runtime/server/platform-md.d.mts +3 -0
  43. package/dist/nuxt/runtime/server/platform-md.d.ts +3 -0
  44. package/dist/nuxt/runtime/server/platform-md.mjs +71 -0
  45. package/dist/nuxt/runtime/server/redirects.d.ts +2 -0
  46. package/dist/nuxt/runtime/server/robots.d.ts +2 -0
  47. package/dist/nuxt/runtime/server/search.d.ts +2 -0
  48. package/dist/nuxt/runtime/server/site-config.d.ts +14 -0
  49. package/dist/nuxt/runtime/server/sitemap-source.d.ts +2 -0
  50. package/dist/nuxt/runtime/server/sitemap-xml.d.ts +24 -0
  51. package/dist/nuxt/runtime/server/sitemap.d.ts +2 -0
  52. package/dist/nuxt/runtime/shared.d.mts +10 -3
  53. package/dist/nuxt/runtime/shared.mjs +2 -2
  54. package/dist/shared/{ginko.BBkZW2nW.d.ts → ginko.6cI8KXZP.d.mts} +2 -2
  55. package/dist/shared/{ginko.CrNRBkS5.mjs → ginko.B1-DoOea.mjs} +253 -309
  56. package/dist/shared/ginko.BNVC-g9n.mjs +10389 -0
  57. package/dist/shared/{ginko.Bwan3BB-.d.mts → ginko.B_pACpVU.d.ts} +2 -2
  58. package/dist/shared/ginko.BaFweQcB.d.mts +91 -0
  59. package/dist/shared/ginko.BiDXgjLy.d.ts +91 -0
  60. package/dist/shared/ginko.C-oz8wwI.mjs +95 -0
  61. package/dist/shared/{ginko.BDF3bW0C.d.ts → ginko.CLzVaDi4.d.ts} +56 -5
  62. package/dist/shared/ginko.C_nq6vk7.d.mts +1622 -0
  63. package/dist/shared/ginko.CjWB8N7f.d.ts +128 -0
  64. package/dist/shared/ginko.D1PeQ3ds.mjs +36 -0
  65. package/dist/shared/ginko.D2oht0Kx.d.ts +1622 -0
  66. package/dist/shared/ginko.DCHGkQ7P.mjs +116 -0
  67. package/dist/shared/ginko.DIl7l-75.mjs +26 -0
  68. package/dist/shared/{ginko.akHewCGt.d.mts → ginko.DkIc0ZZk.d.mts} +56 -5
  69. package/dist/shared/{ginko.D-fNojn9.d.mts → ginko.DrkUeYFP.d.mts} +824 -10
  70. package/dist/shared/{ginko.D-fNojn9.d.ts → ginko.DrkUeYFP.d.ts} +824 -10
  71. package/dist/shared/ginko.WAfoMhv1.mjs +601 -0
  72. package/dist/shared/ginko.o1yIKlgj.d.mts +128 -0
  73. package/dist/shared/{ginko.Dht0xxE5.mjs → ginko.raCzOPYC.mjs} +1 -2
  74. package/dist/shared/{ginko.C90fPpMP.mjs → ginko.t-gbwzxh.mjs} +128 -101
  75. package/dist/shared/ginko.yKMIjJ68.mjs +84 -0
  76. package/dist/vue.d.mts +5 -4
  77. package/dist/vue.d.ts +5 -4
  78. package/dist/vue.mjs +1 -1
  79. package/i18n/ar.json +212 -0
  80. package/i18n/de.json +9 -1
  81. package/i18n/en.json +9 -1
  82. package/i18n/es.json +12 -4
  83. package/i18n/fr.json +212 -0
  84. package/i18n/it.json +212 -0
  85. package/package.json +7 -3
  86. package/dist/shared/ginko.BTUfZCfa.d.mts +0 -172
  87. package/dist/shared/ginko.C-QjDn3O.mjs +0 -4944
  88. package/dist/shared/ginko.CQvxhyez.mjs +0 -40
  89. package/dist/shared/ginko.Dc0oNcDv.d.ts +0 -172
  90. package/dist/shared/ginko.Mn30dmlm.mjs +0 -20
@@ -1,11 +1,14 @@
1
- import { hasNuxtModule, defineNuxtModule, createResolver, tryResolveModule, installModule, addServerTemplate, addServerHandler, addComponentsDir, addTemplate, addTypeTemplate, addNitroPlugin, addPlugin, extendPages, addImports, addComponent } from '@nuxt/kit';
2
- import { existsSync, mkdirSync, copyFileSync, readdirSync, rmSync } from 'node:fs';
1
+ import { hasNuxtModule, defineNuxtModule, createResolver, tryResolveModule, installModule, addServerTemplate, addServerHandler, addComponentsDir, addTemplate, addTypeTemplate, addNitroPlugin, addPlugin, extendPages, addImports, addComponent, addServerImports } from '@nuxt/kit';
2
+ import { watch, existsSync, readFileSync, mkdirSync, copyFileSync, readdirSync, rmSync } from 'node:fs';
3
3
  import { join, relative, resolve, basename } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
- import { f as fileLinkIssues } from '../shared/ginko.Dht0xxE5.mjs';
6
- import { s as serializeSchema, a as checkSite, b as redirectProblems, d as entryRedirects } from '../shared/ginko.CrNRBkS5.mjs';
7
- import { n as coreMessage } from '../shared/ginko.C-QjDn3O.mjs';
8
- import '../shared/ginko.CQvxhyez.mjs';
5
+ import { f as fileLinkIssues } from '../shared/ginko.raCzOPYC.mjs';
6
+ import { createHash, randomUUID } from 'node:crypto';
7
+ import { c as checkSite, s as serializeSchema } from '../shared/ginko.B1-DoOea.mjs';
8
+ import { g as siteForContent } from '../shared/ginko.C-oz8wwI.mjs';
9
+ import { L as redirectProblems, N as entryRedirects, F as coreMessage } from '../shared/ginko.BNVC-g9n.mjs';
10
+ import { w as withMarkdownRefs, f as fileMarkdownRef } from '../shared/ginko.DIl7l-75.mjs';
11
+ import '../shared/ginko.D1PeQ3ds.mjs';
9
12
  import 'comark';
10
13
  import 'comark/plugins/alert';
11
14
  import 'comark/plugins/attributes';
@@ -19,6 +22,32 @@ import 'comark/plugins/task-list';
19
22
  import 'comark/plugins/toc';
20
23
  import '@lupinum/ginko-i18n/core';
21
24
 
25
+ function appSchema(schema) {
26
+ const { components: _components, ...schemaRest } = schema;
27
+ return {
28
+ ...schemaRest,
29
+ bricks: Object.fromEntries(Object.entries(schema.bricks).map(([key, brick]) => [key, appBrick(brick)]))
30
+ };
31
+ }
32
+ function appBrick(brick) {
33
+ const { describe: _describe, tasks: _tasks, ...rest } = brick;
34
+ return { ...rest, label: "", fields: appFields(brick.fields) };
35
+ }
36
+ function componentsForMarkdown(schema) {
37
+ return Object.fromEntries(Object.entries(schema.components ?? {}).map(([tag, def]) => [
38
+ tag,
39
+ { kind: def.kind, props: Object.fromEntries(Object.entries(def.props).map(([name, field]) => [name, { type: field.type }])) }
40
+ ]));
41
+ }
42
+ function appFields(fields) {
43
+ return Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, appField(field)]));
44
+ }
45
+ function appField(field) {
46
+ const { hint: _hint, group: _group, ...rest } = field;
47
+ if (rest.type === "list") return { ...rest, label: "", itemLabel: "", of: appFields(rest.of) };
48
+ return { ...rest, label: "" };
49
+ }
50
+
22
51
  const OWNED = { locales: "locales", defaultLocale: "the first entry of locales", baseUrl: "url", timeZone: "timeZone" };
23
52
  function checkI18nOptions(userI18n) {
24
53
  const set = new Set(userI18n && typeof userI18n === "object" ? Object.entries(userI18n).filter(([, value]) => value !== void 0).map(([key]) => key) : []);
@@ -91,12 +120,13 @@ function unservedPaths(schema, pages) {
91
120
  add(pages, "");
92
121
  const served = (path) => patterns.some((pattern) => pattern.test(path));
93
122
  const warnings = [];
123
+ const ours = (locale) => schema.locales.includes(locale);
94
124
  for (const [key, paths] of Object.entries(schema.pages)) {
95
- for (const [locale, path] of Object.entries(paths)) if (path && !served(path)) warnings.push(`pages.${key}.${locale}: no page at \u201C${path}\u201D. Fix: create the page or change the path in ginko.config.ts.`);
125
+ for (const [locale, path] of Object.entries(paths)) if (path && ours(locale) && !served(path)) warnings.push(`pages.${key}.${locale}: no page at \u201C${path}\u201D. Fix: create the page or change the path in ginko.config.ts.`);
96
126
  }
97
127
  for (const [key, brick] of Object.entries(schema.bricks)) {
98
128
  if (brick.kind !== "collection" || !brick.route) continue;
99
- for (const [locale, route] of Object.entries(brick.route)) if (route && !served(route.replace("[slug]", "beispiel"))) warnings.push(`${key}.route.${locale}: no page at \u201C${route}\u201D. Fix: create the page (for example pages/\u2026/[slug].vue) or change the route.`);
129
+ for (const [locale, route] of Object.entries(brick.route)) if (route && ours(locale) && !served(route.replace("[slug]", "beispiel"))) warnings.push(`${key}.route.${locale}: no page at \u201C${route}\u201D. Fix: create the page (for example pages/\u2026/[slug].vue) or change the route.`);
100
130
  }
101
131
  return warnings;
102
132
  }
@@ -148,8 +178,61 @@ function ginkoRouteRules(site) {
148
178
  return rules;
149
179
  }
150
180
 
151
- function configFilesToWatch(rootDir) {
152
- return ["ts", "mts", "js", "mjs"].map((ext) => join(rootDir, `ginko.config.${ext}`)).filter(existsSync);
181
+ const CONFIG_NAMES = new Set(["ts", "mts", "js", "mjs"].map((ext) => `ginko.config.${ext}`));
182
+ function restartOnConfigChange(nuxt, rootDir) {
183
+ let ready = false;
184
+ let pending;
185
+ let restarting = false;
186
+ const restart = (name) => {
187
+ restarting = true;
188
+ console.info(`[ginko] ${name} changed: restarting the dev server.`);
189
+ void Promise.resolve(nuxt.callHook("restart", { hard: true })).catch((error) => console.error("[ginko] The dev server could not restart:", error));
190
+ };
191
+ const hashOf = (file) => existsSync(file) ? createHash("sha1").update(readFileSync(file)).digest("hex") : "";
192
+ const hashes = new Map([...CONFIG_NAMES].map((name) => [name, hashOf(join(rootDir, name))]));
193
+ const check = async (name) => {
194
+ const file = join(rootDir, name);
195
+ const hash = hashOf(file);
196
+ if (restarting || !hash || hashes.get(name) === hash) return;
197
+ hashes.set(name, hash);
198
+ const problem = await configProblem(file);
199
+ if (problem) console.error(`[ginko] ${name} has an error, the dev server keeps the last good version: ${problem}`);
200
+ else if (ready) restart(name);
201
+ else pending = name;
202
+ };
203
+ let queue = Promise.resolve();
204
+ let debounce;
205
+ const watcher = watch(rootDir, (_event, name) => {
206
+ if (!name || !CONFIG_NAMES.has(name)) return;
207
+ clearTimeout(debounce);
208
+ debounce = setTimeout(() => {
209
+ queue = queue.then(() => check(name)).catch((error) => console.error("[ginko]", error));
210
+ }, 100);
211
+ });
212
+ watcher.unref();
213
+ let settle;
214
+ nuxt.hook("ready", () => nuxt.hook("build:done", () => {
215
+ settle = setTimeout(() => {
216
+ ready = true;
217
+ if (pending && !restarting) restart(pending);
218
+ }, 1e3);
219
+ }));
220
+ nuxt.hook("close", () => {
221
+ clearTimeout(debounce);
222
+ clearTimeout(settle);
223
+ watcher.close();
224
+ });
225
+ }
226
+ async function configProblem(file) {
227
+ try {
228
+ const module = await import(`${pathToFileURL(file).href}?ginko=${randomUUID()}`);
229
+ const site = module.default;
230
+ if (!site?.site || !site.bricks) return "the default export must be the result of defineSite().";
231
+ const problems = checkSite(site);
232
+ return problems.length ? problems.join(" ") : void 0;
233
+ } catch (error) {
234
+ return error instanceof Error ? error.message.split("\n")[0] : String(error);
235
+ }
153
236
  }
154
237
 
155
238
  const runtimeExt = import.meta.url.endsWith(".mjs") ? "mjs" : "ts";
@@ -157,18 +240,19 @@ const module$1 = defineNuxtModule({
157
240
  meta: { name: "@lupinum/ginko", configKey: "ginko", compatibility: { nuxt: "^4.6.0 || ^5.0.0-0" } },
158
241
  // The site's data for the Nuxt SEO modules it installs (nuxt-seo.ts). Read from nuxt.config, before setup runs.
159
242
  moduleDependencies: (nuxt) => nuxtSeoDependencies(nuxt.options.ginko, nuxt),
160
- async setup(site, nuxt) {
161
- if (!site?.site || !site.bricks) throw new Error("[ginko] Option `ginko` is missing: the result of defineSite() from ginko.config.ts.");
243
+ async setup(options, nuxt) {
244
+ if (!options?.site || !options.bricks) throw new Error("[ginko] Option `ginko` is missing: the result of defineSite() from ginko.config.ts.");
245
+ const site = siteForContent(options, process.env.GINKO_CONTENT);
162
246
  const schema = serializeSchema(site);
163
247
  const problems = checkSite(site, schema);
164
248
  if (problems.length) throw new Error(`[ginko] ginko.config.ts is invalid:
165
249
  - ${problems.join("\n- ")}`);
250
+ if (nuxt.options.dev) restartOnConfigChange(nuxt, nuxt.options.rootDir);
166
251
  const sources = {
167
252
  platform: Object.values(schema.bricks).some((b) => b.source !== "files"),
168
253
  files: Object.values(schema.bricks).some((b) => b.source === "files")
169
254
  };
170
255
  const { resolve } = createResolver(import.meta.url);
171
- if (nuxt.options.dev) nuxt.options.watch.push(...configFilesToWatch(nuxt.options.rootDir));
172
256
  Object.assign(nuxt.options.runtimeConfig.public, { ginko: {
173
257
  site: schema.site,
174
258
  name: site.name,
@@ -178,6 +262,10 @@ const module$1 = defineNuxtModule({
178
262
  locales: schema.locales,
179
263
  mainLocale: schema.locales[0],
180
264
  timeZone: schema.timeZone,
265
+ // ISO country code or ''; the hours rules of the basics take it as `country`, JSON-LD as `addressCountry`.
266
+ country: site.country ?? "",
267
+ // The resolved holiday calendar (`schema.holidays`) or '': `custom` means the site passes its own `calendar`.
268
+ holidays: schema.holidays ?? "",
181
269
  seo: site.seo ?? {},
182
270
  agents: site.agents ?? {},
183
271
  sources
@@ -222,8 +310,10 @@ export const ogTemplate = 'GinkoCard'` : "export const defineOgImage = undefined
222
310
  nuxt.options.alias["#ginko/og"] = resolve(nuxt.options.buildDir, "ginko/og.mjs");
223
311
  addTemplate({ filename: "ginko/schema-org.mjs", getContents: () => runsNuxtSeo("nuxt-schema-org", nuxt) ? `export { useSchemaOrg } from '#imports'` : "export const useSchemaOrg = undefined" });
224
312
  nuxt.options.alias["#ginko/schema-org"] = resolve(nuxt.options.buildDir, "ginko/schema-org.mjs");
225
- addTemplate({ filename: "ginko/schema.mjs", getContents: () => `export default ${JSON.stringify(schema)}` });
313
+ addTemplate({ filename: "ginko/schema.mjs", getContents: () => `export default ${JSON.stringify(appSchema(schema))}` });
226
314
  nuxt.options.alias["#ginko/schema"] = resolve(nuxt.options.buildDir, "ginko/schema.mjs");
315
+ addTemplate({ filename: "ginko/components.mjs", getContents: () => `export default ${JSON.stringify(componentsForMarkdown(schema))}` });
316
+ nuxt.options.alias["#ginko/components"] = resolve(nuxt.options.buildDir, "ginko/components.mjs");
227
317
  addServerTemplate({ filename: "#ginko/schema", getContents: () => `export default ${JSON.stringify(schema)}` });
228
318
  const composables = resolve(`./runtime/composables.${runtimeExt}`);
229
319
  addTypeTemplate({ filename: "types/ginko-virtual.d.ts", getContents: () => `/// <reference path=${JSON.stringify(resolve("./runtime/schema.d.ts"))} />
@@ -252,6 +342,7 @@ export {}` }, { nuxt: true, nitro: true });
252
342
  else {
253
343
  addServerTemplate({ filename: "#ginko/files", getContents: () => "export default {}" });
254
344
  addServerTemplate({ filename: "#ginko/files-md", getContents: () => "export default {}" });
345
+ addServerTemplate({ filename: "#ginko/search", getContents: () => "export default {}" });
255
346
  }
256
347
  nuxt.hook("pages:resolved", (pages) => {
257
348
  for (const warning of unservedPaths(schema, pages)) console.warn(`[ginko] ${warning}`);
@@ -262,12 +353,14 @@ export {}` }, { nuxt: true, nitro: true });
262
353
  extendPages((pages) => {
263
354
  pages.push({ name: "ginko-preview", path: "/__ginko/preview", file: resolve("./runtime/preview.vue") });
264
355
  });
356
+ addServerHandler({ route: "/__ginko/platform/:locale/:rev/md/**", handler: resolve(`./runtime/server/platform-md.${runtimeExt}`) });
265
357
  }
266
358
  for (const name of ["useGinko", "useGinkoEntry", "useGinkoQuery", "useGinkoNeighbors", "useGinkoNav", "useGinkoBreadcrumbs", "useGinkoToday", "useGinkoWeek", "useGinkoLocale", "useGinkoSeo", "useGinkoPath", "useGinkoLang", "useGinkoSite"]) addImports({ name, from: composables });
267
359
  for (const name of ["GinkoItem", "GinkoPageKey", "GinkoLocale", "GinkoSeoInput"]) addImports({ name, from: composables, type: true });
268
360
  addComponent({ name: "GinkoMarkdown", export: "GinkoMarkdown", filePath: resolve(`./runtime/GinkoMarkdown.${runtimeExt}`) });
269
361
  addComponent({ name: "GinkoImage", export: "GinkoImage", filePath: resolve(`./runtime/GinkoImage.${runtimeExt}`) });
270
362
  for (const name of ["useGinkoMarkdown", "fetchGinkoMarkdown"]) addImports({ name, from: resolve(`./runtime/markdown.${runtimeExt}`) });
363
+ addServerImports([{ name: "getGinkoItems", from: resolve(`./runtime/server/items.${runtimeExt}`) }, { name: "GinkoServerItem", from: resolve(`./runtime/server/items.${runtimeExt}`), type: true }]);
271
364
  }
272
365
  });
273
366
  async function useFiles(schema, site, nuxt) {
@@ -312,7 +405,7 @@ ${linkReport}`);
312
405
  writeImages(result.imageFiles);
313
406
  trees = /* @__PURE__ */ new Map();
314
407
  const now = /* @__PURE__ */ new Date();
315
- bundles = Object.fromEntries(Object.entries(result.bundles).map(([locale, bundle]) => [locale, content.withMarkdownRefs(content.publicBundle(bundle, schema, now, fileOptions), schema, trees)]));
408
+ bundles = Object.fromEntries(Object.entries(result.bundles).map(([locale, bundle]) => [locale, withMarkdownRefs(content.publicBundle(bundle, schema, now, fileOptions), schema, trees, (brick, id, field) => fileMarkdownRef(locale, brick, id, field))]));
316
409
  searches = Object.fromEntries(await Promise.all(Object.entries(bundles).map(async ([locale, bundle]) => [locale, await content.searchIndex(bundle, trees, schema)])));
317
410
  };
318
411
  await build();
@@ -33,7 +33,7 @@ export declare const GinkoImage: import("vue").DefineComponent<import("vue").Ext
33
33
  default: boolean;
34
34
  };
35
35
  }>> & Readonly<{}>, {
36
- sizes: string;
37
36
  image: ImageValue | undefined;
37
+ sizes: string;
38
38
  eager: boolean;
39
39
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,5 +1,5 @@
1
1
  import { computed, defineComponent, h, inject, provide } from "vue";
2
- import schema from "#ginko/schema";
2
+ import components from "#ginko/components";
3
3
  import { GINKO_COMPONENTS_KEY, GINKO_TOC_KEY } from "../../internal.mjs";
4
4
  import { GinkoMarkdown as GinkoMarkdownTree } from "../../vue.mjs";
5
5
  import { useGinkoMarkdown } from "./markdown.mjs";
@@ -19,6 +19,6 @@ export const GinkoMarkdown = defineComponent({
19
19
  let tree;
20
20
  provide(GINKO_TOC_KEY, computed(() => tree?.value?.toc ?? null));
21
21
  tree = await useGinkoMarkdown(() => props.value);
22
- return () => h(GinkoMarkdownTree, { value: tree.value, components: { ...provided, ...props.components }, registry: schema.components });
22
+ return () => h(GinkoMarkdownTree, { value: tree.value, components: { ...provided, ...props.components }, registry: components });
23
23
  }
24
24
  });
@@ -90,11 +90,12 @@ export declare function useGinkoLang(): (item: {
90
90
  fallback?: readonly string[];
91
91
  }, path: string) => string | undefined;
92
92
  /**
93
- * The visible entry whose slug in the current locale is `slug`; otherwise a real 404 (SPEC 8). `slug` may be a
93
+ * The visible entry whose slug in the current locale is `slug`; otherwise a real 404 (SPEC 8). An entry without a
94
+ * page in this locale (untranslated) is a 404 too, also when a list links it to its main-language page. `slug` may be a
94
95
  * getter such as `() => route.params.slug`, so a reused page follows client navigation; an array (catch-all
95
- * route) is joined with `/`. The 404 is thrown when the value is read, not at the call.
96
+ * route) is joined with `/`. The 404 is thrown on every read of the value while no entry matches, not at the call.
96
97
  */
97
- export declare function useGinkoEntry<K extends CollectionKey>(brick: K, slug: MaybeRefOrGetter<string | string[] | undefined>): ComputedRef<GinkoItem<K>>;
98
+ export declare function useGinkoEntry<K extends CollectionKey>(brick: K, slug: MaybeRefOrGetter<string | string[] | undefined>): Readonly<Ref<GinkoItem<K>>>;
98
99
  export interface GinkoSeoInput {
99
100
  title?: string;
100
101
  description?: string;
@@ -1,4 +1,4 @@
1
- import { computed, createError, toValue, useI18n, useHead, useRoute, useRuntimeConfig, useState } from "#imports";
1
+ import { computed, createError, toRef, toValue, useI18n, useHead, useRoute, useRuntimeConfig, useState } from "#imports";
2
2
  import schema from "#ginko/schema";
3
3
  import { defineOgImage, ogTemplate } from "#ginko/og";
4
4
  import { useSchemaOrg } from "#ginko/schema-org";
@@ -9,10 +9,12 @@ import {
9
9
  navTree,
10
10
  neighbors,
11
11
  organizationLd,
12
+ ORGANIZATION_TYPES,
12
13
  query,
13
14
  seoHead,
14
15
  todayIn,
15
- websiteLd
16
+ websiteLd,
17
+ withCountry
16
18
  } from "../../internal.mjs";
17
19
  import { withUrl } from "./shared.mjs";
18
20
  export const BUNDLE_STATE = "ginko:bundle";
@@ -53,12 +55,11 @@ export function useGinkoWeek() {
53
55
  }
54
56
  export function useGinko(brick) {
55
57
  const bundle = useVisibleBundle([brick]);
56
- const locale = useGinkoLocale();
57
58
  return computed(() => {
58
59
  const value = bundle.value.bricks[brick];
59
60
  const def = schema.bricks[brick];
60
61
  if (value?.kind === "collection" && def) {
61
- return { items: def.kind === "collection" && def.route ? withUrl(value.items, locale.value) : value.items };
62
+ return { items: def.kind === "collection" && def.route ? withUrl(value.items, bundle.value.locale, bundle.value.mainLocale) : value.items };
62
63
  }
63
64
  return value;
64
65
  });
@@ -77,24 +78,22 @@ function useVisibleBundle(bricks) {
77
78
  return { ...bundle.value, bricks: out };
78
79
  });
79
80
  }
80
- function withRouteUrl(brick, items, locale) {
81
+ function withRouteUrl(brick, items, bundle) {
81
82
  const def = schema.bricks[brick];
82
- return def?.kind === "collection" && def.route ? withUrl(items, locale) : items;
83
+ return def?.kind === "collection" && def.route ? withUrl(items, bundle.locale, bundle.mainLocale) : items;
83
84
  }
84
85
  export function useGinkoQuery(brick, q = {}) {
85
86
  const bundle = useVisibleBundle([brick]);
86
- const locale = useGinkoLocale();
87
87
  return computed(() => {
88
88
  const result = query(bundle.value, brick, toValue(q), schema);
89
- return { ...result, items: withRouteUrl(brick, result.items, locale.value) };
89
+ return { ...result, items: withRouteUrl(brick, result.items, bundle.value) };
90
90
  });
91
91
  }
92
92
  export function useGinkoNeighbors(brick, id, options = {}) {
93
93
  const bundle = useVisibleBundle([brick]);
94
- const locale = useGinkoLocale();
95
94
  return computed(() => {
96
95
  const { prev, next } = neighbors(bundle.value, brick, toValue(id), options.order);
97
- const one = (item) => item && withRouteUrl(brick, [item], locale.value)[0];
96
+ const one = (item) => item && withRouteUrl(brick, [item], bundle.value)[0];
98
97
  return { prev: one(prev), next: one(next) };
99
98
  });
100
99
  }
@@ -116,12 +115,15 @@ export function useGinkoLang() {
116
115
  export function useGinkoEntry(brick, slug) {
117
116
  const items = useGinko(brick);
118
117
  const locale = useGinkoLocale();
119
- return computed(() => {
118
+ const entry = computed(() => {
120
119
  const def = schema.bricks[brick];
121
120
  const route = def.kind === "collection" ? def.route?.[locale.value] : void 0;
122
121
  const value = toValue(slug);
123
122
  const path = value === void 0 ? void 0 : route?.replace("[slug]", Array.isArray(value) ? value.join("/") : value);
124
- const item = items.value.items.find((i) => path && i.url === path);
123
+ return items.value.items.find((i) => path && i.urls?.[locale.value] === path);
124
+ });
125
+ return toRef(() => {
126
+ const item = entry.value;
125
127
  if (!item) throw createError({ statusCode: 404, statusMessage: "Not found", fatal: true });
126
128
  return item;
127
129
  });
@@ -153,7 +155,8 @@ export function useGinkoSeo(input) {
153
155
  }
154
156
  });
155
157
  }
156
- if (useSchemaOrg) useSchemaOrg(computed(() => (toValue(input).jsonLd ?? []).map(({ "@context": _context, ...node }) => node)));
158
+ const pageLd = () => (toValue(input).jsonLd ?? []).map((node) => withCountry(node, config.country));
159
+ if (useSchemaOrg) useSchemaOrg(computed(() => pageLd().map(({ "@context": _context, ...node }) => node)));
157
160
  const trim = (p) => p.length > 1 ? p.replace(/\/$/, "") : p;
158
161
  useHead(() => {
159
162
  const page = toValue(input);
@@ -161,12 +164,13 @@ export function useGinkoSeo(input) {
161
164
  const raw = generated ? void 0 : page.image ?? seo.image;
162
165
  const image = raw === void 0 ? void 0 : typeof raw === "string" ? { url: absolute(raw) } : { ...raw, url: absolute(raw.url) };
163
166
  const head = seoHead({ siteName: config.name, titleTemplate: seo.titleTemplate, title: page.title, description: page.description ?? seo.description, image, type: page.type, noindex: page.noindex });
164
- const ld = useSchemaOrg ? [] : [...page.jsonLd ?? []];
167
+ const ld = useSchemaOrg ? [] : pageLd();
165
168
  const home = Object.keys(schema.pages).find((key) => schema.pages[key]?.[config.mainLocale] === "/");
166
169
  if (!useSchemaOrg && home !== void 0 && trim(route.path) === trim(path(home, locale.value))) {
167
170
  const lang = i18n.locales.find((l) => l.code === locale.value)?.language ?? locale.value;
168
171
  ld.unshift(websiteLd({ name: config.name, url: config.url, lang }));
169
- if (seo.organization) ld.splice(1, 0, organizationLd({ name: config.name, url: config.url, ...seo.organization, logo: seo.organization.logo && absolute(seo.organization.logo) }));
172
+ const own = ld.some((node) => ORGANIZATION_TYPES.includes(node["@type"]));
173
+ if (seo.organization && !own) ld.splice(1, 0, organizationLd({ name: config.name, url: config.url, ...seo.organization, logo: seo.organization.logo && absolute(seo.organization.logo) }));
170
174
  }
171
175
  return {
172
176
  title: head.title,
@@ -8,8 +8,10 @@ import type { GinkoPublicConfig } from './shared.mjs';
8
8
  export declare function loadBundle(config: GinkoPublicConfig, locale: Locale, minRev: string | undefined, loadFiles: (locale: Locale) => Promise<Bundle | undefined>, siteToken?: string): Promise<Bundle>;
9
9
  /** A platform failure: a plain error that carries the status, for the caller to turn into its framework's error. */
10
10
  export declare class PlatformError extends Error {
11
+ readonly platformStatus?: number | undefined;
11
12
  readonly statusCode = 503;
12
- constructor(message: string, cause?: unknown);
13
+ /** `platformStatus`: the platform's own HTTP status when it answered with one (409: it has not reached `minRev`). */
14
+ constructor(message: string, cause?: unknown, platformStatus?: number | undefined);
13
15
  }
14
16
  /**
15
17
  * The 503 text. In dev the platform is a local address that is often not running (a new site, a pasted README
@@ -1,7 +1,7 @@
1
1
  import schema from "#ginko/schema";
2
- import { BUNDLE_FORMAT, bundleFits, isBundle, knownBricks, withFileBricks } from "../../internal.mjs";
2
+ import { BUNDLE_FORMAT, bundleFits, isBundle, knownBricks, SITE_BUDGET, withFileBricks } from "../../internal.mjs";
3
3
  const TIMEOUT_MS = 1e4;
4
- const MAX_CHARS = 8 * 1024 * 1024;
4
+ const MAX_CHARS = (SITE_BUDGET.siteBytes + SITE_BUDGET.treeBytes) * 1.5;
5
5
  export async function loadBundle(config, locale, minRev, loadFiles, siteToken) {
6
6
  const [platform, files] = await Promise.all([
7
7
  config.sources.platform ? fetchBundle(config, locale, minRev, siteToken) : void 0,
@@ -10,10 +10,12 @@ export async function loadBundle(config, locale, minRev, loadFiles, siteToken) {
10
10
  return withFileBricks(platform, files, schema);
11
11
  }
12
12
  export class PlatformError extends Error {
13
- statusCode = 503;
14
- constructor(message, cause) {
13
+ /** `platformStatus`: the platform's own HTTP status when it answered with one (409: it has not reached `minRev`). */
14
+ constructor(message, cause, platformStatus) {
15
15
  super(message, { cause });
16
+ this.platformStatus = platformStatus;
16
17
  }
18
+ statusCode = 503;
17
19
  }
18
20
  export function unreachableMessage(config, url) {
19
21
  const local = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/.test(config.platform);
@@ -34,16 +36,20 @@ export async function fetchBundle(config, locale, minRev, siteToken) {
34
36
  if (minRev && /^\d+\.\d+$/.test(minRev)) query.set("minRev", minRev);
35
37
  const url = `${config.platform.replace(/\/$/, "")}/bundle/${encodeURIComponent(config.site)}?${query}`;
36
38
  let body;
39
+ let status;
37
40
  try {
38
41
  const headers = { accept: "application/json" };
39
42
  if (siteToken) headers[SITE_TOKEN_HEADER] = siteToken;
40
43
  const response = await fetch(url, { headers, signal: AbortSignal.timeout(TIMEOUT_MS) });
41
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
44
+ if (!response.ok) {
45
+ status = response.status;
46
+ throw new Error(`HTTP ${status}`);
47
+ }
42
48
  const text = await response.text();
43
49
  if (text.length > MAX_CHARS) throw new Error("bundle too large");
44
50
  body = JSON.parse(text);
45
51
  } catch (error) {
46
- throw new PlatformError(unreachableMessage(config, url), error);
52
+ throw new PlatformError(unreachableMessage(config, url), error, status);
47
53
  }
48
54
  if (!isBundle(body) || body.site !== config.site || !bundleFits(body, schema)) throw new PlatformError("Content in the wrong format");
49
55
  return knownBricks(body, schema);
@@ -1,5 +1,8 @@
1
1
  import { type ComputedRef, type MaybeRefOrGetter } from 'vue';
2
+ import type { NuxtApp } from '#app';
2
3
  import type { MarkdownTree, MarkdownValue } from '../../internal.mjs';
4
+ /** The trees of this server request (empty outside a server render). @internal Filled by the Ginko plugin. */
5
+ export declare function serverTrees(nuxtApp: NuxtApp): Map<string, MarkdownTree>;
3
6
  /**
4
7
  * The tree of a markdown value, for server code such as a list page's `useAsyncData` that reads
5
8
  * several texts and keeps only what the page needs. `undefined` (an optional field) gives `null`.
@@ -1,15 +1,27 @@
1
1
  import { computed, toValue } from "vue";
2
- import { useAsyncData } from "#imports";
2
+ import { tryUseNuxtApp, useAsyncData } from "#imports";
3
+ const requestTrees = /* @__PURE__ */ new WeakMap();
4
+ export function serverTrees(nuxtApp) {
5
+ const context = nuxtApp.ssrContext;
6
+ if (!context) return /* @__PURE__ */ new Map();
7
+ let trees = requestTrees.get(context);
8
+ if (!trees) requestTrees.set(context, trees = /* @__PURE__ */ new Map());
9
+ return trees;
10
+ }
11
+ function loadTree(ref, nuxtApp = tryUseNuxtApp() ?? void 0) {
12
+ const tree = import.meta.server && nuxtApp?.ssrContext ? requestTrees.get(nuxtApp.ssrContext)?.get(ref) : void 0;
13
+ return tree ? Promise.resolve(tree) : $fetch(ref);
14
+ }
3
15
  export async function fetchGinkoMarkdown(value) {
4
16
  if (!value) return null;
5
- return "nodes" in value ? value : $fetch(value.ref);
17
+ return "nodes" in value ? value : loadTree(value.ref);
6
18
  }
7
19
  export async function useGinkoMarkdown(value) {
8
20
  const ref = computed(() => {
9
21
  const v = toValue(value);
10
22
  return v && "ref" in v ? v.ref : "";
11
23
  });
12
- const { data } = await useAsyncData(() => `ginko-md:${ref.value}`, () => ref.value ? $fetch(ref.value) : Promise.resolve(null));
24
+ const { data } = await useAsyncData(() => `ginko-md:${ref.value}`, (nuxtApp) => ref.value ? loadTree(ref.value, nuxtApp) : Promise.resolve(null));
13
25
  return computed(() => {
14
26
  const v = toValue(value);
15
27
  return v && "nodes" in v ? v : data.value ?? null;
@@ -1,8 +1,9 @@
1
- import { addAlternatesResolver, addRouteMiddleware, clearError, createError, defineNuxtPlugin, useError, useRequestHeader, useRequestURL, useResponseHeader, useRuntimeConfig, useState } from "#imports";
1
+ import { addAlternatesResolver, addRouteMiddleware, createError, defineNuxtPlugin, useRequestHeader, useRequestURL, useResponseHeader, useRuntimeConfig, useState } from "#imports";
2
2
  import schema from "#ginko/schema";
3
- import { bundleFits, isBundle, knownBricks, withFileBricks } from "../../internal.mjs";
3
+ import { platformMarkdownRef, withMarkdownRefs } from "../../internal.mjs";
4
4
  import { BUNDLE_STATE, PREVIEW_STATE } from "./composables.mjs";
5
5
  import { loadBundle as loadBundleCore, PlatformError } from "./fetch.mjs";
6
+ import { serverTrees } from "./markdown.mjs";
6
7
  import { localeFromPath } from "@lupinum/ginko-i18n/core";
7
8
  import { config as i18nConfig } from "#ginko-i18n/config.mjs";
8
9
  async function loadBundle(config, locale, minRev, siteToken) {
@@ -24,10 +25,11 @@ export default defineNuxtPlugin(async (nuxtApp) => {
24
25
  const preview = useState(PREVIEW_STATE, () => null);
25
26
  const load = async (locale) => {
26
27
  if (bundles.value[locale]) return;
27
- bundles.value = { ...bundles.value, [locale]: await loadBundle(config, locale, minRev, siteToken) };
28
+ const full = await loadBundle(config, locale, minRev, siteToken);
29
+ const rev = `${full.schemaRev}.${full.contentRev}`;
30
+ bundles.value = { ...bundles.value, [locale]: import.meta.server && config.sources.platform ? withMarkdownRefs(full, schema, serverTrees(nuxtApp), (brick, id, field) => platformMarkdownRef(locale, rev, brick, id, field)) : full };
28
31
  };
29
32
  const path = useRequestURL().pathname;
30
- if (import.meta.client && PREVIEW.test(path)) listenToPlatform(nuxtApp, config, bundles, preview);
31
33
  const errorQuery = ERROR_PAGE.test(path) ? useRequestURL().searchParams : void 0;
32
34
  const pageError = () => import.meta.server ? nuxtApp.ssrContext?.error : nuxtApp.payload.error;
33
35
  const errorPage = () => {
@@ -56,34 +58,3 @@ function entryUrls(bundle, path) {
56
58
  }
57
59
  return void 0;
58
60
  }
59
- function listenToPlatform(nuxtApp, config, bundles, preview) {
60
- const origin = config.platform ? new URL(config.platform).origin : "";
61
- window.addEventListener("message", (event) => {
62
- const state = preview.value;
63
- if (!state || !origin || event.source !== window.parent || event.origin !== origin) return;
64
- const data = event.data;
65
- if (typeof data !== "object" || data === null) return;
66
- const message = data;
67
- if (message.type !== "ginko:preview" || message.session !== state.session || message.site !== config.site || message.locale !== state.locale || !isBundle(message.bundle) || !bundleFits(message.bundle, schema)) return;
68
- const platform = knownBricks(message.bundle, schema);
69
- const locale = state.locale;
70
- void filesOf(locale).then((files) => {
71
- if (preview.value?.session !== state.session) return;
72
- bundles.value = { ...bundles.value, [locale]: withFileBricks(platform, files, schema) };
73
- void nuxtApp.runWithContext(() => useError().value ? clearError() : void 0);
74
- });
75
- });
76
- const fileBundles = {};
77
- const filesOf = (locale) => fileBundles[locale] ??= config.sources.files ? $fetch(`/__ginko/files/${locale}.json`).catch(() => void 0) : Promise.resolve(void 0);
78
- const interceptLink = (event) => {
79
- const state = preview.value;
80
- if (!state || event.type === "auxclick" && event.button !== 1 || !(event.target instanceof Element)) return;
81
- const link = event.target.closest("a[href]");
82
- if (!link) return;
83
- event.preventDefault();
84
- const url = new URL(link.getAttribute("href") ?? "", location.href);
85
- if (origin && url.origin === location.origin) window.parent.postMessage({ type: "ginko:navigate", session: state.session, path: url.pathname + url.search + url.hash }, origin);
86
- };
87
- document.addEventListener("click", interceptLink, true);
88
- document.addEventListener("auxclick", interceptLink, true);
89
- }
@@ -0,0 +1,13 @@
1
+ import type { NuxtApp } from '#app';
2
+ import type { Ref } from 'vue';
3
+ import { type Bundle, type Locale } from '../../internal.mjs';
4
+ import type { GinkoPublicConfig } from './shared.mjs';
5
+ /**
6
+ * Preview messages (SPEC 8 "Vorschau"). Only the platform's frame (source and origin) may send a bundle
7
+ * for the session and locale the preview page announced. Links are cancelled and reported to the platform.
8
+ * Once per page load: a second call does nothing.
9
+ */
10
+ export declare function listenToPlatform(nuxtApp: NuxtApp, config: GinkoPublicConfig, bundles: Ref<Partial<Record<Locale, Bundle>>>, preview: Ref<{
11
+ session: string;
12
+ locale: Locale;
13
+ } | null>): void;
@@ -0,0 +1,37 @@
1
+ import { clearError, useError } from "#imports";
2
+ import schema from "#ginko/schema";
3
+ import { bundleFits, isBundle, knownBricks, withFileBricks } from "../../internal.mjs";
4
+ let listening = false;
5
+ export function listenToPlatform(nuxtApp, config, bundles, preview) {
6
+ if (listening) return;
7
+ listening = true;
8
+ const origin = config.platform ? new URL(config.platform).origin : "";
9
+ window.addEventListener("message", (event) => {
10
+ const state = preview.value;
11
+ if (!state || !origin || event.source !== window.parent || event.origin !== origin) return;
12
+ const data = event.data;
13
+ if (typeof data !== "object" || data === null) return;
14
+ const message = data;
15
+ if (message.type !== "ginko:preview" || message.session !== state.session || message.site !== config.site || message.locale !== state.locale || !isBundle(message.bundle) || !bundleFits(message.bundle, schema)) return;
16
+ const platform = knownBricks(message.bundle, schema);
17
+ const locale = state.locale;
18
+ void filesOf(locale).then((files) => {
19
+ if (preview.value?.session !== state.session) return;
20
+ bundles.value = { ...bundles.value, [locale]: withFileBricks(platform, files, schema) };
21
+ void nuxtApp.runWithContext(() => useError().value ? clearError() : void 0);
22
+ });
23
+ });
24
+ const fileBundles = {};
25
+ const filesOf = (locale) => fileBundles[locale] ??= config.sources.files ? $fetch(`/__ginko/files/${locale}.json`).catch(() => void 0) : Promise.resolve(void 0);
26
+ const interceptLink = (event) => {
27
+ const state = preview.value;
28
+ if (!state || event.type === "auxclick" && event.button !== 1 || !(event.target instanceof Element)) return;
29
+ const link = event.target.closest("a[href]");
30
+ if (!link) return;
31
+ event.preventDefault();
32
+ const url = new URL(link.getAttribute("href") ?? "", location.href);
33
+ if (origin && url.origin === location.origin) window.parent.postMessage({ type: "ginko:navigate", session: state.session, path: url.pathname + url.search + url.hash }, origin);
34
+ };
35
+ document.addEventListener("click", interceptLink, true);
36
+ document.addEventListener("auxclick", interceptLink, true);
37
+ }
@@ -1,13 +1,15 @@
1
1
  <script setup>
2
- import { definePageMeta, navigateTo, onBeforeUnmount, onMounted, ref, useRequestHeaders, useRoute, useRouter, useRuntimeConfig, useState, watch } from "#imports";
2
+ import { definePageMeta, navigateTo, onBeforeUnmount, onMounted, ref, useNuxtApp, useRequestHeaders, useRoute, useRouter, useRuntimeConfig, useState, watch } from "#imports";
3
3
  import { coreMessage } from "../../internal.mjs";
4
4
  import { BUNDLE_STATE, PREVIEW_STATE } from "./composables.mjs";
5
+ import { listenToPlatform } from "./preview-listener.mjs";
5
6
  definePageMeta({ layout: false });
6
7
  const route = useRoute();
7
8
  const router = useRouter();
8
9
  const config = useRuntimeConfig().public.ginko;
9
10
  const bundles = useState(BUNDLE_STATE, () => ({}));
10
11
  const preview = useState(PREVIEW_STATE, () => null);
12
+ if (import.meta.client) listenToPlatform(useNuxtApp(), config, bundles, preview);
11
13
  const reader = useState("ginko:preview-reader", () => (import.meta.server ? useRequestHeaders(["accept-language"])["accept-language"]?.split(/[,;]/)[0] : navigator.language) || "en");
12
14
  function isSafePath(raw) {
13
15
  let decoded;
@@ -7,6 +7,12 @@ declare module '#ginko/schema' {
7
7
  export default schema
8
8
  }
9
9
 
10
+ // The Markdown component registry for the renderer (module.ts, componentsForMarkdown): declared props and their types.
11
+ declare module '#ginko/components' {
12
+ const components: import('@lupinum/ginko/internal').AttributeRegistry
13
+ export default components
14
+ }
15
+
10
16
  // The site's file bricks as bundles per locale (CONTENT.md), a Nitro virtual module built from content/.
11
17
  declare module '#ginko/files' {
12
18
  const bundles: Partial<Record<import('@lupinum/ginko').Locale, import('@lupinum/ginko/internal').Bundle>>
@@ -0,0 +1,4 @@
1
+ import { getRequestHeader } from 'nuxt/server';
2
+ import type { Bundle, Locale } from '../../../internal.mjs';
3
+ import type { GinkoPublicConfig } from '../shared.mjs';
4
+ export declare function loadServerBundles(event: Parameters<typeof getRequestHeader>[0], config: GinkoPublicConfig, locales: readonly Locale[]): Promise<Partial<Record<Locale, Bundle>>>;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/server").EventHandler<import("../../..").MarkdownTree>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/server").EventHandler<import("../../../core").Bundle>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/server").EventHandler<void>;
2
+ export default _default;