Use reference-backed hyperzine manifest

This commit is contained in:
fn
2026-06-08 18:43:12 +01:00
parent ac3b7d7ce9
commit d02bca8646
6 changed files with 236 additions and 23 deletions

View File

@@ -5,6 +5,7 @@ import { spawn } from "node:child_process";
import Arweave from "arweave";
import mime from "mime-types";
import arbundles from "warp-arbundles";
import { ReferenceClient } from "@permaweb/references";
const { createData, ArweaveSigner } = arbundles;
@@ -228,6 +229,25 @@ async function resolveBlogManifestTxId() {
return match?.[1] || "";
}
async function resolveBlogManifestReferenceName() {
const source = await fs.readFile(sourceConfigPath, "utf8");
const match = source.match(/MANIFEST_REFERENCE_NAME\s*=\s*"([^"]+)"/);
return match?.[1] || "";
}
async function getLatestManifestFromReference(gateway) {
const referenceName = await resolveBlogManifestReferenceName();
if (!referenceName) return "";
try {
const names = new ReferenceClient({ gateway });
const value = await names.resolveName(referenceName);
return typeof value === "string" ? value : "";
} catch {
return "";
}
}
async function resolveAoConfig() {
const source = await fs.readFile(sourceConfigPath, "utf8");
const aoUrlMatch = source.match(/AO_URL\s*=\s*"([^"]+)"/);
@@ -308,21 +328,51 @@ async function getLatestManifestFromAo() {
}
}
async function resolvePostSlugs(gateway) {
const blogManifestTxId = (await getLatestManifestFromAo()) || (await resolveBlogManifestTxId());
if (!blogManifestTxId) return [];
function timestampMs(value) {
if (typeof value !== "string" || !value) return 0;
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
try {
const response = await fetch(`${gateway.replace(/\/+$/, "")}/${blogManifestTxId}`);
if (!response.ok) return [];
const payload = await response.json();
if (!Array.isArray(payload?.posts)) return [];
return payload.posts
.map((post) => (typeof post?.slug === "string" ? post.slug.trim() : ""))
.filter((slug) => slug.length > 0);
} catch {
return [];
function manifestFreshness(payload) {
const posts = Array.isArray(payload?.posts) ? payload.posts : [];
return Math.max(
timestampMs(payload?.updated),
timestampMs(payload?.date),
timestampMs(payload?.generatedAt),
...posts.map((post) => Math.max(timestampMs(post?.updated), timestampMs(post?.publishedAt)))
);
}
async function resolvePostSlugs(gateway) {
const candidates = [
await getLatestManifestFromReference(gateway),
await getLatestManifestFromAo(),
await resolveBlogManifestTxId()
].filter(Boolean);
const manifests = [];
for (const blogManifestTxId of candidates) {
try {
const response = await fetch(`${gateway.replace(/\/+$/, "")}/${blogManifestTxId}`);
if (!response.ok) continue;
const payload = await response.json();
if (!Array.isArray(payload?.posts)) continue;
manifests.push({
txId: blogManifestTxId,
freshness: manifestFreshness(payload),
posts: payload.posts
});
} catch {
// Try the next manifest source.
}
}
if (!manifests.length) return [];
manifests.sort((a, b) => b.freshness - a.freshness);
return manifests[0].posts
.map((post) => (typeof post?.slug === "string" ? post.slug.trim() : ""))
.filter((slug) => slug.length > 0);
}
async function main() {
@@ -396,8 +446,9 @@ async function main() {
const slugs = await resolvePostSlugs(gateway);
for (const slug of slugs) {
pathMap[slug] = { id: indexTxId };
pathMap[`${slug}/`] = { id: indexTxId };
const routeIndexId = pathMap[`${slug}/index.html`]?.id || indexTxId;
pathMap[slug] = { id: routeIndexId };
pathMap[`${slug}/`] = { id: routeIndexId };
}
const manifest = {

View File

@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { ReferenceClient } from "@permaweb/references";
const ROOT = process.cwd();
const DIST_DIR = path.join(ROOT, "dist");
@@ -10,6 +11,8 @@ const DEFAULTS = {
BLOG_NAME: "hyperzine",
BLOG_SITE_URL: "https://hyperzine.xyz",
BLOG_DEFAULT_DESCRIPTION: "hyperzine",
ARWEAVE_GATEWAY: "https://arweave.net",
MANIFEST_REFERENCE_NAME: "",
MANIFEST_TX_ID: ""
};
@@ -50,6 +53,8 @@ const parseConfig = async () => {
BLOG_NAME: pick("BLOG_NAME"),
BLOG_SITE_URL: pick("BLOG_SITE_URL"),
BLOG_DEFAULT_DESCRIPTION: pick("BLOG_DEFAULT_DESCRIPTION"),
ARWEAVE_GATEWAY: pick("ARWEAVE_GATEWAY"),
MANIFEST_REFERENCE_NAME: pick("MANIFEST_REFERENCE_NAME"),
MANIFEST_TX_ID: pick("MANIFEST_TX_ID")
};
};
@@ -116,13 +121,81 @@ const parseManifest = (input) => {
});
};
const timestampMs = (value) => {
if (typeof value !== "string" || !value) return 0;
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
};
const manifestFreshness = (payload, posts) =>
Math.max(
timestampMs(payload?.updated),
timestampMs(payload?.date),
timestampMs(payload?.generatedAt),
...posts.map((post) => Math.max(timestampMs(post.updated), timestampMs(post.publishedAt)))
);
const fetchManifestPosts = async (manifestTxId) => {
if (!manifestTxId) return [];
const url = `https://arweave.net/${manifestTxId}`;
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) throw new Error(`Manifest fetch failed (${response.status})`);
const payload = await response.json();
return parseManifest(payload);
const posts = parseManifest(payload);
return { txId: manifestTxId, posts, freshness: manifestFreshness(payload, posts) };
};
const resolveManifestTxIds = async (config) => {
const candidates = [];
if (config.MANIFEST_REFERENCE_NAME) {
try {
const names = new ReferenceClient({ gateway: config.ARWEAVE_GATEWAY || DEFAULTS.ARWEAVE_GATEWAY });
const value = await names.resolveName(config.MANIFEST_REFERENCE_NAME);
if (typeof value === "string" && value.length > 0) candidates.push(value);
else throw new Error(`Reference ${config.MANIFEST_REFERENCE_NAME} did not resolve to a manifest id`);
} catch (error) {
console.warn(
`[prerender-meta] Trying configured manifest; reference ${config.MANIFEST_REFERENCE_NAME} failed: ${error.message}`
);
}
}
if (config.MANIFEST_TX_ID) candidates.push(config.MANIFEST_TX_ID);
return [...new Set(candidates)];
};
const fetchBestManifestPosts = async (config) => {
const candidates = await resolveManifestTxIds(config);
const loaded = [];
for (const manifestTxId of candidates) {
try {
loaded.push(await fetchManifestPosts(manifestTxId));
} catch (error) {
console.warn(`[prerender-meta] Manifest ${manifestTxId} failed: ${error.message}`);
}
}
if (!loaded.length) return [];
loaded.sort((a, b) => b.freshness - a.freshness);
return loaded[0].posts;
};
const resolveManifestTxId = async (config) => {
if (config.MANIFEST_REFERENCE_NAME) {
try {
const names = new ReferenceClient({ gateway: config.ARWEAVE_GATEWAY || DEFAULTS.ARWEAVE_GATEWAY });
const value = await names.resolveName(config.MANIFEST_REFERENCE_NAME);
if (typeof value === "string" && value.length > 0) return value;
throw new Error(`Reference ${config.MANIFEST_REFERENCE_NAME} did not resolve to a manifest id`);
} catch (error) {
console.warn(
`[prerender-meta] Falling back to configured manifest; reference ${config.MANIFEST_REFERENCE_NAME} failed: ${error.message}`
);
}
}
return config.MANIFEST_TX_ID;
};
const buildMetaTags = ({
@@ -197,7 +270,7 @@ const main = async () => {
let posts = [];
try {
posts = await fetchManifestPosts(config.MANIFEST_TX_ID);
posts = await fetchBestManifestPosts(config);
} catch (error) {
console.warn(
`[prerender-meta] Could not fetch manifest; generating root metadata only: ${error.message}`