@tokenoftrust/storefront-runner 2.0.15 → 2.0.16
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.
|
@@ -156,6 +156,9 @@ function toSummary(p: BlogPost): BlogPostSummary {
|
|
|
156
156
|
tags: p.tags,
|
|
157
157
|
status: p.status,
|
|
158
158
|
...(p.scheduledAt ? { scheduledAt: p.scheduledAt } : {}),
|
|
159
|
+
// Carried, not dropped: a listing renders the entitlement badge off this,
|
|
160
|
+
// so omitting it makes a members-only post read as public in every listing.
|
|
161
|
+
...(p.access ? { access: p.access } : {}),
|
|
159
162
|
...(p.featuredImage ? { featuredImage: p.featuredImage } : {}),
|
|
160
163
|
...(p.canonicalUrl ? { canonicalUrl: p.canonicalUrl } : {}),
|
|
161
164
|
...(p.inLanguage ? { inLanguage: p.inLanguage } : {}),
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// The live WordPress `/blog` deliberately keeps reading its committed, curated
|
|
2
2
|
// index.json (its order is hand-maintained and SEO-load-bearing). The generic
|
|
3
3
|
// derived engine lives in ./collection.ts and powers `the-build` + any new
|
|
4
|
-
// collection
|
|
4
|
+
// collection, and backs `/blog` whenever that curated index is unusable;
|
|
5
|
+
// switching `/blog` onto it outright is a later migration once order is
|
|
5
6
|
// encoded as data (see B0.1 §"/blog migration caution").
|
|
6
7
|
import { tenantDirSegment } from "@tot/public-runtime";
|
|
8
|
+
import { listPosts } from "./collection.js";
|
|
7
9
|
import type { BlogPost, BlogPostSummary } from "./types.js";
|
|
8
10
|
import type { ContentProvider } from "../storyblok/provider.js";
|
|
9
11
|
|
|
12
|
+
/** The collection id `/blog` reads — the derived fallback's second argument. */
|
|
13
|
+
const BLOG_COLLECTION = "blog";
|
|
14
|
+
|
|
10
15
|
type ArtifactSource = Pick<ContentProvider, "readArtifact" | "listArtifactPaths">;
|
|
11
16
|
|
|
12
17
|
function canReadArtifacts(value: ArtifactSource | undefined): value is ArtifactSource {
|
|
@@ -43,25 +48,54 @@ function tenantKey(keys: string[], tenantId: string, suffix: string): string | u
|
|
|
43
48
|
return keys.find((key) => key.includes(segment) && key.endsWith(suffix));
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
/** A curated index is usable only if it parses AND is the array shape it claims. */
|
|
52
|
+
function parseCuratedIndex(raw: string): BlogPostSummary[] | null {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
55
|
+
return Array.isArray(parsed) ? (parsed as BlogPostSummary[]) : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The curated `index.json` for this tenant, or null when it is absent, malformed,
|
|
63
|
+
* or not an array. Null is "cannot use the curated order", never "no posts".
|
|
64
|
+
*/
|
|
65
|
+
async function readCuratedIndex(
|
|
47
66
|
tenantId: string,
|
|
48
67
|
source?: ArtifactSource,
|
|
49
|
-
): Promise<BlogPostSummary[]> {
|
|
68
|
+
): Promise<BlogPostSummary[] | null> {
|
|
50
69
|
if (canReadArtifacts(source)) {
|
|
51
|
-
const paths = await source.listArtifactPaths("blog/");
|
|
52
70
|
const raw = await source.readArtifact("blog/index.json");
|
|
53
|
-
if (raw)
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(raw) as BlogPostSummary[];
|
|
56
|
-
} catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if (paths !== null) return [];
|
|
71
|
+
if (raw) return parseCuratedIndex(raw);
|
|
61
72
|
}
|
|
62
73
|
const key = tenantKey(Object.keys(indexGlobs), tenantId, "/index.json")
|
|
63
74
|
?? Object.keys(indexGlobs).find((candidate) => candidate.includes(`${tenantDirSegment(tenantId)}content/blog/index.json`));
|
|
64
|
-
return key ? indexGlobs[key]!() :
|
|
75
|
+
return key ? (await indexGlobs[key]!()) ?? null : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The curated index stays the preferred source — its order is hand-maintained
|
|
80
|
+
* and SEO-load-bearing — but it must never be the blog's single point of
|
|
81
|
+
* failure. One malformed byte in `content/blog/index.json` makes the reconcile's
|
|
82
|
+
* JSON validator advisory-skip it, and the whole surface (index, taxonomy pages,
|
|
83
|
+
* RSS, sitemap entries) then reads as an empty collection and hard-404s while
|
|
84
|
+
* every `posts/*.json` sits materialized beside it.
|
|
85
|
+
*
|
|
86
|
+
* So an unusable curated index falls back to the derived index over
|
|
87
|
+
* `blog/posts/*.json` (./collection.ts): newest-first rather than curated order,
|
|
88
|
+
* and every article still reachable and listed. Whether the curated index is
|
|
89
|
+
* authoritative or derived, the runtime/build-time source selection is identical
|
|
90
|
+
* — `listPosts` applies the same "artifact paths, else build-time globs" rule.
|
|
91
|
+
*/
|
|
92
|
+
export async function listBlogPosts(
|
|
93
|
+
tenantId: string,
|
|
94
|
+
source?: ArtifactSource,
|
|
95
|
+
): Promise<BlogPostSummary[]> {
|
|
96
|
+
const curated = await readCuratedIndex(tenantId, source);
|
|
97
|
+
if (curated && curated.length > 0) return curated;
|
|
98
|
+
return listPosts(tenantId, BLOG_COLLECTION, { content: source });
|
|
65
99
|
}
|
|
66
100
|
|
|
67
101
|
export async function getBlogPost(
|
|
@@ -5,7 +5,16 @@ import { listBlogPosts } from "@/lib/blog/provider.js";
|
|
|
5
5
|
|
|
6
6
|
const { tenant, basePath, canonicalBase, content } = Astro.locals;
|
|
7
7
|
const posts = await listBlogPosts(tenant.tenant_id, content);
|
|
8
|
-
|
|
8
|
+
// A tenant with no blog content has no blog index, so this is still a 404 — but
|
|
9
|
+
// it is a DIFFERENT 404 from "no such route", and telling them apart from the
|
|
10
|
+
// outside used to be impossible (both were bare and byte-identical). The header
|
|
11
|
+
// says which one this is; a nonexistent route never carries it.
|
|
12
|
+
if (posts.length === 0) {
|
|
13
|
+
return new Response(null, {
|
|
14
|
+
status: 404,
|
|
15
|
+
headers: { "x-tot-blog-collection": "empty" },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
const href = (path: string) => `${basePath}${path}` || "/";
|
|
11
20
|
const tags = [...new Set(posts.flatMap((post) => post.tags))].sort((a, b) => a.localeCompare(b));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
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",
|