@tokenoftrust/storefront-runner 2.4.18 → 2.4.20
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/apps/storefront/src/config/publicationSpaceTopology.ts +18 -0
- package/apps/storefront/src/lib/publication/mountedDiscovery.ts +188 -0
- package/apps/storefront/src/lib/publication/rootDiscovery.ts +97 -0
- package/apps/storefront/src/pages/feed.json.ts +74 -0
- package/apps/storefront/src/pages/llms.txt.ts +33 -2
- package/apps/storefront/src/pages/search-index.json.ts +49 -0
- package/apps/storefront/src/pages/sitemap.xml.ts +26 -0
- package/package.json +1 -1
|
@@ -64,6 +64,24 @@ export const PUBLICATION_SPACE_TOPOLOGY: Readonly<Record<string, PublicationTopo
|
|
|
64
64
|
},
|
|
65
65
|
],
|
|
66
66
|
},
|
|
67
|
+
// The same three-space shape, served by CloudFront: certifies the s3-overwrite publication path.
|
|
68
|
+
"fixture-cloudfront.e2e.test": {
|
|
69
|
+
root: { source: { forge: "fixture", repo: "fixture-cloudfront.e2e.test", defaultBranch: "main" } },
|
|
70
|
+
mounted: [
|
|
71
|
+
{
|
|
72
|
+
id: "fixture-cloudfront-docs",
|
|
73
|
+
source: { forge: "fixture", repo: "fixture-cloudfront.e2e.test-docs", defaultBranch: "main" },
|
|
74
|
+
mount: "/docs",
|
|
75
|
+
visibility: "public",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "fixture-cloudfront-internal",
|
|
79
|
+
source: { forge: "fixture", repo: "fixture-cloudfront.e2e.test-internal", defaultBranch: "main" },
|
|
80
|
+
mount: "/internal-docs",
|
|
81
|
+
visibility: "internal",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
67
85
|
"chrome.e2e.test": {
|
|
68
86
|
root: { source: { forge: "fixture", repo: "chrome.e2e.test", defaultBranch: "main" } },
|
|
69
87
|
},
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounted publication spaces in the tenant's discovery surfaces (sitemap.xml,
|
|
3
|
+
* llms.txt, feed.json, search-index.json).
|
|
4
|
+
*
|
|
5
|
+
* The root routes emit the root's own discovery; this module adds the mounted
|
|
6
|
+
* public spaces. Their entries are composed once per pointer change by the
|
|
7
|
+
* control plane (`composeMountedPublicationDiscoveryV1`, written by the
|
|
8
|
+
* Cloudflare projection and by the CloudFront aggregate render) and stored at
|
|
9
|
+
* {@link publicationMountedDiscoveryKeyV1}. Serving reads that projection and
|
|
10
|
+
* never composes: ADR 0013, recompute per change rather than per request.
|
|
11
|
+
*
|
|
12
|
+
* Reading re-checks every entry against current authority, so a stale or
|
|
13
|
+
* tampered projection cannot list anything the route plan does not serve
|
|
14
|
+
* publicly right now: the space must be a declared public mount whose live
|
|
15
|
+
* pointer is still the revision the entry was composed from, and the entry's
|
|
16
|
+
* path must resolve to that same space. Internal spaces fail every one of those
|
|
17
|
+
* checks, so they are excluded structurally, not by a flag.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
compileRouteTierIndexV1,
|
|
21
|
+
parsePublicationVersionStateV1Json,
|
|
22
|
+
publicationVersionStateKey,
|
|
23
|
+
resolvePublicationRoutePlanV1,
|
|
24
|
+
resolveRouteTierV1,
|
|
25
|
+
type PublicationSpaceManifestV1,
|
|
26
|
+
type RouteTierIndexV1,
|
|
27
|
+
type TenantConfig,
|
|
28
|
+
} from "@tot/public-runtime";
|
|
29
|
+
|
|
30
|
+
export type DiscoverySurfaceV1 = "sitemap" | "feeds" | "publicSearch" | "llms";
|
|
31
|
+
|
|
32
|
+
/** One mounted-space document listed on a discovery surface. */
|
|
33
|
+
export interface MountedDiscoveryEntryV1 {
|
|
34
|
+
spaceId: string;
|
|
35
|
+
/** Tenant-relative canonical route (no trailing slash). */
|
|
36
|
+
canonicalPath: string;
|
|
37
|
+
title: string;
|
|
38
|
+
summary: string;
|
|
39
|
+
searchText: string;
|
|
40
|
+
publishedAt?: string;
|
|
41
|
+
updatedAt?: string;
|
|
42
|
+
surfaces: Record<DiscoverySurfaceV1, boolean>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MAX_ENTRIES = 10_000;
|
|
46
|
+
const MAX_TEXT = 256 * 1024;
|
|
47
|
+
const MAX_PROJECTION_CHARS = 32 * 1024 * 1024;
|
|
48
|
+
const SURFACES: readonly DiscoverySurfaceV1[] = ["sitemap", "feeds", "publicSearch", "llms"];
|
|
49
|
+
|
|
50
|
+
/** KV key of a tenant's composed mounted-space discovery projection. */
|
|
51
|
+
export function publicationMountedDiscoveryKeyV1(tenant: string): string {
|
|
52
|
+
return `cust:publication-discovery:${tenant}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type DiscoveryTenantV1 = Pick<TenantConfig, "tenant_id" | "publicationSpaces">;
|
|
56
|
+
|
|
57
|
+
function isPublicMount(manifest: PublicationSpaceManifestV1): boolean {
|
|
58
|
+
return manifest.routes.canonicalMount !== "/" && manifest.visibility === "public" &&
|
|
59
|
+
manifest.authorization.policy === "public" && manifest.assets.access === "public";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True when the tenant declares any space beside its root. */
|
|
63
|
+
export function declaresMountedSpacesV1(tenant: DiscoveryTenantV1): boolean {
|
|
64
|
+
return (tenant.publicationSpaces ?? []).some((manifest) => manifest.routes.canonicalMount !== "/");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether a root route row may be listed. A path inside a mounted space belongs
|
|
69
|
+
* to that space: its own entries list it when it is public, and nothing does when
|
|
70
|
+
* it is internal. Root content can never list into a mount.
|
|
71
|
+
*/
|
|
72
|
+
export function rootOwnsDiscoveryPathV1(index: RouteTierIndexV1, path: string): boolean {
|
|
73
|
+
const route = resolveRouteTierV1(index, path);
|
|
74
|
+
return route.origin !== "publication-space" ||
|
|
75
|
+
(route.mount?.mount === "/" && route.visibility === "public");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function exactKeys(value: object, required: readonly string[], optional: readonly string[] = []): boolean {
|
|
79
|
+
const keys = Object.keys(value);
|
|
80
|
+
return required.every((key) => keys.includes(key)) &&
|
|
81
|
+
keys.every((key) => required.includes(key) || optional.includes(key));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function boundedString(value: unknown, allowEmpty = true): value is string {
|
|
85
|
+
return typeof value === "string" && value.length <= MAX_TEXT && (allowEmpty || value.length > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseEntry(value: unknown): MountedDiscoveryEntryV1 | null {
|
|
89
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
90
|
+
const raw = value as Record<string, unknown>;
|
|
91
|
+
if (!exactKeys(raw, ["spaceId", "canonicalPath", "title", "summary", "searchText", "surfaces"],
|
|
92
|
+
["publishedAt", "updatedAt"])) return null;
|
|
93
|
+
const surfaces = raw.surfaces;
|
|
94
|
+
if (!surfaces || typeof surfaces !== "object" || Array.isArray(surfaces) ||
|
|
95
|
+
!exactKeys(surfaces, SURFACES) ||
|
|
96
|
+
SURFACES.some((surface) => typeof (surfaces as Record<string, unknown>)[surface] !== "boolean")) return null;
|
|
97
|
+
if (!boundedString(raw.spaceId, false) || !boundedString(raw.canonicalPath, false) ||
|
|
98
|
+
!raw.canonicalPath.startsWith("/") || !boundedString(raw.title, false) ||
|
|
99
|
+
!boundedString(raw.summary) || !boundedString(raw.searchText) ||
|
|
100
|
+
(raw.publishedAt !== undefined && !boundedString(raw.publishedAt, false)) ||
|
|
101
|
+
(raw.updatedAt !== undefined && !boundedString(raw.updatedAt, false))) return null;
|
|
102
|
+
return {
|
|
103
|
+
spaceId: raw.spaceId,
|
|
104
|
+
canonicalPath: raw.canonicalPath,
|
|
105
|
+
title: raw.title,
|
|
106
|
+
summary: raw.summary,
|
|
107
|
+
searchText: raw.searchText,
|
|
108
|
+
...(raw.publishedAt !== undefined ? { publishedAt: raw.publishedAt as string } : {}),
|
|
109
|
+
...(raw.updatedAt !== undefined ? { updatedAt: raw.updatedAt as string } : {}),
|
|
110
|
+
surfaces: { ...(surfaces as Record<DiscoverySurfaceV1, boolean>) },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The mounted public spaces' entries currently servable for `tenant`, in
|
|
116
|
+
* canonical path order. Empty — the root routes then emit root-only output —
|
|
117
|
+
* whenever the tenant declares no public mount, no projection or state is
|
|
118
|
+
* readable, or the projection is malformed.
|
|
119
|
+
*/
|
|
120
|
+
export async function readMountedPublicationDiscoveryV1(input: {
|
|
121
|
+
tenant: DiscoveryTenantV1;
|
|
122
|
+
store?: { get(key: string): Promise<string | null> };
|
|
123
|
+
index?: RouteTierIndexV1;
|
|
124
|
+
}): Promise<MountedDiscoveryEntryV1[]> {
|
|
125
|
+
const mounts = new Map((input.tenant.publicationSpaces ?? [])
|
|
126
|
+
.filter((manifest) => manifest.tenant === input.tenant.tenant_id && isPublicMount(manifest))
|
|
127
|
+
.map((manifest) => [manifest.id, manifest]));
|
|
128
|
+
if (mounts.size === 0 || !input.store) return [];
|
|
129
|
+
let projectionJson: string | null;
|
|
130
|
+
let stateJson: string | null;
|
|
131
|
+
try {
|
|
132
|
+
[projectionJson, stateJson] = await Promise.all([
|
|
133
|
+
input.store.get(publicationMountedDiscoveryKeyV1(input.tenant.tenant_id)),
|
|
134
|
+
input.store.get(publicationVersionStateKey(input.tenant.tenant_id)),
|
|
135
|
+
]);
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
if (!projectionJson || !stateJson || projectionJson.length > MAX_PROJECTION_CHARS) return [];
|
|
140
|
+
const state = parsePublicationVersionStateV1Json(stateJson);
|
|
141
|
+
if (!state.ok) return [];
|
|
142
|
+
let projection: unknown;
|
|
143
|
+
try {
|
|
144
|
+
projection = JSON.parse(projectionJson);
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
if (!projection || typeof projection !== "object" || Array.isArray(projection) ||
|
|
149
|
+
!exactKeys(projection, ["version", "tenant", "spaces", "entries"])) return [];
|
|
150
|
+
const raw = projection as Record<string, unknown>;
|
|
151
|
+
if (raw.version !== 1 || raw.tenant !== input.tenant.tenant_id || !Array.isArray(raw.spaces) ||
|
|
152
|
+
!Array.isArray(raw.entries) || raw.entries.length > MAX_ENTRIES) return [];
|
|
153
|
+
|
|
154
|
+
// A space is listed only while its live pointer is the exact revision composed.
|
|
155
|
+
const current = new Set<string>();
|
|
156
|
+
for (const space of raw.spaces as unknown[]) {
|
|
157
|
+
if (!space || typeof space !== "object" || Array.isArray(space)) return [];
|
|
158
|
+
const { spaceId, revision, versionId } = space as Record<string, unknown>;
|
|
159
|
+
if (typeof spaceId !== "string" || !mounts.has(spaceId)) continue;
|
|
160
|
+
const live = state.value.pointers.find((pointer) => pointer.tenant === input.tenant.tenant_id &&
|
|
161
|
+
pointer.spaceId === spaceId && pointer.channel === "live");
|
|
162
|
+
if (live && live.revision === revision && live.versionId === versionId) current.add(spaceId);
|
|
163
|
+
}
|
|
164
|
+
if (current.size === 0) return [];
|
|
165
|
+
|
|
166
|
+
const index = input.index ?? compileRouteTierIndexV1(input.tenant);
|
|
167
|
+
const entries: MountedDiscoveryEntryV1[] = [];
|
|
168
|
+
const seen = new Set<string>();
|
|
169
|
+
for (const value of raw.entries as unknown[]) {
|
|
170
|
+
const entry = parseEntry(value);
|
|
171
|
+
if (!entry) return [];
|
|
172
|
+
if (!current.has(entry.spaceId) || seen.has(entry.canonicalPath)) continue;
|
|
173
|
+
const plan = resolvePublicationRoutePlanV1(index, entry.canonicalPath);
|
|
174
|
+
if (plan.action !== "render" || plan.manifest.id !== entry.spaceId ||
|
|
175
|
+
plan.canonicalPath !== entry.canonicalPath || !isPublicMount(plan.manifest)) continue;
|
|
176
|
+
seen.add(entry.canonicalPath);
|
|
177
|
+
entries.push(entry);
|
|
178
|
+
}
|
|
179
|
+
return entries.sort((left, right) =>
|
|
180
|
+
left.canonicalPath < right.canonicalPath ? -1 : left.canonicalPath > right.canonicalPath ? 1 : 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** An RFC 3339 instant for a feed, or undefined when the value is not a date. */
|
|
184
|
+
export function feedInstantV1(value: string | undefined): string | undefined {
|
|
185
|
+
if (!value) return undefined;
|
|
186
|
+
const parsed = Date.parse(value);
|
|
187
|
+
return Number.isFinite(parsed) ? new Date(parsed).toISOString() : undefined;
|
|
188
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root site's own documents for the JSON discovery surfaces (feed.json,
|
|
3
|
+
* search-index.json), read through the request's resolved content provider so
|
|
4
|
+
* they follow the same published channel and visibility clock as the pages.
|
|
5
|
+
* Paths are spelled exactly as the root sitemap lists them.
|
|
6
|
+
*/
|
|
7
|
+
import type { APIContext } from "astro";
|
|
8
|
+
import { listBlogPosts } from "@/lib/blog/provider.js";
|
|
9
|
+
import { listBuildPosts } from "@/lib/the-build/provider.js";
|
|
10
|
+
import { isIndexableHtml, rawPageCanonicalUrl } from "@/lib/rawMarketingHtml.js";
|
|
11
|
+
|
|
12
|
+
export interface RootDiscoveryDocumentV1 {
|
|
13
|
+
kind: "home" | "page" | "post";
|
|
14
|
+
/** Tenant-relative path as the root sitemap spells it. */
|
|
15
|
+
path: string;
|
|
16
|
+
title: string;
|
|
17
|
+
summary: string;
|
|
18
|
+
searchText: string;
|
|
19
|
+
publishedAt?: string;
|
|
20
|
+
updatedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function plain(value: string | undefined | null): string {
|
|
24
|
+
return (value ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function htmlTitle(html: string): string {
|
|
28
|
+
const match = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(html);
|
|
29
|
+
return plain(match?.[1]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Root listable posts (blog and the-build), newest first as the collections list them. */
|
|
33
|
+
export async function rootPostDocumentsV1(locals: APIContext["locals"]): Promise<RootDiscoveryDocumentV1[]> {
|
|
34
|
+
const { tenant, content, publicationTimestamp } = locals;
|
|
35
|
+
const [blogPosts, buildPosts] = await Promise.all([
|
|
36
|
+
listBlogPosts(tenant.tenant_id, content, publicationTimestamp),
|
|
37
|
+
listBuildPosts(tenant.tenant_id, { content, now: publicationTimestamp }),
|
|
38
|
+
]);
|
|
39
|
+
return [
|
|
40
|
+
...blogPosts.map((post) => ({ post, base: "/blog" })),
|
|
41
|
+
...buildPosts.map((post) => ({ post, base: "/the-build" })),
|
|
42
|
+
].map(({ post, base }) => {
|
|
43
|
+
const summary = plain(post.description || post.excerpt);
|
|
44
|
+
return {
|
|
45
|
+
kind: "post" as const,
|
|
46
|
+
path: `${base}/${post.slug}/`,
|
|
47
|
+
title: plain(post.title) || post.slug,
|
|
48
|
+
summary,
|
|
49
|
+
searchText: [plain(post.title), summary].filter(Boolean).join(" "),
|
|
50
|
+
...(post.publishedAt ? { publishedAt: post.publishedAt } : {}),
|
|
51
|
+
...(post.modifiedAt ? { updatedAt: post.modifiedAt } : {}),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Root home, editorial pages, indexable raw pages, and posts. */
|
|
57
|
+
export async function rootSearchDocumentsV1(locals: APIContext["locals"]): Promise<RootDiscoveryDocumentV1[]> {
|
|
58
|
+
const { tenant, content, canonicalBase } = locals;
|
|
59
|
+
const base = canonicalBase.replace(/\/$/, "");
|
|
60
|
+
const [home, pages, rawSlugs, posts] = await Promise.all([
|
|
61
|
+
content.getHome().catch(() => null),
|
|
62
|
+
content.listPages(),
|
|
63
|
+
content.listPageHtmlSlugs(),
|
|
64
|
+
rootPostDocumentsV1(locals),
|
|
65
|
+
]);
|
|
66
|
+
const homeSummary = plain(home?.seoDescription);
|
|
67
|
+
const documents: RootDiscoveryDocumentV1[] = [{
|
|
68
|
+
kind: "home",
|
|
69
|
+
path: "/",
|
|
70
|
+
title: plain(home?.seoTitle) || tenant.displayName,
|
|
71
|
+
summary: homeSummary,
|
|
72
|
+
searchText: [plain(home?.seoTitle) || tenant.displayName, homeSummary].filter(Boolean).join(" "),
|
|
73
|
+
}];
|
|
74
|
+
for (const page of pages) {
|
|
75
|
+
const summary = plain(page.seoDescription);
|
|
76
|
+
documents.push({
|
|
77
|
+
kind: "page",
|
|
78
|
+
path: `/${page.slug}`,
|
|
79
|
+
title: plain(page.seoTitle || page.title) || page.slug,
|
|
80
|
+
summary,
|
|
81
|
+
searchText: [plain(page.title), summary].filter(Boolean).join(" "),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const rawPages = await Promise.all(rawSlugs.map(async (slug) => ({ slug, html: await content.getPageHtml(slug) })));
|
|
85
|
+
for (const { slug, html } of rawPages) {
|
|
86
|
+
if (!html || !isIndexableHtml(html)) continue;
|
|
87
|
+
const title = htmlTitle(html) || slug;
|
|
88
|
+
documents.push({
|
|
89
|
+
kind: "page",
|
|
90
|
+
path: rawPageCanonicalUrl(canonicalBase, slug).slice(base.length) || "/",
|
|
91
|
+
title,
|
|
92
|
+
summary: "",
|
|
93
|
+
searchText: title,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return [...documents, ...posts];
|
|
97
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tenant JSON Feed 1.1 (https://jsonfeed.org/version/1.1). The root
|
|
3
|
+
* syndicates its listable posts — the same visibility chokepoint the sitemap
|
|
4
|
+
* and `/the-build/rss.xml` ride, so drafts, unlisted and not-yet-due posts never
|
|
5
|
+
* appear — and each public mounted publication space adds the entries it
|
|
6
|
+
* declares for feeds. An internal space lists nothing.
|
|
7
|
+
*/
|
|
8
|
+
import type { APIContext } from "astro";
|
|
9
|
+
import { compileRouteTierIndexV1 } from "@tot/public-runtime";
|
|
10
|
+
import { readKv } from "@/lib/runtimeEnv.js";
|
|
11
|
+
import {
|
|
12
|
+
declaresMountedSpacesV1,
|
|
13
|
+
feedInstantV1,
|
|
14
|
+
readMountedPublicationDiscoveryV1,
|
|
15
|
+
rootOwnsDiscoveryPathV1,
|
|
16
|
+
} from "@/lib/publication/mountedDiscovery.js";
|
|
17
|
+
import { rootPostDocumentsV1 } from "@/lib/publication/rootDiscovery.js";
|
|
18
|
+
|
|
19
|
+
interface FeedItem {
|
|
20
|
+
id: string;
|
|
21
|
+
url: string;
|
|
22
|
+
title: string;
|
|
23
|
+
summary?: string;
|
|
24
|
+
content_text: string;
|
|
25
|
+
date_published?: string;
|
|
26
|
+
date_modified?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function item(url: string, title: string, summary: string, text: string,
|
|
30
|
+
publishedAt?: string, updatedAt?: string): FeedItem {
|
|
31
|
+
const published = feedInstantV1(publishedAt);
|
|
32
|
+
const modified = feedInstantV1(updatedAt);
|
|
33
|
+
return {
|
|
34
|
+
id: url,
|
|
35
|
+
url,
|
|
36
|
+
title,
|
|
37
|
+
...(summary ? { summary } : {}),
|
|
38
|
+
content_text: text || summary || title,
|
|
39
|
+
...(published ? { date_published: published } : {}),
|
|
40
|
+
...(modified ? { date_modified: modified } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
45
|
+
const { tenant, canonicalBase } = context.locals;
|
|
46
|
+
const base = canonicalBase.replace(/\/$/, "");
|
|
47
|
+
const index = declaresMountedSpacesV1(tenant) ? compileRouteTierIndexV1(tenant) : null;
|
|
48
|
+
|
|
49
|
+
const items: FeedItem[] = (await rootPostDocumentsV1(context.locals))
|
|
50
|
+
.filter((doc) => !index || rootOwnsDiscoveryPathV1(index, doc.path))
|
|
51
|
+
.map((doc) => item(`${base}${doc.path}`, doc.title, doc.summary, doc.searchText, doc.publishedAt, doc.updatedAt));
|
|
52
|
+
if (index) {
|
|
53
|
+
const mounted = await readMountedPublicationDiscoveryV1({ tenant, store: await readKv("TENANT_CACHE"), index });
|
|
54
|
+
for (const entry of mounted) {
|
|
55
|
+
if (!entry.surfaces.feeds) continue;
|
|
56
|
+
items.push(item(`${base}${entry.canonicalPath}`, entry.title, entry.summary, entry.searchText,
|
|
57
|
+
entry.publishedAt, entry.updatedAt));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const body = `${JSON.stringify({
|
|
62
|
+
version: "https://jsonfeed.org/version/1.1",
|
|
63
|
+
title: tenant.displayName,
|
|
64
|
+
home_page_url: `${base}/`,
|
|
65
|
+
feed_url: `${base}/feed.json`,
|
|
66
|
+
items,
|
|
67
|
+
}, null, 2)}\n`;
|
|
68
|
+
return new Response(body, {
|
|
69
|
+
headers: {
|
|
70
|
+
"content-type": "application/json; charset=utf-8",
|
|
71
|
+
"cache-control": "public, max-age=300, s-maxage=3600",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
import type { APIContext } from "astro";
|
|
13
13
|
import { nonEmptyCollections } from "@/lib/tot/query.js";
|
|
14
14
|
import { listBlogPosts } from "@/lib/blog/provider.js";
|
|
15
|
+
import { compileRouteTierIndexV1 } from "@tot/public-runtime";
|
|
16
|
+
import { readKv } from "@/lib/runtimeEnv.js";
|
|
17
|
+
import {
|
|
18
|
+
declaresMountedSpacesV1,
|
|
19
|
+
readMountedPublicationDiscoveryV1,
|
|
20
|
+
rootOwnsDiscoveryPathV1,
|
|
21
|
+
} from "@/lib/publication/mountedDiscovery.js";
|
|
15
22
|
|
|
16
23
|
/** Collapse HTML/whitespace to a single trimmed line for a link description. */
|
|
17
24
|
function oneLine(s: string | undefined, max = 140): string | undefined {
|
|
@@ -27,7 +34,7 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
27
34
|
|
|
28
35
|
// Use the request-resolved ContentProvider (locals.content) so any published /
|
|
29
36
|
// dev-draft overlay is reflected, matching sitemap.xml.ts (not a fresh provider).
|
|
30
|
-
const [allCollections, activeProducts,
|
|
37
|
+
const [allCollections, activeProducts, rootPages, featured, home, rootBlogPosts] =
|
|
31
38
|
await Promise.all([
|
|
32
39
|
tot.listCollections(),
|
|
33
40
|
tot.getAllActiveProducts(),
|
|
@@ -39,6 +46,21 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
39
46
|
// Empty synthesized collections (no active members) never make the guide —
|
|
40
47
|
// an LLM shouldn't be pointed at a URL that resolves to zero products.
|
|
41
48
|
const collections = nonEmptyCollections(allCollections, activeProducts);
|
|
49
|
+
// Mounted publication spaces: root pages never list into a mount; each public
|
|
50
|
+
// mount's own entries follow the root sections (an internal mount lists nothing).
|
|
51
|
+
const hasMounts = declaresMountedSpacesV1(tenant);
|
|
52
|
+
const index = hasMounts ? compileRouteTierIndexV1(tenant) : null;
|
|
53
|
+
const pages = index
|
|
54
|
+
? rootPages.filter((pg) => rootOwnsDiscoveryPathV1(index, `/${pg.slug}`))
|
|
55
|
+
: rootPages;
|
|
56
|
+
const rootOwnsBlogIndex = !index || rootOwnsDiscoveryPathV1(index, "/blog/");
|
|
57
|
+
const blogPosts = index
|
|
58
|
+
? rootBlogPosts.filter((post) => rootOwnsDiscoveryPathV1(index, `/blog/${post.slug}/`))
|
|
59
|
+
: rootBlogPosts;
|
|
60
|
+
const mounted = index
|
|
61
|
+
? (await readMountedPublicationDiscoveryV1({ tenant, store: await readKv("TENANT_CACHE"), index }))
|
|
62
|
+
.filter((entry) => entry.surfaces.llms)
|
|
63
|
+
: [];
|
|
42
64
|
|
|
43
65
|
const lines: string[] = [`# ${tenant.displayName}`, ""];
|
|
44
66
|
|
|
@@ -85,7 +107,7 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
85
107
|
|
|
86
108
|
if (blogPosts.length) {
|
|
87
109
|
lines.push("## Blog");
|
|
88
|
-
lines.push(`- [All articles](${base}/blog/)`);
|
|
110
|
+
if (rootOwnsBlogIndex) lines.push(`- [All articles](${base}/blog/)`);
|
|
89
111
|
for (const post of blogPosts) {
|
|
90
112
|
const desc = oneLine(post.description || post.excerpt);
|
|
91
113
|
lines.push(`- [${post.title}](${base}/blog/${post.slug}/)${desc ? `: ${desc}` : ""}`);
|
|
@@ -93,6 +115,15 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
93
115
|
lines.push("");
|
|
94
116
|
}
|
|
95
117
|
|
|
118
|
+
if (mounted.length) {
|
|
119
|
+
lines.push("## More content");
|
|
120
|
+
for (const entry of mounted) {
|
|
121
|
+
const desc = oneLine(entry.summary);
|
|
122
|
+
lines.push(`- [${oneLine(entry.title, 200) ?? entry.title}](${base}${entry.canonicalPath})${desc ? `: ${desc}` : ""}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
lines.push("## Optional");
|
|
97
128
|
lines.push(`- [Full product catalog (XML sitemap)](${base}/sitemap.xml)`);
|
|
98
129
|
lines.push(`- [Search](${base}/search)`);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-tenant public search index: the root's home, editorial and indexable raw
|
|
3
|
+
* pages, and listable posts, plus every entry a public mounted publication
|
|
4
|
+
* space declares for public search. An internal space lists nothing. Catalog
|
|
5
|
+
* search stays on `/search`; products are not indexed here.
|
|
6
|
+
*/
|
|
7
|
+
import type { APIContext } from "astro";
|
|
8
|
+
import { compileRouteTierIndexV1 } from "@tot/public-runtime";
|
|
9
|
+
import { readKv } from "@/lib/runtimeEnv.js";
|
|
10
|
+
import {
|
|
11
|
+
declaresMountedSpacesV1,
|
|
12
|
+
readMountedPublicationDiscoveryV1,
|
|
13
|
+
rootOwnsDiscoveryPathV1,
|
|
14
|
+
} from "@/lib/publication/mountedDiscovery.js";
|
|
15
|
+
import { rootSearchDocumentsV1 } from "@/lib/publication/rootDiscovery.js";
|
|
16
|
+
|
|
17
|
+
interface SearchEntry {
|
|
18
|
+
canonicalPath: string;
|
|
19
|
+
canonicalUrl: string;
|
|
20
|
+
title: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
searchText: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
26
|
+
const { tenant, canonicalBase } = context.locals;
|
|
27
|
+
const base = canonicalBase.replace(/\/$/, "");
|
|
28
|
+
const index = declaresMountedSpacesV1(tenant) ? compileRouteTierIndexV1(tenant) : null;
|
|
29
|
+
|
|
30
|
+
const entries: SearchEntry[] = (await rootSearchDocumentsV1(context.locals))
|
|
31
|
+
.filter((doc) => !index || rootOwnsDiscoveryPathV1(index, doc.path))
|
|
32
|
+
.map((doc) => ({ canonicalPath: doc.path, canonicalUrl: `${base}${doc.path}`, title: doc.title,
|
|
33
|
+
summary: doc.summary, searchText: doc.searchText }));
|
|
34
|
+
if (index) {
|
|
35
|
+
const mounted = await readMountedPublicationDiscoveryV1({ tenant, store: await readKv("TENANT_CACHE"), index });
|
|
36
|
+
for (const entry of mounted) {
|
|
37
|
+
if (!entry.surfaces.publicSearch) continue;
|
|
38
|
+
entries.push({ canonicalPath: entry.canonicalPath, canonicalUrl: `${base}${entry.canonicalPath}`,
|
|
39
|
+
title: entry.title, summary: entry.summary, searchText: entry.searchText });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new Response(`${JSON.stringify({ version: 1, entries }, null, 2)}\n`, {
|
|
44
|
+
headers: {
|
|
45
|
+
"content-type": "application/json; charset=utf-8",
|
|
46
|
+
"cache-control": "public, max-age=300, s-maxage=3600",
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -16,6 +16,13 @@ import { resolveBlogIndexCopy } from "@/lib/blog/pagination.js";
|
|
|
16
16
|
import { getTheBuildSettings, listBuildPosts } from "@/lib/the-build/provider.js";
|
|
17
17
|
import { resolveTheBuildIndexCopy } from "@/lib/the-build/pagination.js";
|
|
18
18
|
import { isIndexableHtml, rawPageCanonicalUrl } from "@/lib/rawMarketingHtml.js";
|
|
19
|
+
import { compileRouteTierIndexV1 } from "@tot/public-runtime";
|
|
20
|
+
import { readKv } from "@/lib/runtimeEnv.js";
|
|
21
|
+
import {
|
|
22
|
+
declaresMountedSpacesV1,
|
|
23
|
+
readMountedPublicationDiscoveryV1,
|
|
24
|
+
rootOwnsDiscoveryPathV1,
|
|
25
|
+
} from "@/lib/publication/mountedDiscovery.js";
|
|
19
26
|
|
|
20
27
|
interface SitemapUrl {
|
|
21
28
|
loc: string;
|
|
@@ -119,6 +126,25 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
// Mounted publication spaces: root rows never list into a mount, and each
|
|
130
|
+
// public mount lists its own entries (an internal mount lists nothing).
|
|
131
|
+
if (declaresMountedSpacesV1(tenant)) {
|
|
132
|
+
const index = compileRouteTierIndexV1(tenant);
|
|
133
|
+
const rootRows = urls.filter((u) => rootOwnsDiscoveryPathV1(index, u.loc.slice(base.length) || "/"));
|
|
134
|
+
urls.length = 0;
|
|
135
|
+
urls.push(...rootRows);
|
|
136
|
+
const listed = new Set(urls.map((u) => u.loc));
|
|
137
|
+
const mounted = await readMountedPublicationDiscoveryV1({ tenant, store: await readKv("TENANT_CACHE"), index });
|
|
138
|
+
for (const entry of mounted) {
|
|
139
|
+
if (!entry.surfaces.sitemap) continue;
|
|
140
|
+
const loc = `${base}${entry.canonicalPath}`;
|
|
141
|
+
if (listed.has(loc)) continue;
|
|
142
|
+
listed.add(loc);
|
|
143
|
+
const lastmod = entry.updatedAt ?? entry.publishedAt;
|
|
144
|
+
urls.push(lastmod ? { loc, lastmod } : { loc });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
123
149
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
124
150
|
${urls
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.20",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|