@ampless/runtime 1.0.0-alpha.22 → 1.0.0-alpha.24
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 +52 -2
- package/dist/index.js +360 -1
- package/dist/routes/index.d.ts +1 -0
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Post, MediaMetadata, ThemeModule, ThemeManifest
|
|
1
|
+
import { Post, MediaMetadata, Config, ThemeModule, ThemeManifest } from 'ampless';
|
|
2
2
|
export { Config, Post, ThemeManifest } from 'ampless';
|
|
3
3
|
import { Metadata } from 'next';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
4
5
|
import { getProperties } from 'aws-amplify/storage/server';
|
|
5
6
|
import { cookies } from 'next/headers';
|
|
6
7
|
|
|
@@ -122,6 +123,51 @@ interface SeoApi {
|
|
|
122
123
|
siteMetadata(): Promise<Metadata>;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Per-instance flat map of stored field values, keyed by the field's
|
|
128
|
+
* `key` (not the full `plugins.<instanceId>.<key>` SK). The keys
|
|
129
|
+
* here mirror the field manifest so callers can hand the map
|
|
130
|
+
* directly to `resolvePluginSettings`.
|
|
131
|
+
*/
|
|
132
|
+
type PluginSettingsSnapshot = Map<string, Record<string, unknown>>;
|
|
133
|
+
interface PluginSettingsApi {
|
|
134
|
+
/**
|
|
135
|
+
* Fetch all `plugins.*` settings from the public cache and bucket
|
|
136
|
+
* them by `instanceId`. Returns an empty Map on any failure mode
|
|
137
|
+
* (storage unconfigured, 404, JSON parse error) — callers fall
|
|
138
|
+
* back to manifest defaults so the layout never crashes when the
|
|
139
|
+
* cache is missing.
|
|
140
|
+
*/
|
|
141
|
+
loadAll(): Promise<PluginSettingsSnapshot>;
|
|
142
|
+
}
|
|
143
|
+
declare function createPluginSettings(storage: StorageApi): PluginSettingsApi;
|
|
144
|
+
|
|
145
|
+
interface PluginHeadApi {
|
|
146
|
+
/**
|
|
147
|
+
* React children safe to drop into `<head>`. Async because admin-
|
|
148
|
+
* managed settings are read from S3 on the first call per request.
|
|
149
|
+
* Within a single request both `renderHead` and `renderBodyEnd` share
|
|
150
|
+
* the same fetched snapshot via Next.js fetch dedup on the
|
|
151
|
+
* `site-settings` cache tag.
|
|
152
|
+
*/
|
|
153
|
+
renderHead(): Promise<ReactNode>;
|
|
154
|
+
/** React children safe to drop just before `</body>`. */
|
|
155
|
+
renderBodyEnd(): Promise<ReactNode>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create the head/body renderer for a `Config`. The constructor-time
|
|
159
|
+
* pass logs a single dev warning when two plugins share an
|
|
160
|
+
* `instanceId ?? name`; everything else happens at render time so
|
|
161
|
+
* descriptors reflect per-request site config.
|
|
162
|
+
*
|
|
163
|
+
* `pluginSettings` (Phase 2) is the runtime accessor that pulls
|
|
164
|
+
* admin-managed `settings.public` values from the S3 site-settings
|
|
165
|
+
* cache. Within a single request we fetch once via `loadAll()` and
|
|
166
|
+
* bind a per-plugin `ctx.setting(key)` accessor before invoking
|
|
167
|
+
* either `publicHead` or `publicBodyEnd`.
|
|
168
|
+
*/
|
|
169
|
+
declare function createPluginHead(cmsConfig: Config, pluginSettings: PluginSettingsApi): PluginHeadApi;
|
|
170
|
+
|
|
125
171
|
interface ThemesRegistry {
|
|
126
172
|
/** Map of theme name → loaded theme module. */
|
|
127
173
|
themes: Record<string, ThemeModule>;
|
|
@@ -336,6 +382,8 @@ interface Ampless {
|
|
|
336
382
|
loadThemeConfig(): Promise<EffectiveThemeConfig>;
|
|
337
383
|
postMetadata(post: Post): Promise<Metadata>;
|
|
338
384
|
siteMetadata(): Promise<Metadata>;
|
|
385
|
+
publicHead(): Promise<ReactNode>;
|
|
386
|
+
publicBodyEnd(): Promise<ReactNode>;
|
|
339
387
|
renderBody(post: Post): string;
|
|
340
388
|
renderThemeCss(cssVars: Record<string, string>): string;
|
|
341
389
|
publicAssetUrl(key: string): string;
|
|
@@ -350,6 +398,8 @@ interface Ampless {
|
|
|
350
398
|
readonly themeActive: ThemeActiveApi;
|
|
351
399
|
readonly themeConfig: ThemeConfigApi;
|
|
352
400
|
readonly storageApi: StorageApi;
|
|
401
|
+
readonly pluginHead: PluginHeadApi;
|
|
402
|
+
readonly pluginSettings: PluginSettingsApi;
|
|
353
403
|
}
|
|
354
404
|
/**
|
|
355
405
|
* Wire up the ampless runtime from user-supplied config blobs. The
|
|
@@ -360,4 +410,4 @@ interface Ampless {
|
|
|
360
410
|
*/
|
|
361
411
|
declare function createAmpless(opts: CreateAmplessOpts): Ampless;
|
|
362
412
|
|
|
363
|
-
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 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, htmlToMarkdown, markdownToHtml, renderBody, renderThemeCss, streamS3Object, streamS3ObjectWithRunner, tiptapToHtml, tiptapToMarkdown, validateColorScheme };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -225,6 +225,357 @@ function createSeo(cmsConfig, settingsApi) {
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// src/plugin-head.ts
|
|
229
|
+
import {
|
|
230
|
+
Fragment,
|
|
231
|
+
createElement
|
|
232
|
+
} from "react";
|
|
233
|
+
import {
|
|
234
|
+
isValidPluginKey,
|
|
235
|
+
resolvePluginSettings
|
|
236
|
+
} from "ampless";
|
|
237
|
+
function isPlugin2(p) {
|
|
238
|
+
return typeof p === "object" && p !== null && "apiVersion" in p;
|
|
239
|
+
}
|
|
240
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set([
|
|
241
|
+
"crossorigin",
|
|
242
|
+
"referrerpolicy",
|
|
243
|
+
"integrity",
|
|
244
|
+
"fetchpriority",
|
|
245
|
+
// `nonce` is intentionally NOT in the allowlist for Phase 1. CSP
|
|
246
|
+
// nonce propagation is scoped out of Phase 1 (see spec §7); attrs
|
|
247
|
+
// shouldn't let plugins smuggle nonces past the design decision.
|
|
248
|
+
// The CSP nonce RFP will reintroduce it through `cspNonce` on
|
|
249
|
+
// PluginPublicRenderContext + `inlineScript.nonce: 'auto'`, not via
|
|
250
|
+
// the `attrs` bag.
|
|
251
|
+
"loading",
|
|
252
|
+
// iframe lazy-loading
|
|
253
|
+
"sandbox",
|
|
254
|
+
// iframe sandbox attribute
|
|
255
|
+
"allow",
|
|
256
|
+
// iframe permissions policy
|
|
257
|
+
"allowfullscreen"
|
|
258
|
+
// iframe fullscreen
|
|
259
|
+
]);
|
|
260
|
+
function isAllowedAttr(name) {
|
|
261
|
+
if (name.startsWith("data-")) return true;
|
|
262
|
+
return ALLOWED_ATTRS.has(name.toLowerCase());
|
|
263
|
+
}
|
|
264
|
+
function isSafeUrl(value) {
|
|
265
|
+
const trimmed = value.trim();
|
|
266
|
+
if (trimmed.length === 0) return false;
|
|
267
|
+
const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
|
|
268
|
+
if (!schemeMatch) return true;
|
|
269
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
270
|
+
return scheme === "http" || scheme === "https";
|
|
271
|
+
}
|
|
272
|
+
function isDev() {
|
|
273
|
+
const env = typeof process !== "undefined" && process.env ? process.env.NODE_ENV : void 0;
|
|
274
|
+
return env !== "production";
|
|
275
|
+
}
|
|
276
|
+
function warn(message) {
|
|
277
|
+
if (!isDev()) return;
|
|
278
|
+
console.warn(`[ampless plugin-head] ${message}`);
|
|
279
|
+
}
|
|
280
|
+
function applyAttrs(target, attrs, ownerLabel) {
|
|
281
|
+
if (!attrs) return;
|
|
282
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
283
|
+
if (!isAllowedAttr(k)) {
|
|
284
|
+
warn(
|
|
285
|
+
`${ownerLabel}: attr "${k}" not in allowlist (data-* / crossorigin / referrerpolicy / integrity / fetchpriority / loading / sandbox / allow / allowfullscreen). skipping.`
|
|
286
|
+
);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
target[k] = v;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function renderHeadDescriptor(descriptor, pluginLabel, index) {
|
|
293
|
+
switch (descriptor.type) {
|
|
294
|
+
case "script": {
|
|
295
|
+
if (!isSafeUrl(descriptor.src)) {
|
|
296
|
+
warn(
|
|
297
|
+
`${pluginLabel}: script descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
298
|
+
);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const props = {
|
|
302
|
+
src: descriptor.src
|
|
303
|
+
};
|
|
304
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
305
|
+
const hasAsync = typeof descriptor.async === "boolean";
|
|
306
|
+
const hasDefer = typeof descriptor.defer === "boolean";
|
|
307
|
+
if (hasAsync) props.async = descriptor.async;
|
|
308
|
+
if (hasDefer) props.defer = descriptor.defer;
|
|
309
|
+
if (!hasAsync && !hasDefer) {
|
|
310
|
+
if (descriptor.strategy === "lazyOnload") {
|
|
311
|
+
props.defer = true;
|
|
312
|
+
} else {
|
|
313
|
+
props.async = true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
applyAttrs(props, descriptor.attrs, `${pluginLabel} script#${descriptor.id ?? index}`);
|
|
317
|
+
return {
|
|
318
|
+
id: descriptor.id ?? null,
|
|
319
|
+
element: createElement("script", props)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
case "inlineScript": {
|
|
323
|
+
if (!descriptor.id) {
|
|
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
|
+
}
|
|
338
|
+
case "meta": {
|
|
339
|
+
const props = { content: descriptor.content };
|
|
340
|
+
if (descriptor.name) props.name = descriptor.name;
|
|
341
|
+
if (descriptor.property) props.property = descriptor.property;
|
|
342
|
+
return {
|
|
343
|
+
// meta has no id channel in the descriptor. Don't derive a
|
|
344
|
+
// dedup id from name/property — multiple `<meta name=...>`
|
|
345
|
+
// entries with the same name are legitimate (e.g. theme-color
|
|
346
|
+
// media variants, and two plugins emitting overlapping names
|
|
347
|
+
// is a real case the runtime shouldn't silently collapse).
|
|
348
|
+
// Position-based React keys handle the stable-key requirement.
|
|
349
|
+
id: null,
|
|
350
|
+
element: createElement("meta", props)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
case "link": {
|
|
354
|
+
if (!isSafeUrl(descriptor.href)) {
|
|
355
|
+
warn(
|
|
356
|
+
`${pluginLabel}: link descriptor #${index} dropped \u2014 unsafe href "${descriptor.href}".`
|
|
357
|
+
);
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const props = {
|
|
361
|
+
rel: descriptor.rel,
|
|
362
|
+
href: descriptor.href
|
|
363
|
+
};
|
|
364
|
+
if (descriptor.as) props.as = descriptor.as;
|
|
365
|
+
if (descriptor.typeAttr) props.type = descriptor.typeAttr;
|
|
366
|
+
return {
|
|
367
|
+
id: null,
|
|
368
|
+
element: createElement("link", props)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
case "noscript": {
|
|
372
|
+
const props = {
|
|
373
|
+
dangerouslySetInnerHTML: { __html: descriptor.html }
|
|
374
|
+
};
|
|
375
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
376
|
+
return {
|
|
377
|
+
id: descriptor.id ?? null,
|
|
378
|
+
element: createElement("noscript", props)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function renderBodyDescriptor(descriptor, pluginLabel, index) {
|
|
384
|
+
if (descriptor.type === "iframe") {
|
|
385
|
+
if (!isSafeUrl(descriptor.src)) {
|
|
386
|
+
warn(
|
|
387
|
+
`${pluginLabel}: iframe descriptor #${index} dropped \u2014 unsafe src "${descriptor.src}".`
|
|
388
|
+
);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const props = {
|
|
392
|
+
src: descriptor.src
|
|
393
|
+
};
|
|
394
|
+
if (descriptor.id) props.id = descriptor.id;
|
|
395
|
+
if (descriptor.title) props.title = descriptor.title;
|
|
396
|
+
if (typeof descriptor.width === "number") props.width = descriptor.width;
|
|
397
|
+
if (typeof descriptor.height === "number") props.height = descriptor.height;
|
|
398
|
+
applyAttrs(props, descriptor.attrs, `${pluginLabel} iframe#${descriptor.id ?? index}`);
|
|
399
|
+
return {
|
|
400
|
+
id: descriptor.id ?? null,
|
|
401
|
+
element: createElement("iframe", props)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return renderHeadDescriptor(descriptor, pluginLabel, index);
|
|
405
|
+
}
|
|
406
|
+
function dedupeAndKey(entries) {
|
|
407
|
+
const lastIndexById = /* @__PURE__ */ new Map();
|
|
408
|
+
for (let i = 0; i < entries.length; i++) {
|
|
409
|
+
const id = entries[i].id;
|
|
410
|
+
if (id === null) continue;
|
|
411
|
+
if (lastIndexById.has(id)) {
|
|
412
|
+
warn(`duplicate descriptor id "${id}" \u2014 keeping the last occurrence.`);
|
|
413
|
+
}
|
|
414
|
+
lastIndexById.set(id, i);
|
|
415
|
+
}
|
|
416
|
+
const kept = [];
|
|
417
|
+
for (let i = 0; i < entries.length; i++) {
|
|
418
|
+
const { id, element } = entries[i];
|
|
419
|
+
if (id !== null && lastIndexById.get(id) !== i) continue;
|
|
420
|
+
const key = id ?? `__pos-${i}`;
|
|
421
|
+
const existingProps = element.props;
|
|
422
|
+
kept.push(createElement(element.type, { ...existingProps, key }));
|
|
423
|
+
}
|
|
424
|
+
return kept;
|
|
425
|
+
}
|
|
426
|
+
function makeCtx(plugin, site, snapshot) {
|
|
427
|
+
const instanceId = plugin.instanceId ?? plugin.name;
|
|
428
|
+
const stored = snapshot.get(instanceId) ?? {};
|
|
429
|
+
const resolved = resolvePluginSettings(plugin.settings, stored);
|
|
430
|
+
return {
|
|
431
|
+
site,
|
|
432
|
+
setting(key) {
|
|
433
|
+
if (!isValidPluginKey(key)) return void 0;
|
|
434
|
+
const v = resolved[key];
|
|
435
|
+
return v === void 0 ? void 0 : v;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function collectFor(plugins, site, snapshot, surface, renderOne) {
|
|
440
|
+
const entries = [];
|
|
441
|
+
for (const plugin of plugins) {
|
|
442
|
+
const factory = surface(plugin);
|
|
443
|
+
if (!factory) continue;
|
|
444
|
+
const ctx = makeCtx(plugin, site, snapshot);
|
|
445
|
+
let descriptors;
|
|
446
|
+
try {
|
|
447
|
+
descriptors = factory.call(plugin, ctx) ?? [];
|
|
448
|
+
} catch (err) {
|
|
449
|
+
warn(
|
|
450
|
+
`plugin "${plugin.instanceId ?? plugin.name}" threw inside descriptor callback: ${err instanceof Error ? err.message : String(err)}`
|
|
451
|
+
);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const label = `plugin "${plugin.instanceId ?? plugin.name}"`;
|
|
455
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
456
|
+
const entry = renderOne(descriptors[i], label, i);
|
|
457
|
+
if (entry) entries.push(entry);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (entries.length === 0) return null;
|
|
461
|
+
const keyed = dedupeAndKey(entries);
|
|
462
|
+
return createElement(Fragment, null, ...keyed);
|
|
463
|
+
}
|
|
464
|
+
function createPluginHead(cmsConfig, pluginSettings) {
|
|
465
|
+
const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
|
|
466
|
+
const validPlugins = [];
|
|
467
|
+
const seenNamespaces = /* @__PURE__ */ new Set();
|
|
468
|
+
for (const plugin of plugins) {
|
|
469
|
+
const ns = plugin.instanceId ?? plugin.name;
|
|
470
|
+
const label = plugin.instanceId ? `${plugin.name}#${plugin.instanceId}` : plugin.name;
|
|
471
|
+
if (!isValidPluginKey(ns)) {
|
|
472
|
+
warn(
|
|
473
|
+
`${label}: plugin namespace "${ns}" violates ${`/^[a-zA-Z0-9_-]+$/`}. Use a simple identifier (letters / digits / "-" / "_"). Skipping plugin.`
|
|
474
|
+
);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (seenNamespaces.has(ns)) {
|
|
478
|
+
warn(
|
|
479
|
+
`duplicate plugin namespace "${ns}" detected in cms.config.plugins. Set distinct \`instanceId\` on each instance to disambiguate.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
seenNamespaces.add(ns);
|
|
483
|
+
if (plugin.settings?.public) {
|
|
484
|
+
for (const field of plugin.settings.public) {
|
|
485
|
+
if (!isValidPluginKey(field.key)) {
|
|
486
|
+
warn(
|
|
487
|
+
`${label}: settings.public field key "${field.key}" violates ${`/^[a-zA-Z0-9_-]+$/`}. The field will not be readable through ctx.setting(). Rename the field key.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
validPlugins.push(plugin);
|
|
493
|
+
const caps = plugin.capabilities;
|
|
494
|
+
if (caps) {
|
|
495
|
+
if (caps.includes("publicHead") && !plugin.publicHead) {
|
|
496
|
+
warn(
|
|
497
|
+
`${label}: declares capability "publicHead" but no \`publicHead\` implementation. Drop the capability or add the function.`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (caps.includes("publicBody") && !plugin.publicBodyEnd) {
|
|
501
|
+
warn(
|
|
502
|
+
`${label}: declares capability "publicBody" but no \`publicBodyEnd\` implementation. Drop the capability or add the function.`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (plugin.publicHead && !caps.includes("publicHead")) {
|
|
506
|
+
warn(
|
|
507
|
+
`${label}: implements \`publicHead\` but "publicHead" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (plugin.publicBodyEnd && !caps.includes("publicBody")) {
|
|
511
|
+
warn(
|
|
512
|
+
`${label}: implements \`publicBodyEnd\` but "publicBody" is not in declared capabilities. Add it so admin UI / capability gates see the surface.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
async renderHead() {
|
|
519
|
+
const snapshot = await pluginSettings.loadAll();
|
|
520
|
+
return collectFor(
|
|
521
|
+
validPlugins,
|
|
522
|
+
cmsConfig.site,
|
|
523
|
+
snapshot,
|
|
524
|
+
(p) => p.publicHead,
|
|
525
|
+
renderHeadDescriptor
|
|
526
|
+
);
|
|
527
|
+
},
|
|
528
|
+
async renderBodyEnd() {
|
|
529
|
+
const snapshot = await pluginSettings.loadAll();
|
|
530
|
+
return collectFor(
|
|
531
|
+
validPlugins,
|
|
532
|
+
cmsConfig.site,
|
|
533
|
+
snapshot,
|
|
534
|
+
(p) => p.publicBodyEnd,
|
|
535
|
+
renderBodyDescriptor
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/plugin-settings.ts
|
|
542
|
+
import { unflattenSettings as unflattenSettings2 } from "ampless";
|
|
543
|
+
function createPluginSettings(storage) {
|
|
544
|
+
return {
|
|
545
|
+
async loadAll() {
|
|
546
|
+
const out = /* @__PURE__ */ new Map();
|
|
547
|
+
if (!storage.isStorageConfigured()) return out;
|
|
548
|
+
let url;
|
|
549
|
+
try {
|
|
550
|
+
url = storage.publicAssetUrl("public/site-settings.json");
|
|
551
|
+
} catch {
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
let flat;
|
|
555
|
+
try {
|
|
556
|
+
const res = await fetch(url, {
|
|
557
|
+
// Same revalidate + tag as site-settings.ts so a single
|
|
558
|
+
// request to the public route hits the JSON once and both
|
|
559
|
+
// helpers share the cached body.
|
|
560
|
+
next: { revalidate: 60, tags: ["site-settings"] }
|
|
561
|
+
});
|
|
562
|
+
if (!res.ok) return out;
|
|
563
|
+
flat = await res.json();
|
|
564
|
+
} catch {
|
|
565
|
+
return out;
|
|
566
|
+
}
|
|
567
|
+
const nested = unflattenSettings2(flat);
|
|
568
|
+
const pluginsBlock = nested.plugins;
|
|
569
|
+
if (!pluginsBlock || typeof pluginsBlock !== "object") return out;
|
|
570
|
+
for (const [instanceId, block] of Object.entries(pluginsBlock)) {
|
|
571
|
+
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
|
|
572
|
+
out.set(instanceId, { ...block });
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
228
579
|
// src/theme-active.ts
|
|
229
580
|
import { headers } from "next/headers";
|
|
230
581
|
function createThemeActive(registry, storage) {
|
|
@@ -657,6 +1008,8 @@ function createAmpless(opts) {
|
|
|
657
1008
|
const media = createMediaApi(outputs);
|
|
658
1009
|
const settings = createSiteSettings(cmsConfig, storage);
|
|
659
1010
|
const seo = createSeo(cmsConfig, settings);
|
|
1011
|
+
const pluginSettings = createPluginSettings(storage);
|
|
1012
|
+
const pluginHead = createPluginHead(cmsConfig, pluginSettings);
|
|
660
1013
|
const themeActive = createThemeActive(themes, storage);
|
|
661
1014
|
const themeConfig = createThemeConfig(themeActive, storage);
|
|
662
1015
|
return {
|
|
@@ -670,6 +1023,8 @@ function createAmpless(opts) {
|
|
|
670
1023
|
loadThemeConfig: () => themeConfig.loadThemeConfig(),
|
|
671
1024
|
postMetadata: (post) => seo.postMetadata(post),
|
|
672
1025
|
siteMetadata: () => seo.siteMetadata(),
|
|
1026
|
+
publicHead: () => pluginHead.renderHead(),
|
|
1027
|
+
publicBodyEnd: () => pluginHead.renderBodyEnd(),
|
|
673
1028
|
renderBody: (post) => renderBody(post),
|
|
674
1029
|
renderThemeCss: (cssVars) => renderThemeCss(cssVars),
|
|
675
1030
|
publicAssetUrl: (key) => storage.publicAssetUrl(key),
|
|
@@ -683,7 +1038,9 @@ function createAmpless(opts) {
|
|
|
683
1038
|
seo,
|
|
684
1039
|
themeActive,
|
|
685
1040
|
themeConfig,
|
|
686
|
-
storageApi: storage
|
|
1041
|
+
storageApi: storage,
|
|
1042
|
+
pluginHead,
|
|
1043
|
+
pluginSettings
|
|
687
1044
|
};
|
|
688
1045
|
}
|
|
689
1046
|
export {
|
|
@@ -691,6 +1048,8 @@ export {
|
|
|
691
1048
|
DEFAULT_COLOR_SCHEME,
|
|
692
1049
|
_resetStreamS3Cache,
|
|
693
1050
|
createAmpless,
|
|
1051
|
+
createPluginHead,
|
|
1052
|
+
createPluginSettings,
|
|
694
1053
|
htmlToMarkdown,
|
|
695
1054
|
markdownToHtml,
|
|
696
1055
|
renderBody,
|
package/dist/routes/index.d.ts
CHANGED
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.24",
|
|
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.18",
|
|
53
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.18"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"next": "^15 || ^16",
|