@ampless/runtime 1.0.0-beta.69 → 1.0.0-beta.71

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/README.ja.md CHANGED
@@ -96,6 +96,15 @@ middleware はリクエストごとに AppSync から `post.format` /
96
96
  `app/md/[slug]/route.ts` で配信 — `ampless.postToMarkdown()` による
97
97
  Markdown 投影。`cms.config.ai.markdownRoutes: false` で無効化できます。
98
98
 
99
+ `/llms.txt` は書き換えを経由せず、`app/llms.txt/route.ts` に直接マウ
100
+ ントされます。直近の公開投稿(デフォルト 100 件、
101
+ `cms.config.ai.llmsTxt.limit` で設定可能、1..1000 にクランプ)を
102
+ `.md` リンクの一覧として返し、サイト全体を 1 ファイルで把握したい AI
103
+ エージェント / クローラー向けの索引になります。
104
+ `cms.config.ai.llmsTxt: false` で無効化できます。どちらの設定でも
105
+ `llms.txt` は予約 slug になります — slug が `llms.txt` の投稿はテーマ
106
+ ページに到達できなくなります。
107
+
99
108
  また、`post.metadata.cache`(auto / deep / hot)+ `post.updatedAt` +
100
109
  `cms.config.cache.{cooldownMs, freshTtlSeconds, deepTtlSeconds}` から
101
110
  `Cache-Control` を算出してレスポンスに付与します。キャッシュ戦略の
@@ -105,7 +114,7 @@ middleware はリクエストごとに AppSync から `post.format` /
105
114
 
106
115
  - `@ampless/runtime` — `createAmpless`、ランタイム型、`renderBody`・`renderThemeCss`・フォーマットコンバーターの再エクスポート
107
116
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`、`defaultMatcherConfig`
108
- - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`、`createMarkdownRouteHandler`
117
+ - `@ampless/runtime/routes` — `createOgRouteHandler`、`createSitemapRouteHandler`、`createFeedRouteHandler`、`createRawRouteHandler`、`createStaticRouteHandler`、`createMarkdownRouteHandler`、`createLlmsTxtRouteHandler`
109
118
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`、`createThemePostDispatcher`、`createThemeTagDispatcher`(それぞれ対応する `*Metadata` ファクトリーあり)
110
119
 
111
120
  ## テンプレートに残るもの
package/README.md CHANGED
@@ -96,6 +96,14 @@ rewrites the request to the right internal handler:
96
96
  `app/md/[slug]/route.ts` — Markdown projection via
97
97
  `ampless.postToMarkdown()`. Disable with `cms.config.ai.markdownRoutes: false`.
98
98
 
99
+ `/llms.txt` isn't rewritten — it's mounted directly at
100
+ `app/llms.txt/route.ts` and lists the site's most recent published
101
+ posts (default 100, configurable via `cms.config.ai.llmsTxt.limit`,
102
+ clamped to 1..1000) as `.md` links, for AI agents / crawlers that want
103
+ a single-file index. Disable with `cms.config.ai.llmsTxt: false`. Note
104
+ that `llms.txt` becomes a reserved slug either way — a post whose slug
105
+ happens to be `llms.txt` can no longer reach the themed route.
106
+
99
107
  It also computes `Cache-Control` from `post.metadata.cache` (auto /
100
108
  deep / hot) + `post.updatedAt` + `cms.config.cache.{cooldownMs,
101
109
  freshTtlSeconds, deepTtlSeconds}` and sets the header on the response.
@@ -105,7 +113,7 @@ See `docs/CONTENT.md` for the cache strategy contract.
105
113
 
106
114
  - `@ampless/runtime` — `createAmpless`, runtime types, and re-exports of `renderBody`, `renderThemeCss`, format converters
107
115
  - `@ampless/runtime/middleware` — `createAmplessMiddleware`, `defaultMatcherConfig`
108
- - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`, `createMarkdownRouteHandler`
116
+ - `@ampless/runtime/routes` — `createOgRouteHandler`, `createSitemapRouteHandler`, `createFeedRouteHandler`, `createRawRouteHandler`, `createStaticRouteHandler`, `createMarkdownRouteHandler`, `createLlmsTxtRouteHandler`
109
117
  - `@ampless/runtime/dispatchers` — `createThemeHomeDispatcher`, `createThemePostDispatcher`, `createThemeTagDispatcher` (each with a matching `*Metadata` factory)
110
118
 
111
119
  ## What's still in the template
