@lupinum/ginko 0.4.0 → 0.5.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 (89) hide show
  1. package/CHANGELOG.md +150 -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 +54 -55
  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 +105 -13
  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.1wwZv4B_.d.ts +1622 -0
  55. package/dist/shared/{ginko.BBkZW2nW.d.ts → ginko.6cI8KXZP.d.mts} +2 -2
  56. package/dist/shared/{ginko.CQvxhyez.mjs → ginko.AprJymER.mjs} +30 -1
  57. package/dist/shared/ginko.BNVC-g9n.mjs +10389 -0
  58. package/dist/shared/{ginko.Bwan3BB-.d.mts → ginko.B_pACpVU.d.ts} +2 -2
  59. package/dist/shared/{ginko.CrNRBkS5.mjs → ginko.C4gcOovG.mjs} +253 -309
  60. package/dist/shared/{ginko.C90fPpMP.mjs → ginko.CXA2R3aS.mjs} +128 -101
  61. package/dist/shared/ginko.CjWB8N7f.d.ts +128 -0
  62. package/dist/shared/ginko.D1PeQ3ds.mjs +36 -0
  63. package/dist/shared/ginko.DIl7l-75.mjs +26 -0
  64. package/dist/shared/ginko.DY7RbCEw.mjs +601 -0
  65. package/dist/shared/ginko.Dmgixfpf.mjs +130 -0
  66. package/dist/shared/{ginko.D-fNojn9.d.mts → ginko.DrkUeYFP.d.mts} +824 -10
  67. package/dist/shared/{ginko.D-fNojn9.d.ts → ginko.DrkUeYFP.d.ts} +824 -10
  68. package/dist/shared/{ginko.akHewCGt.d.mts → ginko.DvPtlHIz.d.mts} +50 -5
  69. package/dist/shared/{ginko.BDF3bW0C.d.ts → ginko.EelR5dt9.d.ts} +50 -5
  70. package/dist/shared/ginko.ULKQnAWd.d.ts +91 -0
  71. package/dist/shared/ginko.loGIxGZg.d.mts +91 -0
  72. package/dist/shared/ginko.o1yIKlgj.d.mts +128 -0
  73. package/dist/shared/ginko.plW4dLmm.d.mts +1622 -0
  74. package/dist/shared/{ginko.Dht0xxE5.mjs → ginko.raCzOPYC.mjs} +1 -2
  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.Dc0oNcDv.d.ts +0 -172
  89. 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.C4gcOovG.mjs';
