blume 2.2.2 → 2.2.3
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/CHANGELOG.md +10 -0
- package/dist/cli/{chunk-080mfch6.js → chunk-0wt5amrk.js} +15 -15
- package/dist/cli/{chunk-k45sq16q.js → chunk-0xyedh7k.js} +39 -23
- package/dist/cli/{chunk-k45sq16q.js.map → chunk-0xyedh7k.js.map} +3 -3
- package/dist/cli/{chunk-f260jdrn.js → chunk-0ynr0fr7.js} +5 -5
- package/dist/cli/{chunk-e1n41w9m.js → chunk-1ay3hzn0.js} +3 -3
- package/dist/cli/{chunk-b680sv30.js → chunk-2f6btenb.js} +10 -8
- package/dist/cli/chunk-2f6btenb.js.map +13 -0
- package/dist/cli/{chunk-gwy55cx3.js → chunk-426rytg6.js} +2 -2
- package/dist/cli/{chunk-9h8mh2tk.js → chunk-5f96yq2c.js} +1 -1
- package/dist/cli/{chunk-x60e15st.js → chunk-5xq12jz7.js} +17 -17
- package/dist/cli/{chunk-x7bh6d9j.js → chunk-6xag1c83.js} +4 -4
- package/dist/cli/{chunk-487b95qy.js → chunk-88q28zav.js} +41 -25
- package/dist/cli/{chunk-487b95qy.js.map → chunk-88q28zav.js.map} +3 -3
- package/dist/cli/{chunk-yg8ckc3g.js → chunk-88zkpg89.js} +2 -2
- package/dist/cli/{chunk-ppa6mg4d.js → chunk-8fm3b4vm.js} +6 -6
- package/dist/cli/{chunk-rtvgmg2j.js → chunk-8h2t2vyj.js} +2 -2
- package/dist/cli/{chunk-2ssh60qh.js → chunk-982qfeny.js} +6 -6
- package/dist/cli/{chunk-t2h8pwy1.js → chunk-9emfspcp.js} +4 -4
- package/dist/cli/{chunk-9jqmq5hv.js → chunk-akj2p7k1.js} +6 -6
- package/dist/cli/{chunk-beb0z983.js → chunk-b664x65a.js} +16 -16
- package/dist/cli/{chunk-kmswjxj9.js → chunk-b7n58hgj.js} +3 -3
- package/dist/cli/{chunk-fnxgtjeq.js → chunk-c0qddrxr.js} +1 -1
- package/dist/cli/{chunk-p834rkcn.js → chunk-g62vnvaw.js} +5 -5
- package/dist/cli/{chunk-zxd5jrgx.js → chunk-gc7knd2s.js} +76 -34
- package/dist/cli/{chunk-zxd5jrgx.js.map → chunk-gc7knd2s.js.map} +4 -4
- package/dist/cli/{chunk-r5nm9w0c.js → chunk-hwjspffr.js} +105 -36
- package/dist/cli/{chunk-r5nm9w0c.js.map → chunk-hwjspffr.js.map} +11 -10
- package/dist/cli/{chunk-40vk8kgf.js → chunk-p4fe7tax.js} +7 -7
- package/dist/cli/{chunk-34wap3ed.js → chunk-psg2za5z.js} +3 -3
- package/dist/cli/{chunk-hktq4j5r.js → chunk-pxp1y6yz.js} +16 -10
- package/dist/cli/{chunk-hktq4j5r.js.map → chunk-pxp1y6yz.js.map} +4 -4
- package/dist/cli/{chunk-qcyxkj44.js → chunk-qax1aekz.js} +43 -17
- package/dist/cli/chunk-qax1aekz.js.map +15 -0
- package/dist/cli/{chunk-h0qwdbem.js → chunk-r5ajrw1g.js} +2 -2
- package/dist/cli/{chunk-q1wwrxzw.js → chunk-rc2qk3qx.js} +3 -3
- package/dist/cli/chunk-rc2qk3qx.js.map +10 -0
- package/dist/cli/{chunk-69rjf786.js → chunk-ry2ymbxf.js} +5 -5
- package/dist/cli/{chunk-gxhcf2hs.js → chunk-t43g7v22.js} +2 -2
- package/dist/cli/{chunk-ydqzkhjn.js → chunk-x0jqyq1g.js} +6 -6
- package/dist/cli/{chunk-aafhwx74.js → chunk-x25s4gph.js} +1 -1
- package/dist/cli/{chunk-nqna8dzp.js → chunk-xkx216rd.js} +17 -17
- package/dist/cli/{chunk-etckvbrh.js → chunk-xv8nkt56.js} +14 -14
- package/dist/cli/{chunk-hn3g2b89.js → chunk-yrmw5n2b.js} +9 -9
- package/dist/cli/{chunk-4zxh25hc.js → chunk-zgca0vxs.js} +2 -2
- package/dist/cli/{chunk-c5bjxq8v.js → chunk-zj6bcx0n.js} +2 -2
- package/dist/cli/{chunk-1ch3ct3r.js → chunk-zjfwzrvd.js} +14 -14
- package/dist/cli/{chunk-j4nfgn2j.js → chunk-znwhf37a.js} +1 -1
- package/dist/cli/index.js +18 -18
- package/dist/types/ai/ask-context.d.ts +8 -0
- package/dist/types/ai/link-text.d.ts +5 -0
- package/dist/types/ai/relative-links.d.ts +26 -6
- package/dist/types/core/config-input.d.ts +35 -1
- package/dist/types/core/data.d.ts +14 -0
- package/dist/types/core/last-modified.d.ts +33 -18
- package/dist/types/core/links.d.ts +16 -0
- package/dist/types/core/schema.d.ts +16 -1
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/deploy/sitemap.d.ts +9 -0
- package/dist/types/search/documents.d.ts +3 -1
- package/dist/types/search/fold.d.ts +33 -0
- package/dist/types/search/orama-index.d.ts +4 -4
- package/dist/types/seo/jsonld.d.ts +33 -0
- package/docs/configuration/search.mdx +2 -2
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/sources/remote-mdx.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +16 -1
- package/docs/discoverability/sitemap-and-robots.mdx +12 -0
- package/docs/discoverability/structured-data.mdx +44 -0
- package/package.json +1 -1
- package/src/ai/ask-context.ts +52 -14
- package/src/ai/ask-data.ts +4 -1
- package/src/ai/ask-tools.ts +17 -5
- package/src/ai/link-text.ts +6 -0
- package/src/ai/llms.ts +1 -7
- package/src/ai/markdown.ts +8 -4
- package/src/ai/relative-links.ts +155 -48
- package/src/ai/site-skill.ts +1 -4
- package/src/astro/generate.ts +7 -0
- package/src/astro/templates.ts +36 -20
- package/src/components/layout/PageLayout.astro +25 -12
- package/src/components/layout/RootLayout.astro +7 -0
- package/src/components/layout/locale-redirect.ts +38 -19
- package/src/components/layout/search/types.ts +12 -4
- package/src/core/config-input.ts +34 -1
- package/src/core/config.ts +5 -2
- package/src/core/data.ts +14 -0
- package/src/core/last-modified.ts +80 -34
- package/src/core/links.ts +43 -2
- package/src/core/manifest.ts +2 -0
- package/src/core/project-graph.ts +83 -43
- package/src/core/schema.ts +47 -9
- package/src/core/types.ts +10 -0
- package/src/deploy/sitemap.ts +127 -49
- package/src/search/documents.ts +34 -11
- package/src/search/fold.ts +175 -0
- package/src/search/orama-index.ts +40 -49
- package/src/seo/jsonld.ts +100 -23
- package/dist/cli/chunk-b680sv30.js.map +0 -13
- package/dist/cli/chunk-q1wwrxzw.js.map +0 -10
- package/dist/cli/chunk-qcyxkj44.js.map +0 -14
- /package/dist/cli/{chunk-080mfch6.js.map → chunk-0wt5amrk.js.map} +0 -0
- /package/dist/cli/{chunk-f260jdrn.js.map → chunk-0ynr0fr7.js.map} +0 -0
- /package/dist/cli/{chunk-e1n41w9m.js.map → chunk-1ay3hzn0.js.map} +0 -0
- /package/dist/cli/{chunk-gwy55cx3.js.map → chunk-426rytg6.js.map} +0 -0
- /package/dist/cli/{chunk-9h8mh2tk.js.map → chunk-5f96yq2c.js.map} +0 -0
- /package/dist/cli/{chunk-x60e15st.js.map → chunk-5xq12jz7.js.map} +0 -0
- /package/dist/cli/{chunk-x7bh6d9j.js.map → chunk-6xag1c83.js.map} +0 -0
- /package/dist/cli/{chunk-yg8ckc3g.js.map → chunk-88zkpg89.js.map} +0 -0
- /package/dist/cli/{chunk-ppa6mg4d.js.map → chunk-8fm3b4vm.js.map} +0 -0
- /package/dist/cli/{chunk-rtvgmg2j.js.map → chunk-8h2t2vyj.js.map} +0 -0
- /package/dist/cli/{chunk-2ssh60qh.js.map → chunk-982qfeny.js.map} +0 -0
- /package/dist/cli/{chunk-t2h8pwy1.js.map → chunk-9emfspcp.js.map} +0 -0
- /package/dist/cli/{chunk-9jqmq5hv.js.map → chunk-akj2p7k1.js.map} +0 -0
- /package/dist/cli/{chunk-beb0z983.js.map → chunk-b664x65a.js.map} +0 -0
- /package/dist/cli/{chunk-kmswjxj9.js.map → chunk-b7n58hgj.js.map} +0 -0
- /package/dist/cli/{chunk-fnxgtjeq.js.map → chunk-c0qddrxr.js.map} +0 -0
- /package/dist/cli/{chunk-p834rkcn.js.map → chunk-g62vnvaw.js.map} +0 -0
- /package/dist/cli/{chunk-40vk8kgf.js.map → chunk-p4fe7tax.js.map} +0 -0
- /package/dist/cli/{chunk-34wap3ed.js.map → chunk-psg2za5z.js.map} +0 -0
- /package/dist/cli/{chunk-h0qwdbem.js.map → chunk-r5ajrw1g.js.map} +0 -0
- /package/dist/cli/{chunk-69rjf786.js.map → chunk-ry2ymbxf.js.map} +0 -0
- /package/dist/cli/{chunk-gxhcf2hs.js.map → chunk-t43g7v22.js.map} +0 -0
- /package/dist/cli/{chunk-ydqzkhjn.js.map → chunk-x0jqyq1g.js.map} +0 -0
- /package/dist/cli/{chunk-aafhwx74.js.map → chunk-x25s4gph.js.map} +0 -0
- /package/dist/cli/{chunk-nqna8dzp.js.map → chunk-xkx216rd.js.map} +0 -0
- /package/dist/cli/{chunk-etckvbrh.js.map → chunk-xv8nkt56.js.map} +0 -0
- /package/dist/cli/{chunk-hn3g2b89.js.map → chunk-yrmw5n2b.js.map} +0 -0
- /package/dist/cli/{chunk-4zxh25hc.js.map → chunk-zgca0vxs.js.map} +0 -0
- /package/dist/cli/{chunk-c5bjxq8v.js.map → chunk-zj6bcx0n.js.map} +0 -0
- /package/dist/cli/{chunk-1ch3ct3r.js.map → chunk-zjfwzrvd.js.map} +0 -0
- /package/dist/cli/{chunk-j4nfgn2j.js.map → chunk-znwhf37a.js.map} +0 -0
package/src/ai/relative-links.ts
CHANGED
|
@@ -11,12 +11,15 @@ import { localePrefix } from "../core/i18n.ts";
|
|
|
11
11
|
import {
|
|
12
12
|
buildFileRouteIndex,
|
|
13
13
|
isIndexFileName,
|
|
14
|
+
resolveRawRelativeHref,
|
|
14
15
|
resolveRelativeHref,
|
|
15
16
|
resolveRootFileHref,
|
|
17
|
+
routeOfLinkedEntry,
|
|
16
18
|
routeOfLinkedFile,
|
|
17
19
|
} from "../core/links.ts";
|
|
18
|
-
import type { RelativeLinkBase } from "../core/links.ts";
|
|
20
|
+
import type { FileRouteIndex, RelativeLinkBase } from "../core/links.ts";
|
|
19
21
|
import { localizeHref, servesRoute } from "../core/locale-links.ts";
|
|
22
|
+
import type { LocalizeHrefOptions } from "../core/locale-links.ts";
|
|
20
23
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
21
24
|
import { resolveDocsCollection } from "../core/sources/collection.ts";
|
|
22
25
|
import { extractLinks } from "../core/sources/normalize.ts";
|
|
@@ -28,7 +31,9 @@ import { extractLinks } from "../core/sources/normalize.ts";
|
|
|
28
31
|
* link lands on `/install`; the rendered page rewrites it to the route it
|
|
29
32
|
* means (`markdown/relative-links.ts`), and the Markdown an agent reads gets
|
|
30
33
|
* the same rewrite from the same resolver (`resolveRelativeHref`), so both
|
|
31
|
-
* point at one page.
|
|
34
|
+
* point at one page. A raw HTML `<a href>` ships as written, so a relative
|
|
35
|
+
* one lands where the browser takes it from the page's URL
|
|
36
|
+
* (`resolveRawRelativeHref`). Root-relative links (`[x](/guide)`,
|
|
32
37
|
* `<Card href="/guide">`, ``) get what the rendered page
|
|
33
38
|
* gives them too (`markdown/base-links.ts`,
|
|
34
39
|
* `components/content/base-href.ts`): a page link the `deployment.base` +
|
|
@@ -40,10 +45,17 @@ import { extractLinks } from "../core/sources/normalize.ts";
|
|
|
40
45
|
* (`resolveRootFileHref`). Inline links, images, component
|
|
41
46
|
* `href`s, and reference definitions are rewritten; fenced and inline code,
|
|
42
47
|
* relative images and asset links, and external URLs are left as written.
|
|
48
|
+
* The assistant's snapshot takes the links that name a page by its path or
|
|
49
|
+
* file alone, as the routes it keys pages by (`assistant`).
|
|
43
50
|
*/
|
|
44
51
|
|
|
45
52
|
/** The page a Markdown text was written for. */
|
|
46
53
|
export interface LinkedPage {
|
|
54
|
+
/**
|
|
55
|
+
* A staged page's path under its staging dir, which its links resolve from
|
|
56
|
+
* when it has no `sourcePath` (a remote or CMS source).
|
|
57
|
+
*/
|
|
58
|
+
entryId?: string;
|
|
47
59
|
route: string;
|
|
48
60
|
sourcePath?: string;
|
|
49
61
|
}
|
|
@@ -51,6 +63,20 @@ export interface LinkedPage {
|
|
|
51
63
|
/** Rewrite a page's relative page links to the routes they mean. */
|
|
52
64
|
export type RelativeLinkRewriter = (text: string, page: LinkedPage) => string;
|
|
53
65
|
|
|
66
|
+
/** Who reads the rewritten links. */
|
|
67
|
+
export interface RelativeLinkOptions {
|
|
68
|
+
/**
|
|
69
|
+
* The assistant's snapshot, rather than a page an agent fetches by URL.
|
|
70
|
+
* Only a link that names a page by its relative path or its content file
|
|
71
|
+
* changes, to the route the snapshot keys that page by: without the
|
|
72
|
+
* `deployment.base`, since the chat panel mounts every link it renders
|
|
73
|
+
* under it, and a locale's fallback copy as the page it renders, since the
|
|
74
|
+
* snapshot holds that page instead. Root page links and images stay as
|
|
75
|
+
* written.
|
|
76
|
+
*/
|
|
77
|
+
assistant?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
/**
|
|
55
81
|
* Whether a text could hold a relative page link at all: an inline link, a
|
|
56
82
|
* reference definition, or a component `href` whose target isn't a URL with
|
|
@@ -123,16 +149,79 @@ const collectSplices = (
|
|
|
123
149
|
return splices;
|
|
124
150
|
};
|
|
125
151
|
|
|
152
|
+
/** What a project's links resolve through, whoever reads them. */
|
|
153
|
+
interface LinkIndex {
|
|
154
|
+
fileRoutes: FileRouteIndex;
|
|
155
|
+
localeByRoute: Map<string, string>;
|
|
156
|
+
navPathBySource: Map<string | undefined, string>;
|
|
157
|
+
routes: Set<string>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The mirrors, llms-full.txt and the assistant's snapshot each build a
|
|
161
|
+
// rewriter for the same project, so the lookups behind them are built once.
|
|
162
|
+
const linkIndexes = new WeakMap<BlumeProject, LinkIndex>();
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The file → route lookup a `.md`/`.mdx` link resolves through, and the
|
|
166
|
+
* routes a link can land on. A file shared by every locale publishes once
|
|
167
|
+
* per locale, and the default locale's route stands for it; fallback copies
|
|
168
|
+
* render another locale's file, so they never stand for one.
|
|
169
|
+
*/
|
|
170
|
+
const linkIndex = (project: BlumeProject): LinkIndex => {
|
|
171
|
+
let index = linkIndexes.get(project);
|
|
172
|
+
if (!index) {
|
|
173
|
+
const { config, graph, manifest } = project;
|
|
174
|
+
index = {
|
|
175
|
+
fileRoutes: buildFileRouteIndex(graph.pages, config.i18n ?? null),
|
|
176
|
+
localeByRoute: new Map(
|
|
177
|
+
manifest.routes.map((route) => [route.path, route.locale])
|
|
178
|
+
),
|
|
179
|
+
navPathBySource: new Map(
|
|
180
|
+
graph.pages.map((page) => [page.sourcePath, page.navPath])
|
|
181
|
+
),
|
|
182
|
+
routes: new Set(manifest.routes.map((route) => route.path)),
|
|
183
|
+
};
|
|
184
|
+
linkIndexes.set(project, index);
|
|
185
|
+
}
|
|
186
|
+
return index;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A resolved route as the assistant's snapshot keys it. A locale's fallback
|
|
191
|
+
* copy (`/fr/guides/install` with no French page there) renders another
|
|
192
|
+
* locale's page, which the snapshot holds in its place, so the route becomes
|
|
193
|
+
* that page's; every other route is kept, `?query#hash` and all.
|
|
194
|
+
*/
|
|
195
|
+
const snapshotRoute = (project: BlumeProject): ((route: string) => string) => {
|
|
196
|
+
const pageRoutes = new Map(
|
|
197
|
+
project.graph.pages.map((page) => [page.id, page.route])
|
|
198
|
+
);
|
|
199
|
+
const rendered = new Map<string, string>();
|
|
200
|
+
for (const route of project.manifest.routes) {
|
|
201
|
+
const page = route.fallback ? pageRoutes.get(route.id) : undefined;
|
|
202
|
+
if (page !== undefined) {
|
|
203
|
+
rendered.set(route.path, page);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return (route) => {
|
|
207
|
+
const suffixAt = route.search(/[?#]/u);
|
|
208
|
+
const path = suffixAt === -1 ? route : route.slice(0, suffixAt);
|
|
209
|
+
const page = rendered.get(path);
|
|
210
|
+
return page === undefined ? route : `${page}${route.slice(path.length)}`;
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
126
214
|
/**
|
|
127
|
-
* Build the rewriter for a project
|
|
128
|
-
*
|
|
129
|
-
* publishes once per locale, and the default locale's route stands for it;
|
|
130
|
-
* fallback copies render another locale's file, so they never stand for one.
|
|
215
|
+
* Build the rewriter for a project, for an agent reading a page by URL or,
|
|
216
|
+
* with `assistant`, for the assistant's snapshot.
|
|
131
217
|
*/
|
|
132
218
|
export const relativeLinkRewriter = (
|
|
133
|
-
project: BlumeProject
|
|
219
|
+
project: BlumeProject,
|
|
220
|
+
options: RelativeLinkOptions = {}
|
|
134
221
|
): RelativeLinkRewriter => {
|
|
135
222
|
const { basePath, deployment, i18n } = project.config;
|
|
223
|
+
// Links are read under the real deployment base whoever reads them, so a
|
|
224
|
+
// root link to a content file that carries it is still recognized.
|
|
136
225
|
const deployBase = normalizeBasePath(deployment.options.base);
|
|
137
226
|
const contentRoot = resolveDocsCollection(
|
|
138
227
|
project.config,
|
|
@@ -141,17 +230,38 @@ export const relativeLinkRewriter = (
|
|
|
141
230
|
const localeTokens = i18n
|
|
142
231
|
? ["$", ...i18n.locales.map((locale) => locale.code)]
|
|
143
232
|
: [];
|
|
144
|
-
const fileRoutes
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
233
|
+
const { fileRoutes, localeByRoute, navPathBySource, routes } =
|
|
234
|
+
linkIndex(project);
|
|
235
|
+
const hasRoute = (path: string): boolean => routes.has(path);
|
|
236
|
+
|
|
237
|
+
// Where a resolved route lands for its reader. A route Blume serves takes
|
|
238
|
+
// the deployment base unconditionally — even over a route that starts with
|
|
239
|
+
// the base's name — except in the assistant's snapshot.
|
|
240
|
+
const land = options.assistant
|
|
241
|
+
? snapshotRoute(project)
|
|
242
|
+
: (route: string): string => mountBasePath(deployBase, route);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* How a page's root-relative links change, if they do: when the site is
|
|
246
|
+
* mounted under a prefix, or the page reads in a prefixed locale, whose
|
|
247
|
+
* copy of a route they move to. The assistant's snapshot keeps them as
|
|
248
|
+
* written.
|
|
249
|
+
*/
|
|
250
|
+
const rootLinksOf = (
|
|
251
|
+
route: string
|
|
252
|
+
): { localize?: LocalizeHrefOptions } | undefined => {
|
|
253
|
+
if (options.assistant) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const locale = localeByRoute.get(route);
|
|
257
|
+
const localize =
|
|
258
|
+
i18n && locale !== undefined && localePrefix(locale, i18n) !== ""
|
|
259
|
+
? { basePath, deployBase, i18n, locale, routes }
|
|
260
|
+
: undefined;
|
|
261
|
+
return deployBase !== "" || basePath !== "" || localize !== undefined
|
|
262
|
+
? { localize }
|
|
263
|
+
: undefined;
|
|
264
|
+
};
|
|
155
265
|
|
|
156
266
|
/**
|
|
157
267
|
* A page's relative-link resolver; `undefined` when the page has no file
|
|
@@ -160,43 +270,36 @@ export const relativeLinkRewriter = (
|
|
|
160
270
|
const relativeRoute = (
|
|
161
271
|
page: LinkedPage
|
|
162
272
|
): ((target: string) => string | undefined) | undefined => {
|
|
163
|
-
const { sourcePath } = page;
|
|
164
|
-
|
|
273
|
+
const { entryId, sourcePath } = page;
|
|
274
|
+
const file = sourcePath ?? entryId;
|
|
275
|
+
if (!file) {
|
|
165
276
|
return;
|
|
166
277
|
}
|
|
167
278
|
const from: RelativeLinkBase = {
|
|
168
|
-
isIndex: isIndexFileName(basename(
|
|
279
|
+
isIndex: isIndexFileName(basename(file), localeTokens),
|
|
169
280
|
route: page.route,
|
|
170
281
|
};
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
|
|
282
|
+
// A staged page (a remote or CMS source) has no file on disk: it
|
|
283
|
+
// resolves a sibling by its entry id, as `blume validate` does.
|
|
284
|
+
let resolveFile: (path: string) => string | undefined;
|
|
285
|
+
if (sourcePath) {
|
|
286
|
+
const navPath = navPathBySource.get(sourcePath) ?? basename(sourcePath);
|
|
287
|
+
resolveFile = (path) =>
|
|
288
|
+
routeOfLinkedFile(fileRoutes, { navPath, sourcePath }, path);
|
|
289
|
+
} else {
|
|
290
|
+
resolveFile = (path) => routeOfLinkedEntry(fileRoutes, file, path);
|
|
291
|
+
}
|
|
292
|
+
return (target) => resolveRelativeHref(target, from, resolveFile, hasRoute);
|
|
182
293
|
};
|
|
183
294
|
|
|
184
295
|
/** Whether a page is served at a based, fragment-less path. */
|
|
185
296
|
const servesPage = (route: string): boolean => servesRoute(routes, route);
|
|
186
297
|
|
|
187
298
|
return (text, page) => {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
const localize =
|
|
191
|
-
i18n && locale !== undefined && localePrefix(locale, i18n) !== ""
|
|
192
|
-
? { basePath, deployBase, i18n, locale, routes }
|
|
193
|
-
: undefined;
|
|
194
|
-
// A root-relative link only changes when the site is mounted under a
|
|
195
|
-
// prefix or the page reads in a prefixed locale.
|
|
196
|
-
const rootLinks =
|
|
197
|
-
deployBase !== "" || basePath !== "" || localize !== undefined;
|
|
299
|
+
const rootLinks = rootLinksOf(page.route);
|
|
300
|
+
const localize = rootLinks?.localize;
|
|
198
301
|
if (
|
|
199
|
-
!(page.sourcePath || rootLinks) ||
|
|
302
|
+
!(page.sourcePath || page.entryId || rootLinks) ||
|
|
200
303
|
!(
|
|
201
304
|
(rootLinks ? MAYBE_PAGE_LINK : MAYBE_RELATIVE).test(text) ||
|
|
202
305
|
MAYBE_ROOT_FILE.test(text)
|
|
@@ -230,20 +333,24 @@ export const relativeLinkRewriter = (
|
|
|
230
333
|
if (route === undefined) {
|
|
231
334
|
return undefined;
|
|
232
335
|
}
|
|
233
|
-
const
|
|
234
|
-
return localize ? localizeHref(
|
|
336
|
+
const landed = land(route);
|
|
337
|
+
return localize ? localizeHref(landed, localize) : landed;
|
|
235
338
|
};
|
|
236
339
|
const routed = (target: string, raw: boolean): string | undefined => {
|
|
237
340
|
if (!isInternalPath(target)) {
|
|
238
|
-
|
|
341
|
+
const route = raw
|
|
342
|
+
? resolveRawRelativeHref(target, page.route, hasRoute)
|
|
343
|
+
: relative?.(target);
|
|
344
|
+
return route === undefined ? undefined : land(route);
|
|
239
345
|
}
|
|
240
|
-
|
|
346
|
+
const file = raw ? undefined : rootFile(target);
|
|
347
|
+
return file ?? (rootLinks ? rooted(target) : undefined);
|
|
241
348
|
};
|
|
242
349
|
// An image is always a file: `public/` (or a generated asset endpoint),
|
|
243
350
|
// served under the deployment base but never `basePath`. A relative one
|
|
244
351
|
// is left to `core/content-assets.ts`.
|
|
245
352
|
const imaged = (target: string): string | undefined => {
|
|
246
|
-
const based = withBasePath(deployBase, target);
|
|
353
|
+
const based = rootLinks ? withBasePath(deployBase, target) : target;
|
|
247
354
|
return based === target ? undefined : based;
|
|
248
355
|
};
|
|
249
356
|
|
package/src/ai/site-skill.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { NavNode, PageRecord } from "../core/types.ts";
|
|
|
14
14
|
import { resolveReferences } from "../openapi/references.ts";
|
|
15
15
|
import { docsPhrase } from "./ai-catalog.ts";
|
|
16
16
|
import { API_PAGES_PATH } from "./api/paths.ts";
|
|
17
|
+
import { linkText } from "./link-text.ts";
|
|
17
18
|
import { asciiSlugify } from "./mcp/discovery.ts";
|
|
18
19
|
import {
|
|
19
20
|
SKILL_DESCRIPTION_MAX,
|
|
@@ -54,10 +55,6 @@ export const siteSkillName = (project: BlumeProject): string => {
|
|
|
54
55
|
return name.slice(0, NAME_MAX).replace(/-+$/u, "");
|
|
55
56
|
};
|
|
56
57
|
|
|
57
|
-
/** A title as Markdown link text (brackets escaped, as in llms.txt). */
|
|
58
|
-
const linkText = (title: string): string =>
|
|
59
|
-
title.replaceAll(/[\\[\]]/gu, String.raw`\$&`);
|
|
60
|
-
|
|
61
58
|
/**
|
|
62
59
|
* Current-version, default-locale pages worth listing: not API operations,
|
|
63
60
|
* and not changelog entries, which the changelog index covers in one line.
|
package/src/astro/generate.ts
CHANGED
|
@@ -88,6 +88,7 @@ import type { Diagnostic, Navigation } from "../core/types.ts";
|
|
|
88
88
|
import { getBlumeVersion } from "../core/version.ts";
|
|
89
89
|
import { servedFiles } from "../deploy/generated-files.ts";
|
|
90
90
|
import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
|
|
91
|
+
import { hreflangAlternates } from "../deploy/sitemap.ts";
|
|
91
92
|
import {
|
|
92
93
|
languageIconCss,
|
|
93
94
|
languageIconSlugsIn,
|
|
@@ -1204,6 +1205,8 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1204
1205
|
i18n?.defaultLocale
|
|
1205
1206
|
);
|
|
1206
1207
|
|
|
1208
|
+
const hreflang = hreflangAlternates(project);
|
|
1209
|
+
|
|
1207
1210
|
const navigationByLocale = i18n
|
|
1208
1211
|
? Object.fromEntries(
|
|
1209
1212
|
i18n.locales.map(({ code }) => [
|
|
@@ -1273,6 +1276,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1273
1276
|
}
|
|
1274
1277
|
: null,
|
|
1275
1278
|
imageZoom: config.markdown.imageZoom,
|
|
1279
|
+
jsonLd: config.seo.jsonLd,
|
|
1276
1280
|
logo,
|
|
1277
1281
|
mcp: config.agents.mcp.enabled
|
|
1278
1282
|
? {
|
|
@@ -1293,6 +1297,7 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1293
1297
|
og: {
|
|
1294
1298
|
description: resolveOgDescription(config),
|
|
1295
1299
|
enabled: config.seo.og.enabled ?? false,
|
|
1300
|
+
image: config.seo.og.image,
|
|
1296
1301
|
logo: ogLogo,
|
|
1297
1302
|
palette: config.seo.og.palette,
|
|
1298
1303
|
site: resolveOgSite(config),
|
|
@@ -1353,12 +1358,14 @@ export const buildRuntimeData = (project: BlumeProject): string => {
|
|
|
1353
1358
|
entryId: route.entryId,
|
|
1354
1359
|
fallback: route.fallback ?? false,
|
|
1355
1360
|
hidden: route.hidden,
|
|
1361
|
+
hreflang: hreflang.get(route.path) ?? [],
|
|
1356
1362
|
id: route.id,
|
|
1357
1363
|
indexable: route.indexable,
|
|
1358
1364
|
lastModified: route.lastModified ?? null,
|
|
1359
1365
|
locale: route.locale,
|
|
1360
1366
|
monolingual: route.monolingual ?? false,
|
|
1361
1367
|
path: route.path,
|
|
1368
|
+
published: route.published ?? null,
|
|
1362
1369
|
title: route.title,
|
|
1363
1370
|
version: route.version,
|
|
1364
1371
|
versionAlternates: route.versionAlternates,
|
package/src/astro/templates.ts
CHANGED
|
@@ -2512,6 +2512,7 @@ import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
|
2512
2512
|
import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
|
|
2513
2513
|
import { mountBasePath, stripBasePath } from "blume/core/base-path.ts";
|
|
2514
2514
|
import { routeSetFor, servesRoute } from "blume/core/locale-links.ts";
|
|
2515
|
+
import { resolveLocalizable } from "blume/core/localizable.ts";
|
|
2515
2516
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
2516
2517
|
${componentImports}
|
|
2517
2518
|
import data from "blume:data";
|
|
@@ -2536,8 +2537,10 @@ export function getStaticPaths() {
|
|
|
2536
2537
|
editUrl: route.editUrl,
|
|
2537
2538
|
entryId: route.entryId,
|
|
2538
2539
|
fallback: route.fallback,
|
|
2540
|
+
hreflang: route.hreflang,
|
|
2539
2541
|
indexable: route.indexable,
|
|
2540
2542
|
lastModified: route.lastModified,
|
|
2543
|
+
published: route.published,
|
|
2541
2544
|
locale: route.locale,
|
|
2542
2545
|
monolingual: route.monolingual,
|
|
2543
2546
|
route: route.path,
|
|
@@ -2548,7 +2551,7 @@ export function getStaticPaths() {
|
|
|
2548
2551
|
}));
|
|
2549
2552
|
}
|
|
2550
2553
|
|
|
2551
|
-
const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback, monolingual, version, versionAlternates } = Astro.props;
|
|
2554
|
+
const { entryId, collection, route, title, indexable, editUrl, lastModified, published, locale, alternates, hreflang, fallback, monolingual, version, versionAlternates } = Astro.props;
|
|
2552
2555
|
const entry = await getEntry(collection as CollectionKey, entryId);
|
|
2553
2556
|
if (!entry) {
|
|
2554
2557
|
return new Response(null, { status: 404 });
|
|
@@ -2598,19 +2601,23 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
|
2598
2601
|
const ogPath = data.config.og.enabled
|
|
2599
2602
|
? encodeURI(\`/og/\${route === "/" ? "index" : route.slice(1)}.png\`)
|
|
2600
2603
|
: null;
|
|
2601
|
-
//
|
|
2602
|
-
// \`site + base + path\`.
|
|
2604
|
+
// Root-relative paths carry the deployment base (the page is served under it),
|
|
2605
|
+
// and the site too once it's known: \`site + base + path\`. \`seo.image\` may be
|
|
2603
2606
|
// an external URL, which passes through verbatim (mirrors PageLayout). An
|
|
2604
|
-
// authored
|
|
2605
|
-
//
|
|
2607
|
+
// authored image keeps a base written by hand; the generated card is always
|
|
2608
|
+
// mounted under it.
|
|
2606
2609
|
const absoluteOg = (path: string, based: (path: string) => string) =>
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
+
path.startsWith("/") ? \`\${base ?? ""}\${based(path)}\` : path;
|
|
2611
|
+
// The page's own image, else the site's default for its locale.
|
|
2612
|
+
const selectedOgImage =
|
|
2613
|
+
seo.image ??
|
|
2614
|
+
(data.config.og.image && resolveLocalizable(data.config.og.image, locale, data.config.i18n?.defaultLocale));
|
|
2615
|
+
const ogImage = selectedOgImage
|
|
2616
|
+
? absoluteOg(selectedOgImage, withBase)
|
|
2610
2617
|
: ogPath && absoluteOg(ogPath, withMountedBase);
|
|
2611
|
-
// Blume's generated card has known dimensions the layout can declare;
|
|
2612
|
-
//
|
|
2613
|
-
const ogGenerated = !
|
|
2618
|
+
// Blume's generated card has known dimensions the layout can declare; an
|
|
2619
|
+
// authored image could be any size or format, so it gets none.
|
|
2620
|
+
const ogGenerated = !selectedOgImage && Boolean(ogPath);
|
|
2614
2621
|
|
|
2615
2622
|
// X attribution: the site's account, plus a creator the page can claim for
|
|
2616
2623
|
// itself (a guest post crediting its own author) over the configured default.
|
|
@@ -2706,11 +2713,12 @@ const canonical =
|
|
|
2706
2713
|
: null);
|
|
2707
2714
|
const effectiveNoindex = Boolean(seo.noindex) || (archived?.noindex ?? false);
|
|
2708
2715
|
|
|
2716
|
+
// hreflang names only indexable translations (see \`hreflangAlternates\`).
|
|
2709
2717
|
const localeAlternates =
|
|
2710
2718
|
i18n && base
|
|
2711
|
-
? (
|
|
2719
|
+
? (hreflang ?? []).map((alt) => ({ hreflang: alt.locale, href: absolute(alt.path) }))
|
|
2712
2720
|
: [];
|
|
2713
|
-
const defaultAlt = i18n ? (
|
|
2721
|
+
const defaultAlt = i18n ? (hreflang ?? []).find((alt) => alt.locale === i18n.defaultLocale) : null;
|
|
2714
2722
|
const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
|
|
2715
2723
|
|
|
2716
2724
|
// \`route\` arrives with \`basePath\` already applied, so the locale segment it
|
|
@@ -2874,7 +2882,8 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2874
2882
|
pageType={frontmatter.type}
|
|
2875
2883
|
apiRail={remarkPluginFrontmatter?.${API_RAIL_KEY} === true}
|
|
2876
2884
|
pageMode={frontmatter.mode}
|
|
2877
|
-
published={frontmatter.date ?? frontmatter.changelog?.date ??
|
|
2885
|
+
published={frontmatter.date ?? frontmatter.changelog?.date ?? published}
|
|
2886
|
+
authors={frontmatter.authors}
|
|
2878
2887
|
lastModified={lastModified}
|
|
2879
2888
|
noindex={effectiveNoindex}
|
|
2880
2889
|
structuredDataEnabled={data.config.structuredData}
|
|
@@ -2922,8 +2931,9 @@ export const changelogIndexTemplate = (options: {
|
|
|
2922
2931
|
// Generated by Blume. Do not edit.
|
|
2923
2932
|
import RootLayout from "blume/components/layout/RootLayout.astro";
|
|
2924
2933
|
import Changelog from "blume/components/content/Changelog.astro";
|
|
2925
|
-
import { withMountedBase } from "blume/components/islands/base-path.ts";
|
|
2934
|
+
import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
|
|
2926
2935
|
import { resolveSlot } from "blume/components/layout/overrides.ts";
|
|
2936
|
+
import { resolveLocalizable } from "blume/core/localizable.ts";
|
|
2927
2937
|
import { layoutOverrides } from "../generated/components.ts";
|
|
2928
2938
|
import data from "blume:data";
|
|
2929
2939
|
|
|
@@ -2945,11 +2955,17 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
|
|
|
2945
2955
|
const basedRoute = withMountedBase("/changelog");
|
|
2946
2956
|
const canonical = base ? base + basedRoute : null;
|
|
2947
2957
|
|
|
2948
|
-
// The generated OG card for this route (the /og
|
|
2949
|
-
// the content-route cards), absolutized like the
|
|
2950
|
-
// a full URL when the site is known.
|
|
2958
|
+
// The site's default image, else the generated OG card for this route (the /og
|
|
2959
|
+
// endpoint emits it alongside the content-route cards), absolutized like the
|
|
2960
|
+
// catch-all's so crawlers get a full URL when the site is known.
|
|
2961
|
+
const defaultOgImage =
|
|
2962
|
+
data.config.og.image && resolveLocalizable(data.config.og.image, htmlLang, i18n?.defaultLocale);
|
|
2951
2963
|
const ogPath = data.config.og.enabled ? withMountedBase("/og/changelog.png") : null;
|
|
2952
|
-
const ogImage =
|
|
2964
|
+
const ogImage = defaultOgImage
|
|
2965
|
+
? defaultOgImage.startsWith("/")
|
|
2966
|
+
? (base ?? "") + withBase(defaultOgImage)
|
|
2967
|
+
: defaultOgImage
|
|
2968
|
+
: ogPath && base ? base + ogPath : ogPath;
|
|
2953
2969
|
|
|
2954
2970
|
// The page chrome (h1, title, description) comes from the translatable
|
|
2955
2971
|
// \`changelog\` group, which carries the \`changelog\` config's title and
|
|
@@ -2997,7 +3013,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
|
|
|
2997
3013
|
searchEnabled={data.config.search.enabled}
|
|
2998
3014
|
indexable={true}
|
|
2999
3015
|
ogImage={ogImage}
|
|
3000
|
-
ogGenerated={Boolean(ogImage)}
|
|
3016
|
+
ogGenerated={!defaultOgImage && Boolean(ogImage)}
|
|
3001
3017
|
x={data.config.x}
|
|
3002
3018
|
canonical={canonical}
|
|
3003
3019
|
exportPdf={${options.exportPdf}}
|
|
@@ -41,6 +41,7 @@ import { normalizeXHandle } from "../../seo/x-handle.ts";
|
|
|
41
41
|
import Icon from "../Icon.astro";
|
|
42
42
|
import { stripBase, withBase, withMountedBase } from "../islands/base-path.ts";
|
|
43
43
|
import { routeSetFor } from "../../core/locale-links.ts";
|
|
44
|
+
import { resolveLocalizable } from "../../core/localizable.ts";
|
|
44
45
|
import {
|
|
45
46
|
browserLanguageTargets,
|
|
46
47
|
LOCALE_REDIRECT_SCRIPT,
|
|
@@ -265,20 +266,30 @@ const resolvedCanonical =
|
|
|
265
266
|
(siteBase
|
|
266
267
|
? `${siteBase}${basedRoute === "/" ? "/" : encodeURI(basedRoute)}`
|
|
267
268
|
: null);
|
|
268
|
-
// An explicit `ogImage` wins
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
269
|
+
// An explicit `ogImage` wins, then the site's default image for this locale. A
|
|
270
|
+
// root-relative path (e.g. an image dropped in `public/`) is served under the
|
|
271
|
+
// deployment base and resolved against the site URL so crawlers get an
|
|
272
|
+
// absolute `og:image`; an already-absolute URL passes through untouched.
|
|
273
|
+
// Otherwise fall back to the generated OG card for this route.
|
|
272
274
|
const absolutizeOgImage = (value: string): string =>
|
|
273
|
-
value.startsWith("/")
|
|
274
|
-
const
|
|
275
|
-
|
|
275
|
+
value.startsWith("/") ? `${siteBase ?? ""}${withBase(value)}` : value;
|
|
276
|
+
const selectedOgImage =
|
|
277
|
+
ogImage ??
|
|
278
|
+
(data.config.og.image &&
|
|
279
|
+
resolveLocalizable(
|
|
280
|
+
data.config.og.image,
|
|
281
|
+
locale,
|
|
282
|
+
data.config.i18n?.defaultLocale
|
|
283
|
+
));
|
|
284
|
+
const resolvedOgImage = selectedOgImage
|
|
285
|
+
? absolutizeOgImage(selectedOgImage)
|
|
276
286
|
: ogEnabled && ogCardGenerated && siteBase
|
|
277
287
|
? `${siteBase}${encodeURI(withMountedBase(`/og/${ogSlug}.png`))}`
|
|
278
288
|
: null;
|
|
279
289
|
// Only the generated card has a known size and format, so its dimensions are
|
|
280
|
-
// declared for crawlers;
|
|
281
|
-
|
|
290
|
+
// declared for crawlers; any other image could be any size. The size and alt
|
|
291
|
+
// props describe the page's own `ogImage`, never the site default.
|
|
292
|
+
const ogGenerated = !selectedOgImage && Boolean(resolvedOgImage);
|
|
282
293
|
// A page with no image still gets a card on X — the compact `summary` variant
|
|
283
294
|
// — rather than rendering as a bare link.
|
|
284
295
|
const twitterCard = resolvedOgImage ? "summary_large_image" : "summary";
|
|
@@ -297,6 +308,8 @@ const structuredData =
|
|
|
297
308
|
breadcrumbs: [],
|
|
298
309
|
description,
|
|
299
310
|
identity: data.config.identity,
|
|
311
|
+
jsonLd: data.config.jsonLd,
|
|
312
|
+
image: resolvedOgImage,
|
|
300
313
|
locale,
|
|
301
314
|
modified: lastModified,
|
|
302
315
|
route,
|
|
@@ -386,7 +399,7 @@ const localeRedirect = localeTargets
|
|
|
386
399
|
<meta content={pageTitle} property="og:image:alt" />
|
|
387
400
|
</>
|
|
388
401
|
)}
|
|
389
|
-
{
|
|
402
|
+
{ogImage && ogImageSize && (
|
|
390
403
|
<>
|
|
391
404
|
<meta
|
|
392
405
|
content={String(ogImageSize.width)}
|
|
@@ -398,12 +411,12 @@ const localeRedirect = localeTargets
|
|
|
398
411
|
/>
|
|
399
412
|
</>
|
|
400
413
|
)}
|
|
401
|
-
{
|
|
414
|
+
{ogImage && ogImageAlt && (
|
|
402
415
|
<meta content={ogImageAlt} property="og:image:alt" />
|
|
403
416
|
)}
|
|
404
417
|
<meta content={resolvedOgImage} name="twitter:image" />
|
|
405
418
|
{ogGenerated && <meta content={pageTitle} name="twitter:image:alt" />}
|
|
406
|
-
{
|
|
419
|
+
{ogImage && ogImageAlt && (
|
|
407
420
|
<meta content={ogImageAlt} name="twitter:image:alt" />
|
|
408
421
|
)}
|
|
409
422
|
</>
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
OG_IMAGE_WIDTH,
|
|
44
44
|
} from "../../og/dimensions.ts";
|
|
45
45
|
import { buildStructuredData, toIso } from "../../seo/jsonld.ts";
|
|
46
|
+
import type { AuthorInput } from "../../seo/jsonld.ts";
|
|
46
47
|
import { normalizeXHandle } from "../../seo/x-handle.ts";
|
|
47
48
|
import { withBase, withMountedBase } from "../islands/base-path.ts";
|
|
48
49
|
import { routeSetFor } from "../../core/locale-links.ts";
|
|
@@ -196,6 +197,8 @@ interface Props {
|
|
|
196
197
|
/** The page's `mode` frontmatter: what it shows around its content (see `core/page-modes.ts`). */
|
|
197
198
|
pageMode?: PageMode;
|
|
198
199
|
published?: string | Date | null;
|
|
200
|
+
/** The page's front matter `authors`, credited in its JSON-LD. */
|
|
201
|
+
authors?: AuthorInput | AuthorInput[];
|
|
199
202
|
lastModified?: string | null;
|
|
200
203
|
noindex?: boolean;
|
|
201
204
|
structuredDataEnabled?: boolean;
|
|
@@ -312,6 +315,7 @@ const {
|
|
|
312
315
|
apiRail = false,
|
|
313
316
|
pageMode,
|
|
314
317
|
published,
|
|
318
|
+
authors,
|
|
315
319
|
lastModified,
|
|
316
320
|
noindex,
|
|
317
321
|
structuredDataEnabled,
|
|
@@ -534,11 +538,14 @@ const structuredData =
|
|
|
534
538
|
structuredDataEnabled === false || noindex
|
|
535
539
|
? null
|
|
536
540
|
: buildStructuredData({
|
|
541
|
+
authors,
|
|
537
542
|
base: import.meta.env.BASE_URL,
|
|
538
543
|
breadcrumbs: crumbs,
|
|
539
544
|
description,
|
|
540
545
|
homeRoute: navigation.root,
|
|
541
546
|
identity: data.config.identity,
|
|
547
|
+
jsonLd: data.config.jsonLd,
|
|
548
|
+
image: ogImage,
|
|
542
549
|
locale,
|
|
543
550
|
modified: lastModified,
|
|
544
551
|
pageType,
|