package/dist/index.d.ts CHANGED
@@ -403,9 +403,13 @@ interface PluginHeadApi {
403
403
  /**
404
404
  * Resolve the per-plugin `PluginPublicRenderContext` snapshot used by
405
405
  * `contentFields` renderers. Same `settings()` binding the public
406
- * surfaces (`publicHead` / `publicBodyEnd`) use. Async because it
407
- * reads the S3 site-settings cache once per request via
408
- * `pluginSettings.loadAll()`.
406
+ * surfaces (`publicHead` / `publicBodyEnd`) use. Async because it reads
407
+ * the S3 site-settings cache once per request — both plugin settings
408
+ * (`pluginSettings.loadAll()`) AND, when `siteSettings` was passed to
409
+ * `createPluginHead`, the effective `site` block
410
+ * (`siteSettings.loadSiteSettings()`, admin overrides included; falls
411
+ * back to `cmsConfig.site` on fetch failure or when `siteSettings` was
412
+ * not supplied).
409
413
  */
410
414
  contextForPlugins(): Promise<(plugin: AmplessPlugin) => PluginPublicRenderContext>;
411
415
  /**
@@ -453,7 +457,7 @@ interface PublicHtmlForPostResult {
453
457
  * U+2029 → '\u2029'
454
458
  */
455
459
  declare function escapeJsonLdInlineBody(value: string): string;
456
- declare function createPluginHead(cmsConfig: Config, pluginSettings: PluginSettingsApi): PluginHeadApi;
460
+ declare function createPluginHead(cmsConfig: Config, pluginSettings: PluginSettingsApi, siteSettings?: SiteSettingsApi): PluginHeadApi;
457
461
 
458
462
  interface ThemesRegistry {
459
463
  /** Map of theme name → loaded theme module. */
package/dist/index.js CHANGED
@@ -1336,6 +1336,25 @@ function makeCtx(plugin, site, snapshot) {
1336
1336
  }
1337
1337
  };
1338
1338
  }
