@ampless/runtime 1.0.0-alpha.25 → 1.0.0-alpha.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +41 -1
- package/dist/index.js +91 -16
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -153,7 +153,37 @@ interface PluginHeadApi {
|
|
|
153
153
|
renderHead(): Promise<ReactNode>;
|
|
154
154
|
/** React children safe to drop just before `</body>`. */
|
|
155
155
|
renderBodyEnd(): Promise<ReactNode>;
|
|
156
|
+
/**
|
|
157
|
+
* Per-post body descriptors (Phase 4). Themes call this from their
|
|
158
|
+
* post template to render plugin-supplied `<script
|
|
159
|
+
* type="application/ld+json">` descriptors keyed off the specific
|
|
160
|
+
* post being viewed. Limited to `inlineScript` with
|
|
161
|
+
* `scriptType: 'application/ld+json'` so the per-post surface stays
|
|
162
|
+
* scoped to structured data — see `PublicPostBodyDescriptor`.
|
|
163
|
+
*/
|
|
164
|
+
renderBodyForPost(post: Post): Promise<ReactNode>;
|
|
156
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Escape characters that would let a value break out of an inline
|
|
168
|
+
* `<script type="application/ld+json">` body. Applied automatically by
|
|
169
|
+
* the runtime to ANY inlineScript descriptor whose `scriptType` is
|
|
170
|
+
* `'application/ld+json'`, regardless of which surface
|
|
171
|
+
* (`publicHead` / `publicBodyEnd` / `publicBodyForPost`) emitted it.
|
|
172
|
+
* Plugin authors do not need to call this themselves; it's exported so
|
|
173
|
+
* other hand-rolled inline-JSON-LD code paths can reuse it.
|
|
174
|
+
*
|
|
175
|
+
* Each character is replaced with its `\uXXXX` form — a JSON-legal way
|
|
176
|
+
* to encode the same character inside a JSON string, so the JSON
|
|
177
|
+
* payload still parses but the HTML parser can no longer see a closing
|
|
178
|
+
* `</script>` sequence:
|
|
179
|
+
*
|
|
180
|
+
* '<' → '\u003c'
|
|
181
|
+
* '>' → '\u003e'
|
|
182
|
+
* '&' → '\u0026'
|
|
183
|
+
* U+2028 → '\u2028'
|
|
184
|
+
* U+2029 → '\u2029'
|
|
185
|
+
*/
|
|
186
|
+
declare function escapeJsonLdInlineBody(value: string): string;
|
|
157
187
|
/**
|
|
158
188
|
* Create the head/body renderer for a `Config`. The constructor-time
|
|
159
189
|
* pass logs a single dev warning when two plugins share an
|
|
@@ -384,6 +414,16 @@ interface Ampless {
|
|
|
384
414
|
siteMetadata(): Promise<Metadata>;
|
|
385
415
|
publicHead(): Promise<ReactNode>;
|
|
386
416
|
publicBodyEnd(): Promise<ReactNode>;
|
|
417
|
+
/**
|
|
418
|
+
* Per-post body descriptors (Phase 4 `schema` capability). Theme
|
|
419
|
+
* post templates render the result so plugins like
|
|
420
|
+
* `@ampless/plugin-schema-jsonld` can emit
|
|
421
|
+
* `<script type="application/ld+json">` Article schema keyed off
|
|
422
|
+
* the specific post being viewed. Limited to inline-script
|
|
423
|
+
* descriptors with `scriptType: 'application/ld+json'` — the
|
|
424
|
+
* runtime auto-escapes `<`, `>`, `&`, U+2028, U+2029 in the body.
|
|
425
|
+
*/
|
|
426
|
+
publicBodyForPost(post: Post): Promise<ReactNode>;
|
|
387
427
|
renderBody(post: Post): string;
|
|
388
428
|
renderThemeCss(cssVars: Record<string, string>): string;
|
|
389
429
|
publicAssetUrl(key: string): string;
|
|
@@ -410,4 +450,4 @@ interface Ampless {
|
|
|
410
450
|
*/
|
|
411
451
|
declare function createAmpless(opts: CreateAmplessOpts): Ampless;
|
|
412
452
|
|
|
413
|
-
export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, createPluginHead, createPluginSettings, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
|
|
453
|
+
export { type Ampless, type AmplessOutputs, COLOR_SCHEME_SETTING_KEY, type ColorScheme, type CreateAmplessOpts, DEFAULT_COLOR_SCHEME, type DataOutput, type EffectiveSiteSettings, type EffectiveThemeConfig, type ListPostsByTagOptions, type ListPostsOptions, type ListPostsResult, type MediaApi, type PluginHeadApi, type PluginSettingsApi, type PluginSettingsSnapshot, type PostsApi, type PublicMediaShape, type PublicPostConnectionShape, type PublicPostShape, type ResolvedAssetMeta, type ResolvedMedia, type ResolvedTheme, type SeoApi, type SiteSettingsApi, type StorageOutput, type StreamS3Options, type ThemeActiveApi, type ThemeConfigApi, type ThemesRegistry, _resetStreamS3Cache, createAmpless, createPluginHead, createPluginSettings, escapeJsonLdInlineBody, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
|
package/dist/index.js
CHANGED
|
@@ -289,6 +289,42 @@ function applyAttrs(target, attrs, ownerLabel) {
|
|
|
289
289
|
target[k] = v;
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
+
function escapeJsonLdInlineBody(value) {
|
|
293
|
+
return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
294
|
+
}
|
|
295
|
+
function inlineScriptTypeAllowed(surface, scriptType) {
|
|
296
|
+
if (scriptType === "application/ld+json") return true;
|
|
297
|
+
if (scriptType === void 0) {
|
|
298
|
+
return surface === "head" || surface === "body-end";
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
function renderInlineScript(descriptor, pluginLabel, index, surface) {
|
|
303
|
+
if (!descriptor.id) {
|
|
304
|
+
warn(
|
|
305
|
+
`${pluginLabel}: inlineScript descriptor #${index} dropped \u2014 missing required "id".`
|
|
306
|
+
);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
if (!inlineScriptTypeAllowed(surface, descriptor.scriptType)) {
|
|
310
|
+
const got = descriptor.scriptType === void 0 ? "undefined" : `"${descriptor.scriptType}"`;
|
|
311
|
+
const allowed = surface === "body-for-post" ? `"application/ld+json" (required on publicBodyForPost \u2014 the per-post surface is scoped to JSON-LD)` : `undefined or "application/ld+json"`;
|
|
312
|
+
warn(
|
|
313
|
+
`${pluginLabel}: inlineScript "${descriptor.id}" dropped \u2014 scriptType ${got} not allowed on ${surface}. Allowed: ${allowed}.`
|
|
314
|
+
);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const body = descriptor.scriptType === "application/ld+json" ? escapeJsonLdInlineBody(descriptor.body) : descriptor.body;
|
|
318
|
+
const props = {
|
|
319
|
+
id: descriptor.id,
|
|
320
|
+
dangerouslySetInnerHTML: { __html: body }
|
|
321
|
+
};
|
|
322
|
+
if (descriptor.scriptType) props.type = descriptor.scriptType;
|
|
323
|
+
return {
|
|
324
|
+
id: descriptor.id,
|
|
325
|
+
element: createElement("script", props)
|
|
326
|
+
};
|
|
327
|
+
}
|
|
292
328
|
function renderHeadDescriptor(descriptor, pluginLabel, index) {
|
|
293
329
|
switch (descriptor.type) {
|
|
294
330
|
case "script": {
|
|
@@ -319,22 +355,8 @@ function renderHeadDescriptor(descriptor, pluginLabel, index) {
|
|
|
319
355
|
element: createElement("script", props)
|
|
320
356
|
};
|
|
321
357
|
}
|
|
322
|
-
case "inlineScript":
|
|
323
|
-
|
|
324
|
-
warn(
|
|
325
|
-
`${pluginLabel}: inlineScript descriptor #${index} dropped \u2014 missing required "id".`
|
|
326
|
-
);
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
const props = {
|
|
330
|
-
id: descriptor.id,
|
|
331
|
-
dangerouslySetInnerHTML: { __html: descriptor.body }
|
|
332
|
-
};
|
|
333
|
-
return {
|
|
334
|
-
id: descriptor.id,
|
|
335
|
-
element: createElement("script", props)
|
|
336
|
-
};
|
|
337
|
-
}
|
|
358
|
+
case "inlineScript":
|
|
359
|
+
return renderInlineScript(descriptor, pluginLabel, index, "head");
|
|
338
360
|
case "meta": {
|
|
339
361
|
const props = { content: descriptor.content };
|
|
340
362
|
if (descriptor.name) props.name = descriptor.name;
|
|
@@ -401,8 +423,20 @@ function renderBodyDescriptor(descriptor, pluginLabel, index) {
|
|
|
401
423
|
element: createElement("iframe", props)
|
|
402
424
|
};
|
|
403
425
|
}
|
|
426
|
+
if (descriptor.type === "inlineScript") {
|
|
427
|
+
return renderInlineScript(descriptor, pluginLabel, index, "body-end");
|
|
428
|
+
}
|
|
404
429
|
return renderHeadDescriptor(descriptor, pluginLabel, index);
|
|
405
430
|
}
|
|
431
|
+
function renderPostBodyDescriptor(descriptor, pluginLabel, index) {
|
|
432
|
+
if (descriptor.type !== "inlineScript") {
|
|
433
|
+
warn(
|
|
434
|
+
`${pluginLabel}: publicBodyForPost descriptor #${index} dropped \u2014 only inlineScript with scriptType "application/ld+json" is allowed on this surface.`
|
|
435
|
+
);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return renderInlineScript(descriptor, pluginLabel, index, "body-for-post");
|
|
439
|
+
}
|
|
406
440
|
function dedupeAndKey(entries) {
|
|
407
441
|
const lastIndexById = /* @__PURE__ */ new Map();
|
|
408
442
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -461,6 +495,31 @@ function collectFor(plugins, site, snapshot, surface, renderOne) {
|
|
|
461
495
|
const keyed = dedupeAndKey(entries);
|
|
462
496
|
return createElement(Fragment, null, ...keyed);
|
|
463
497
|
}
|
|
498
|
+
function collectForPost(plugins, site, snapshot, post) {
|
|
499
|
+
const entries = [];
|
|
500
|
+
for (const plugin of plugins) {
|
|
501
|
+
const factory = plugin.publicBodyForPost;
|
|
502
|
+
if (!factory) continue;
|
|
503
|
+
const ctx = makeCtx(plugin, site, snapshot);
|
|
504
|
+
let descriptors;
|
|
505
|
+
try {
|
|
506
|
+
descriptors = factory.call(plugin, post, ctx) ?? [];
|
|
507
|
+
} catch (err) {
|
|
508
|
+
warn(
|
|
509
|
+
`plugin "${plugin.instanceId ?? plugin.name}" threw inside publicBodyForPost callback: ${err instanceof Error ? err.message : String(err)}`
|
|
510
|
+
);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
514
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
515
|
+
const entry = renderPostBodyDescriptor(descriptors[i], label, i);
|
|
516
|
+
if (entry) entries.push(entry);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (entries.length === 0) return null;
|
|
520
|
+
const keyed = dedupeAndKey(entries);
|
|
521
|
+
return createElement(Fragment, null, ...keyed);
|
|
522
|
+
}
|
|
464
523
|
function createPluginHead(cmsConfig, pluginSettings) {
|
|
465
524
|
const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
|
|
466
525
|
const validPlugins = [];
|
|
@@ -512,6 +571,16 @@ function createPluginHead(cmsConfig, pluginSettings) {
|
|
|
512
571
|
`${label}: implements \`publicBodyEnd\` but "publicBody" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
513
572
|
);
|
|
514
573
|
}
|
|
574
|
+
if (caps.includes("schema") && !plugin.publicBodyForPost) {
|
|
575
|
+
warn(
|
|
576
|
+
`${label}: declares capability "schema" but no \`publicBodyForPost\` implementation. Drop the capability or add the function.`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
if (plugin.publicBodyForPost && !caps.includes("schema")) {
|
|
580
|
+
warn(
|
|
581
|
+
`${label}: implements \`publicBodyForPost\` but "schema" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
515
584
|
}
|
|
516
585
|
}
|
|
517
586
|
return {
|
|
@@ -534,6 +603,10 @@ function createPluginHead(cmsConfig, pluginSettings) {
|
|
|
534
603
|
(p) => p.publicBodyEnd,
|
|
535
604
|
renderBodyDescriptor
|
|
536
605
|
);
|
|
606
|
+
},
|
|
607
|
+
async renderBodyForPost(post) {
|
|
608
|
+
const snapshot = await pluginSettings.loadAll();
|
|
609
|
+
return collectForPost(validPlugins, cmsConfig.site, snapshot, post);
|
|
537
610
|
}
|
|
538
611
|
};
|
|
539
612
|
}
|
|
@@ -1025,6 +1098,7 @@ function createAmpless(opts) {
|
|
|
1025
1098
|
siteMetadata: () => seo.siteMetadata(),
|
|
1026
1099
|
publicHead: () => pluginHead.renderHead(),
|
|
1027
1100
|
publicBodyEnd: () => pluginHead.renderBodyEnd(),
|
|
1101
|
+
publicBodyForPost: (post) => pluginHead.renderBodyForPost(post),
|
|
1028
1102
|
renderBody: (post) => renderBody(post),
|
|
1029
1103
|
renderThemeCss: (cssVars) => renderThemeCss(cssVars),
|
|
1030
1104
|
publicAssetUrl: (key) => storage.publicAssetUrl(key),
|
|
@@ -1050,6 +1124,7 @@ export {
|
|
|
1050
1124
|
createAmpless,
|
|
1051
1125
|
createPluginHead,
|
|
1052
1126
|
createPluginSettings,
|
|
1127
|
+
escapeJsonLdInlineBody,
|
|
1053
1128
|
htmlToMarkdown,
|
|
1054
1129
|
markdownToHtml,
|
|
1055
1130
|
renderBody,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.27",
|
|
4
4
|
"description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"lucide-react": "^1.16.0",
|
|
50
50
|
"marked": "^18.0.4",
|
|
51
51
|
"tailwind-merge": "^3.6.0",
|
|
52
|
-
"ampless": "1.0.0-alpha.
|
|
53
|
-
"@ampless/plugin-og-image": "0.2.0-alpha.
|
|
52
|
+
"ampless": "1.0.0-alpha.21",
|
|
53
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.21"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": "^15 || ^16",
|