8
+ import { L as redirectProblems, N as entryRedirects, F as coreMessage } from '../shared/ginko.BNVC-g9n.mjs';
9
+ import { w as withMarkdownRefs, f as fileMarkdownRef } from '../shared/ginko.DIl7l-75.mjs';
10
+ import '../shared/ginko.D1PeQ3ds.mjs';
11
+ import '../shared/ginko.AprJymER.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";
@@ -163,12 +246,12 @@ const module$1 = defineNuxtModule({
163
246
  const problems = checkSite(site, schema);
164
247
  if (problems.length) throw new Error(`[ginko] ginko.config.ts is invalid:
165
248
  - ${problems.join("\n- ")}`);
249
+ if (nuxt.options.dev) restartOnConfigChange(nuxt, nuxt.options.rootDir);
166
250
  const sources = {
167
251
  platform: Object.values(schema.bricks).some((b) => b.source !== "files"),
168
252
  files: Object.values(schema.bricks).some((b) => b.source === "files")
169
253
  };
170
254
  const { resolve } = createResolver(import.meta.url);
171
- if (nuxt.options.dev) nuxt.options.watch.push(...configFilesToWatch(nuxt.options.rootDir));
172
255
  Object.assign(nuxt.options.runtimeConfig.public, { ginko: {
173
256
  site: schema.site,
174
257
  name: site.name,
@@ -178,6 +261,10 @@ const module$1 = defineNuxtModule({
178
261
  locales: schema.locales,
179
262
  mainLocale: schema.locales[0],
180
263
  timeZone: schema.timeZone,
264
+ // ISO country code or ''; the hours rules of the basics take it as `country`, JSON-LD as `addressCountry`.
265
+ country: site.country ?? "",
266
+ // The resolved holiday calendar (`schema.holidays`) or '': `custom` means the site passes its own `calendar`.
267
+ holidays: schema.holidays ?? "",
181
268
  seo: site.seo ?? {},
182
269
  agents: site.agents ?? {},
183
270
  sources
@@ -222,8 +309,10 @@ export const ogTemplate = 'GinkoCard'` : "export const defineOgImage = undefined
222
309
  nuxt.options.alias["#ginko/og"] = resolve(nuxt.options.buildDir, "ginko/og.mjs");
223
310
  addTemplate({ filename: "ginko/schema-org.mjs", getContents: () => runsNuxtSeo("nuxt-schema-org", nuxt) ? `export { useSchemaOrg } from '#imports'` : "export const useSchemaOrg = undefined" });
224
311
  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)}` });
312
+ addTemplate({ filename: "ginko/schema.mjs", getContents: () => `export default ${JSON.stringify(appSchema(schema))}` });
226
313
  nuxt.options.alias["#ginko/schema"] = resolve(nuxt.options.buildDir, "ginko/schema.mjs");
314
+ addTemplate({ filename: "ginko/components.mjs", getContents: () => `export default ${JSON.stringify(componentsForMarkdown(schema))}` });
315
+ nuxt.options.alias["#ginko/components"] = resolve(nuxt.options.buildDir, "ginko/components.mjs");
227
316
  addServerTemplate({ filename: "#ginko/schema", getContents: () => `export default ${JSON.stringify(schema)}` });
228
317
  const composables = resolve(`./runtime/composables.${runtimeExt}`);
229
318
  addTypeTemplate({ filename: "types/ginko-virtual.d.ts", getContents: () => `/// <reference path=${JSON.stringify(resolve("./runtime/schema.d.ts"))} />
@@ -252,6 +341,7 @@ export {}` }, { nuxt: true, nitro: true });
252
341
  else {
253
342
  addServerTemplate({ filename: "#ginko/files", getContents: () => "export default {}" });
254
343
  addServerTemplate({ filename: "#ginko/files-md", getContents: () => "export default {}" });
344
+ addServerTemplate({ filename: "#ginko/search", getContents: () => "export default {}" });
255
345
  }
256
346
  nuxt.hook("pages:resolved", (pages) => {
257
347
  for (const warning of unservedPaths(schema, pages)) console.warn(`[ginko] ${warning}`);
@@ -262,12 +352,14 @@ export {}` }, { nuxt: true, nitro: true });
262
352
  extendPages((pages) => {
263
353
  pages.push({ name: "ginko-preview", path: "/__ginko/preview", file: resolve("./runtime/preview.vue") });
264
354
  });
355
+ addServerHandler({ route: "/__ginko/platform/:locale/:rev/md/**", handler: resolve(`./runtime/server/platform-md.${runtimeExt}`) });
265
356
  }
266
357
  for (const name of ["useGinko", "useGinkoEntry", "useGinkoQuery", "useGinkoNeighbors", "useGinkoNav", "useGinkoBreadcrumbs", "useGinkoToday", "useGinkoWeek", "useGinkoLocale", "useGinkoSeo", "useGinkoPath", "useGinkoLang", "useGinkoSite"]) addImports({ name, from: composables });
267
358
  for (const name of ["GinkoItem", "GinkoPageKey", "GinkoLocale", "GinkoSeoInput"]) addImports({ name, from: composables, type: true });
268
359
  addComponent({ name: "GinkoMarkdown", export: "GinkoMarkdown", filePath: resolve(`./runtime/GinkoMarkdown.${runtimeExt}`) });
269
360
  addComponent({ name: "GinkoImage", export: "GinkoImage", filePath: resolve(`./runtime/GinkoImage.${runtimeExt}`) });
270
361
  for (const name of ["useGinkoMarkdown", "fetchGinkoMarkdown"]) addImports({ name, from: resolve(`./runtime/markdown.${runtimeExt}`) });
362
+ addServerImports([{ name: "getGinkoItems", from: resolve(`./runtime/server/items.${runtimeExt}`) }, { name: "GinkoServerItem", from: resolve(`./runtime/server/items.${runtimeExt}`), type: true }]);
271
363
  }
272
364
  });
273
365
  async function useFiles(schema, site, nuxt) {
@@ -312,7 +404,7 @@ ${linkReport}`);
312
404
  writeImages(result.imageFiles);
313
405
  trees = /* @__PURE__ */ new Map();
314
406
  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)]));
407
+ 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
408
  searches = Object.fromEntries(await Promise.all(Object.entries(bundles).map(async ([locale, bundle]) => [locale, await content.searchIndex(bundle, trees, schema)])));
317
409
  };
318
410
  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;
@@ -0,0 +1,12 @@
1
+ import { type RequestEvent } from 'nuxt/server';
2
+ import { type BundleItem } from '../../../internal.mjs';
3
+ /** An entry as `getGinkoItems` returns it: `url` is its path in the requested language (collections with a route). */
4
+ export type GinkoServerItem = BundleItem & {
5
+ url?: string;
6
+ };
7
+ /**
8
+ * The visible entries of collection `brick` in `locale` (default: the main language), in the collection's order, as
9
+ * `useGinko(brick).items` has them on a page: drafts and entries past their `visible` date are left out. Throws a 404
10
+ * for a language the site does not have or a key that is no collection, and a 503 when the platform cannot be reached.
11
+ */
12
+ export declare function getGinkoItems(event: RequestEvent, brick: string, locale?: string): Promise<GinkoServerItem[]>;