1339
+ async function resolveEffectiveSite(cmsConfig, siteSettings) {
1340
+ if (!siteSettings) return cmsConfig.site;
1341
+ try {
1342
+ const { site } = await siteSettings.loadSiteSettings();
1343
+ return site;
1344
+ } catch (err) {
1345
+ warn(
1346
+ `effective site settings fetch failed \u2014 falling back to cms.config.ts site block: ${err instanceof Error ? err.message : String(err)}`
1347
+ );
1348
+ return cmsConfig.site;
1349
+ }
1350
+ }
1351
+ async function resolveRenderContext(cmsConfig, pluginSettings, siteSettings) {
1352
+ const [snapshot, site] = await Promise.all([
1353
+ pluginSettings.loadAll(),
1354
+ resolveEffectiveSite(cmsConfig, siteSettings)
1355
+ ]);
1356
+ return { snapshot, site };
1357
+ }
1339
1358
  function collectFor(plugins, site, snapshot, surface, renderOne) {
1340
1359
  const entries = [];
1341
1360
  for (const plugin of plugins) {
@@ -1532,7 +1551,7 @@ async function isPublicRequest() {
1532
1551
  if (h.get(PREVIEW_THEME_HEADER) || h.get(PREVIEW_COLOR_SCHEME_HEADER)) return false;
1533
1552
  return true;
1534
1553
  }
1535
- function createPluginHead(cmsConfig, pluginSettings) {
1554
+ function createPluginHead(cmsConfig, pluginSettings, siteSettings) {
1536
1555
  const plugins = (cmsConfig.plugins ?? []).filter(isPlugin2);
1537
1556
  const validPlugins = [];
1538
1557
  const seenNamespaces = /* @__PURE__ */ new Set();
@@ -1638,10 +1657,10 @@ function createPluginHead(cmsConfig, pluginSettings) {
1638
1657
  return {
1639
1658
  async renderHead() {
1640
1659
  if (!await isPublicRequest()) return null;
1641
- const snapshot = await pluginSettings.loadAll();
1660
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1642
1661
  return collectFor(
1643
1662
  validPlugins,
1644
- cmsConfig.site,
1663
+ site,
1645
1664
  snapshot,
1646
1665
  (p) => p.publicHead,
1647
1666
  renderHeadDescriptor
@@ -1649,38 +1668,38 @@ function createPluginHead(cmsConfig, pluginSettings) {
1649
1668
  },
1650
1669
  async renderBodyEnd() {
1651
1670
  if (!await isPublicRequest()) return null;
1652
- const snapshot = await pluginSettings.loadAll();
1671
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1653
1672
  return collectFor(
1654
1673
  validPlugins,
1655
- cmsConfig.site,
1674
+ site,
1656
1675
  snapshot,
1657
1676
  (p) => p.publicBodyEnd,
1658
1677
  renderBodyDescriptor
1659
1678
  );
1660
1679
  },
1661
1680
  async renderBodyForPost(post) {
1662
- const snapshot = await pluginSettings.loadAll();
1663
- return collectForPost(validPlugins, cmsConfig.site, snapshot, post);
1681
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1682
+ return collectForPost(validPlugins, site, snapshot, post);
1664
1683
  },
1665
1684
  async renderHtmlForPost(post) {
1666
- const snapshot = await pluginSettings.loadAll();
1667
- return collectHtmlForPost(validPlugins, cmsConfig.site, snapshot, post);
1685
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1686
+ return collectHtmlForPost(validPlugins, site, snapshot, post);
1668
1687
  },
1669
1688
  async renderPostScriptsForPage(posts) {
1670
- const snapshot = await pluginSettings.loadAll();
1671
- return collectPostScriptsForPage(validPlugins, cmsConfig.site, snapshot, posts);
1689
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1690
+ return collectPostScriptsForPage(validPlugins, site, snapshot, posts);
1672
1691
  },
1673
1692
  contentFieldsRegistry,
1674
1693
  markdownAdapters,
1675
1694
  async contextForPlugins() {
1676
- const snapshot = await pluginSettings.loadAll();
1677
- return (plugin) => makeCtx(plugin, cmsConfig.site, snapshot);
1695
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1696
+ return (plugin) => makeCtx(plugin, site, snapshot);
1678
1697
  },
1679
1698
  async renderHeadForPreview() {
1680
- const snapshot = await pluginSettings.loadAll();
1699
+ const { snapshot, site } = await resolveRenderContext(cmsConfig, pluginSettings, siteSettings);
1681
1700
  return collectFor(
1682
1701
  validPlugins,
1683
- cmsConfig.site,
1702
+ site,
1684
1703
  snapshot,
1685
1704
  (p) => p.publicHead,
1686
1705
  renderHeadDescriptor
@@ -1910,7 +1929,7 @@ function createAmpless(opts) {
1910
1929
  const settings = createSiteSettings(cmsConfig, storage);
1911
1930
  const seo = createSeo(cmsConfig, settings);
1912
1931
  const pluginSettings = createPluginSettings(storage);
1913
- const pluginHead = createPluginHead(cmsConfig, pluginSettings);
1932
+ const pluginHead = createPluginHead(cmsConfig, pluginSettings, settings);
1914
1933
  const themeActive = createThemeActive(themes, storage);
1915
1934
  const themeConfig = createThemeConfig(themeActive, storage);
1916
1935
  return {
@@ -120,6 +120,13 @@ function computeCacheControl(flags, cmsConfig) {
120
120
  }
121
121
  return `public, max-age=${freshTtl}, s-maxage=${freshTtl}`;
122
122
  }
123
+ function safeDecodeURIComponent(s) {
124
+ try {
125
+ return decodeURIComponent(s);
126
+ } catch {
127
+ return s;
128
+ }
129
+ }
123
130
  var RESERVED_PREFIXES = /* @__PURE__ */ new Set([
124
131
  "admin",
125
132
  "api",
@@ -131,6 +138,7 @@ var RESERVED_PREFIXES = /* @__PURE__ */ new Set([
131
138
  // route handler folders that live alongside `app/[slug]/page.tsx`
132
139
  "feed.xml",
133
140
  "sitemap.xml",
141
+ "llms.txt",
134
142
  "og",
135
143
  "tag",
136
144
  // internal rewrite targets — direct hits should not be middleware-driven
@@ -163,7 +171,8 @@ function createAmplessMiddleware(opts) {
163
171
  const restPath = segments.slice(1);
164
172
  const isMdRequest = restPath.length === 0 && slug.endsWith(".md") && slug.length > 3 && opts.cmsConfig.ai?.markdownRoutes !== false;
165
173
  const lookupSlug = isMdRequest ? slug.slice(0, -3) : slug;
166
- const flags = await getCachedFlags(opts, lookupSlug);
174
+ const flagsSlug = isMdRequest ? safeDecodeURIComponent(lookupSlug) : lookupSlug;
175
+ const flags = await getCachedFlags(opts, flagsSlug);
167
176
  if (!flags) {
168
177
  if (restPath.length > 0 || isMdRequest) {
169
178
  return new NextResponse("Not Found", { status: 404 });
@@ -5,18 +5,18 @@ import 'react';
5
5
  import 'aws-amplify/storage/server';
6
6
  import 'next/headers';
7
7
 
8
- interface Ctx$5 {
8
+ interface Ctx$6 {
9
9
  params: Promise<{
10
10
  slug: string;
11
11
  }>;
12
12
  }
13
- type OgRouteHandler = (req: Request, ctx: Ctx$5) => Promise<Response>;
13
+ type OgRouteHandler = (req: Request, ctx: Ctx$6) => Promise<Response>;
14
14
  declare function createOgRouteHandler(ampless: Ampless): OgRouteHandler;
15
15
 
16
- interface Ctx$4 {
16
+ interface Ctx$5 {
17
17
  params: Promise<Record<string, never>>;
18
18
  }
19
- type SitemapRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
19
+ type SitemapRouteHandler = (req: Request, ctx: Ctx$5) => Promise<Response>;
20
20
  /**
21
21
  * Sitemap route delegate. Looks up the active theme and forwards to
22
22
  * whichever `routes.sitemap` handler the theme provides. Themes
@@ -24,10 +24,10 @@ type SitemapRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
24
24
  */
25
25
  declare function createSitemapRouteHandler(ampless: Ampless): SitemapRouteHandler;
26
26
 
27
- interface Ctx$3 {
27
+ interface Ctx$4 {
28
28
  params: Promise<Record<string, never>>;
29
29
  }
30
- type FeedRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
30
+ type FeedRouteHandler = (req: Request, ctx: Ctx$4) => Promise<Response>;
31
31
  /**
32
32
  * Feed route delegate. Looks up the active theme and forwards to
33
33
  * whichever `routes.feed` handler the theme provides. Themes without
@@ -35,12 +35,12 @@ type FeedRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
35
35
  */
36
36
  declare function createFeedRouteHandler(ampless: Ampless): FeedRouteHandler;
37
37
 
38
- interface Ctx$2 {
38
+ interface Ctx$3 {
39
39
  params: Promise<{
40
40
  slug: string;
41
41
  }>;
42
42
  }
43
- type RawRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
43
+ type RawRouteHandler = (req: Request, ctx: Ctx$3) => Promise<Response>;
44
44
  /**
45
45
  * Internal route handler for `format: 'html'` posts whose
46
46
  * `metadata.no_layout === true` — the body is its own complete HTML
@@ -63,13 +63,13 @@ type RawRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
63
63
  */
64
64
  declare function createRawRouteHandler(ampless: Ampless): RawRouteHandler;
65
65
 
66
- interface Ctx$1 {
66
+ interface Ctx$2 {
67
67
  params: Promise<{
68
68
  slug: string;
69
69
  path?: string[];
70
70
  }>;
71
71
  }
72
- type StaticRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
72
+ type StaticRouteHandler = (req: Request, ctx: Ctx$2) => Promise<Response>;
73
73
  /**
74
74
  * Internal route handler for `format: 'static'` posts — the body is a
75
75
  * manifest describing a bundle of files in S3 at
@@ -97,12 +97,12 @@ type StaticRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
97
97
  */
98
98
  declare function createStaticRouteHandler(ampless: Ampless): StaticRouteHandler;
99
99
 
100
- interface Ctx {
100
+ interface Ctx$1 {
101
101
  params: Promise<{
102
102
  slug: string;
103
103
  }>;
104
104
  }
105
- type MarkdownRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
105
+ type MarkdownRouteHandler = (req: Request, ctx: Ctx$1) => Promise<Response>;
106
106
  /**
107
107
  * Route handler serving the per-post Markdown projection
108
108
  * (`ampless.postToMarkdown()`) for published posts of any format.
@@ -119,4 +119,16 @@ type MarkdownRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
119
119
  */
120
120
  declare function createMarkdownRouteHandler(ampless: Ampless): MarkdownRouteHandler;
121
121
 
122
- export { type FeedRouteHandler, type MarkdownRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createMarkdownRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
122
+ interface Ctx {
123
+ params: Promise<Record<string, never>>;
124
+ }
125
+ type LlmsTxtRouteHandler = (req: Request, ctx: Ctx) => Promise<Response>;
126
+ /**
127
+ * `ai.llmsTxt: false` disables the route (404). Note that `llms.txt`
128
+ * is always a reserved slug (`middleware.ts` `RESERVED_PREFIXES`)
129
+ * regardless of that flag — a post whose slug happens to be
130
+ * `llms.txt` can never reach the themed route.
131
+ */
132
+ declare function createLlmsTxtRouteHandler(ampless: Ampless): LlmsTxtRouteHandler;
133
+
134
+ export { type FeedRouteHandler, type LlmsTxtRouteHandler, type MarkdownRouteHandler, type OgRouteHandler, type RawRouteHandler, type SitemapRouteHandler, type StaticRouteHandler, createFeedRouteHandler, createLlmsTxtRouteHandler, createMarkdownRouteHandler, createOgRouteHandler, createRawRouteHandler, createSitemapRouteHandler, createStaticRouteHandler };
@@ -189,11 +189,10 @@ async function signStaticAsset({
189
189
  function createMarkdownRouteHandler(ampless) {
190
190
  return async function GET(_request, { params }) {
191
191
  const { slug } = await params;
192
- const cleanSlug = slug.replace(/\.md$/, "");
193
192
  if (ampless.cmsConfig.ai?.markdownRoutes === false) {
194
193
  return new Response("Not Found", { status: 404 });
195
194
  }
196
- const post = await ampless.getPublishedPost(cleanSlug);
195
+ const post = await ampless.getPublishedPost(slug);
197
196
  if (!post) {
198
197
  return new Response("Not Found", { status: 404 });
199
198
  }
@@ -206,8 +205,155 @@ function createMarkdownRouteHandler(ampless) {
206
205
  });
207
206
  };
208
207
  }
208
+
209
+ // src/routes/llms.ts
210
+ var DEFAULT_LIMIT = 100;
211
+ var MIN_LIMIT = 1;
212
+ var MAX_LIMIT = 1e3;
213
+ var PAGE_SIZE_CAP = 50;
214
+ var MAX_PAGES = 21;
215
+ var EXCERPT_MAX = 200;
216
+ function clampLimit(raw) {
217
+ const n = raw ?? DEFAULT_LIMIT;
218
+ if (!Number.isFinite(n)) return DEFAULT_LIMIT;
219
+ return Math.min(MAX_LIMIT, Math.max(MIN_LIMIT, Math.floor(n)));
220
+ }
221
+ function resolveLimit(ampless) {
222
+ const cfg = ampless.cmsConfig.ai?.llmsTxt;
223
+ if (cfg === false) return false;
224
+ const raw = typeof cfg === "object" && cfg !== null ? cfg.limit : void 0;
225
+ return clampLimit(raw);
226
+ }
227
+ var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
228
+ function normalizeText(s) {
229
+ return s.replace(CONTROL_CHARS_RE, " ").replace(/\s+/g, " ").trim();
230
+ }
231
+ function escapeLinkText(s) {
232
+ return s.replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
233
+ }
234
+ function truncateExcerpt(s) {
235
+ return s.length > EXCERPT_MAX ? s.slice(0, EXCERPT_MAX) + "\u2026" : s;
236
+ }
237
+ function fixedEncodeURIComponent(s) {
238
+ return encodeURIComponent(s).replace(
239
+ /[!'()*]/g,
240
+ (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
241
+ );
242
+ }
243
+ async function collectPosts(ampless, limit) {
244
+ const items = [];
245
+ const seenTokens = /* @__PURE__ */ new Set();
246
+ let token;
247
+ let truncated = null;
248
+ for (let page = 0; page < MAX_PAGES; page++) {
249
+ const remaining = limit + 1 - items.length;
250
+ const pageLimit = Math.min(PAGE_SIZE_CAP, remaining);
251
+ const res = await ampless.listPublishedPosts({ limit: pageLimit, nextToken: token });
252
+ items.push(...res.items);
253
+ if (items.length > limit) {
254
+ truncated = "limit";
255
+ break;
256
+ }
257
+ if (!res.nextToken) {
258
+ break;
259
+ }
260
+ if (seenTokens.has(res.nextToken)) {
261
+ truncated = "early";
262
+ break;
263
+ }
264
+ seenTokens.add(res.nextToken);
265
+ token = res.nextToken;
266
+ if (page === MAX_PAGES - 1) {
267
+ truncated = "early";
268
+ }
269
+ }
270
+ return {
271
+ items: truncated === "limit" ? items.slice(0, limit) : items,
272
+ truncated
273
+ };
274
+ }
275
+ function truncationNote(truncated, limit) {
276
+ if (truncated === "limit") {
277
+ return `Note: only the ${limit} most recent posts are listed; older posts are omitted.`;
278
+ }
279
+ if (truncated === "early") {
280
+ return "Note: this index was truncated early while scanning the post list; some older posts may be missing. Lower ai.llmsTxt.limit or reduce post sizes.";
281
+ }
282
+ return null;
283
+ }
284
+ function createLlmsTxtRouteHandler(ampless) {
285
+ return async function GET(request) {
286
+ const limit = resolveLimit(ampless);
287
+ if (limit === false) {
288
+ return new Response("Not Found", { status: 404 });
289
+ }
290
+ const url = new URL(request.url);
291
+ if (url.search) {
292
+ return new Response(null, {
293
+ status: 308,
294
+ headers: {
295
+ Location: url.pathname,
296
+ "Cache-Control": "public, max-age=3600"
297
+ }
298
+ });
299
+ }
300
+ const [{ items, truncated }, settings] = await Promise.all([
301
+ collectPosts(ampless, limit),
302
+ ampless.loadSiteSettings()
303
+ ]);
304
+ const markdownRoutesEnabled = ampless.cmsConfig.ai?.markdownRoutes !== false;
305
+ const siteUrl = (settings.site.url || "").replace(/\/+$/, "");
306
+ const buildUrl = (path) => siteUrl ? `${siteUrl}${path}` : path;
307
+ const blocks = [`# ${normalizeText(settings.site.name)}`];
308
+ if (settings.site.description) {
309
+ blocks.push(`> ${normalizeText(settings.site.description)}`);
310
+ }
311
+ if (markdownRoutesEnabled) {
312
+ blocks.push(
313
+ "Each entry links to the markdown projection of the post; drop the `.md` suffix for the themed HTML page."
314
+ );
315
+ }
316
+ const note = truncationNote(truncated, limit);
317
+ if (note) blocks.push(note);
318
+ if (items.length > 0) {
319
+ const lines = items.map((post) => {
320
+ const encodedSlug = fixedEncodeURIComponent(post.slug);
321
+ const href = markdownRoutesEnabled ? buildUrl(`/${encodedSlug}.md`) : buildUrl(`/${encodedSlug}`);
322
+ const title = escapeLinkText(normalizeText(post.title));
323
+ const excerpt = post.excerpt ? truncateExcerpt(normalizeText(post.excerpt)) : "";
324
+ const tags = (post.tags ?? []).map((t) => normalizeText(t)).filter(Boolean);
325
+ let line = `- [${title}](${href})`;
326
+ if (excerpt && tags.length > 0) {
327
+ line += `: ${excerpt} (tags: ${tags.join(", ")})`;
328
+ } else if (excerpt) {
329
+ line += `: ${excerpt}`;
330
+ } else if (tags.length > 0) {
331
+ line += `: (tags: ${tags.join(", ")})`;
332
+ }
333
+ return line;
334
+ });
335
+ blocks.push(`## Posts
336
+
337
+ ${lines.join("\n")}`);
338
+ }
339
+ const body = blocks.join("\n\n") + "\n";
340
+ return new Response(body, {
341
+ status: 200,
342
+ headers: {
343
+ "Content-Type": "text/plain; charset=utf-8",
344
+ // Self-computed: this route isn't reached via a middleware
345
+ // rewrite, so nothing else would set Cache-Control (see the
346
+ // header comment / og.ts for the same pattern). CDN-cacheable
347
+ // for an hour so repeated crawler hits don't each cost a fresh
348
+ // paginated AppSync walk.
349
+ "Cache-Control": "public, max-age=300, s-maxage=3600, stale-while-revalidate=3600"
350
+ }
351
+ });
352
+ };
353
+ }
209
354
  export {
210
355
  createFeedRouteHandler,
356
+ createLlmsTxtRouteHandler,
211
357
  createMarkdownRouteHandler,
212
358
  createOgRouteHandler,
213
359
  createRawRouteHandler,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-beta.69",
3
+ "version": "1.0.0-beta.71",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,8 +51,8 @@
51
51
  "marked": "^18.0.4",
52
52
  "sanitize-html": "^2.17.4",
53
53
  "tailwind-merge": "^3.6.0",
54
- "@ampless/plugin-og-image": "0.2.0-beta.56",
55
- "ampless": "1.0.0-beta.56"
54
+ "@ampless/plugin-og-image": "0.2.0-beta.58",
55
+ "ampless": "1.0.0-beta.58"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@aws-amplify/adapter-nextjs": "^1",