Use reference-backed hyperzine manifest
This commit is contained in:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "hyperzine",
|
"name": "hyperzine",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@permaweb/references": "^0.1.1",
|
||||||
"arweave": "^1.15.7",
|
"arweave": "^1.15.7",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
@@ -811,6 +812,24 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@permaweb/references": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@permaweb/references/-/references-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-N3raK74AS339mJB8ho00oLUYpTRlZw4gL3qjxA1YyKyueKWQzh3099ovW7AgFftev94HkDgSZA2BpXzb1hpWSA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"arbundles": "*",
|
||||||
|
"arweave": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"arbundles": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"arweave": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"ship": "node scripts/ship.mjs"
|
"ship": "node scripts/ship.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@permaweb/references": "^0.1.1",
|
||||||
"arweave": "^1.15.7",
|
"arweave": "^1.15.7",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { spawn } from "node:child_process";
|
|||||||
import Arweave from "arweave";
|
import Arweave from "arweave";
|
||||||
import mime from "mime-types";
|
import mime from "mime-types";
|
||||||
import arbundles from "warp-arbundles";
|
import arbundles from "warp-arbundles";
|
||||||
|
import { ReferenceClient } from "@permaweb/references";
|
||||||
|
|
||||||
const { createData, ArweaveSigner } = arbundles;
|
const { createData, ArweaveSigner } = arbundles;
|
||||||
|
|
||||||
@@ -228,6 +229,25 @@ async function resolveBlogManifestTxId() {
|
|||||||
return match?.[1] || "";
|
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() {
|
async function resolveAoConfig() {
|
||||||
const source = await fs.readFile(sourceConfigPath, "utf8");
|
const source = await fs.readFile(sourceConfigPath, "utf8");
|
||||||
const aoUrlMatch = source.match(/AO_URL\s*=\s*"([^"]+)"/);
|
const aoUrlMatch = source.match(/AO_URL\s*=\s*"([^"]+)"/);
|
||||||
@@ -308,21 +328,51 @@ async function getLatestManifestFromAo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolvePostSlugs(gateway) {
|
function timestampMs(value) {
|
||||||
const blogManifestTxId = (await getLatestManifestFromAo()) || (await resolveBlogManifestTxId());
|
if (typeof value !== "string" || !value) return 0;
|
||||||
if (!blogManifestTxId) return [];
|
const parsed = Date.parse(value);
|
||||||
|
return Number.isNaN(parsed) ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
function manifestFreshness(payload) {
|
||||||
const response = await fetch(`${gateway.replace(/\/+$/, "")}/${blogManifestTxId}`);
|
const posts = Array.isArray(payload?.posts) ? payload.posts : [];
|
||||||
if (!response.ok) return [];
|
return Math.max(
|
||||||
const payload = await response.json();
|
timestampMs(payload?.updated),
|
||||||
if (!Array.isArray(payload?.posts)) return [];
|
timestampMs(payload?.date),
|
||||||
return payload.posts
|
timestampMs(payload?.generatedAt),
|
||||||
.map((post) => (typeof post?.slug === "string" ? post.slug.trim() : ""))
|
...posts.map((post) => Math.max(timestampMs(post?.updated), timestampMs(post?.publishedAt)))
|
||||||
.filter((slug) => slug.length > 0);
|
);
|
||||||
} catch {
|
}
|
||||||
return [];
|
|
||||||
|
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() {
|
async function main() {
|
||||||
@@ -396,8 +446,9 @@ async function main() {
|
|||||||
|
|
||||||
const slugs = await resolvePostSlugs(gateway);
|
const slugs = await resolvePostSlugs(gateway);
|
||||||
for (const slug of slugs) {
|
for (const slug of slugs) {
|
||||||
pathMap[slug] = { id: indexTxId };
|
const routeIndexId = pathMap[`${slug}/index.html`]?.id || indexTxId;
|
||||||
pathMap[`${slug}/`] = { id: indexTxId };
|
pathMap[slug] = { id: routeIndexId };
|
||||||
|
pathMap[`${slug}/`] = { id: routeIndexId };
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { ReferenceClient } from "@permaweb/references";
|
||||||
|
|
||||||
const ROOT = process.cwd();
|
const ROOT = process.cwd();
|
||||||
const DIST_DIR = path.join(ROOT, "dist");
|
const DIST_DIR = path.join(ROOT, "dist");
|
||||||
@@ -10,6 +11,8 @@ const DEFAULTS = {
|
|||||||
BLOG_NAME: "hyperzine",
|
BLOG_NAME: "hyperzine",
|
||||||
BLOG_SITE_URL: "https://hyperzine.xyz",
|
BLOG_SITE_URL: "https://hyperzine.xyz",
|
||||||
BLOG_DEFAULT_DESCRIPTION: "hyperzine",
|
BLOG_DEFAULT_DESCRIPTION: "hyperzine",
|
||||||
|
ARWEAVE_GATEWAY: "https://arweave.net",
|
||||||
|
MANIFEST_REFERENCE_NAME: "",
|
||||||
MANIFEST_TX_ID: ""
|
MANIFEST_TX_ID: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +53,8 @@ const parseConfig = async () => {
|
|||||||
BLOG_NAME: pick("BLOG_NAME"),
|
BLOG_NAME: pick("BLOG_NAME"),
|
||||||
BLOG_SITE_URL: pick("BLOG_SITE_URL"),
|
BLOG_SITE_URL: pick("BLOG_SITE_URL"),
|
||||||
BLOG_DEFAULT_DESCRIPTION: pick("BLOG_DEFAULT_DESCRIPTION"),
|
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")
|
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) => {
|
const fetchManifestPosts = async (manifestTxId) => {
|
||||||
if (!manifestTxId) return [];
|
if (!manifestTxId) return [];
|
||||||
const url = `https://arweave.net/${manifestTxId}`;
|
const url = `https://arweave.net/${manifestTxId}`;
|
||||||
const response = await fetch(url, { cache: "no-store" });
|
const response = await fetch(url, { cache: "no-store" });
|
||||||
if (!response.ok) throw new Error(`Manifest fetch failed (${response.status})`);
|
if (!response.ok) throw new Error(`Manifest fetch failed (${response.status})`);
|
||||||
const payload = await response.json();
|
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 = ({
|
const buildMetaTags = ({
|
||||||
@@ -197,7 +270,7 @@ const main = async () => {
|
|||||||
|
|
||||||
let posts = [];
|
let posts = [];
|
||||||
try {
|
try {
|
||||||
posts = await fetchManifestPosts(config.MANIFEST_TX_ID);
|
posts = await fetchBestManifestPosts(config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[prerender-meta] Could not fetch manifest; generating root metadata only: ${error.message}`
|
`[prerender-meta] Could not fetch manifest; generating root metadata only: ${error.message}`
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
export const BLOG_NAME = "hyperzine";
|
export const BLOG_NAME = "hyperzine";
|
||||||
export const BLOG_SITE_URL = "https://386900368387k.arweave.net/"
|
export const BLOG_SITE_URL = "https://hyperzine.xyz";
|
||||||
export const BLOG_PERMALINK_PREFIX = "https://386900368387k.arweave.net"
|
export const BLOG_PERMALINK_PREFIX = "https://hyperzine.xyz";
|
||||||
export const BLOG_DEFAULT_DESCRIPTION =
|
export const BLOG_DEFAULT_DESCRIPTION =
|
||||||
"A blog about cyberspace decentralization";
|
"A blog about cyberspace decentralization";
|
||||||
export const BLOG_TWITTER_HANDLE = "";
|
export const BLOG_TWITTER_HANDLE = "";
|
||||||
export const MANIFEST_TX_ID = "yVgW1MbkWYc76paeQZ92RK4GmzCXYiFLn_Z_1K7RzrE";
|
export const MANIFEST_REFERENCE_NAME = "mystic";
|
||||||
|
export const MANIFEST_TX_ID = "SShELKFa2EsiB-OJbeoNOjAIxgah9OmRhxYXBF_M9wA";
|
||||||
export const ARWEAVE_GATEWAY = "https://arweave.net";
|
export const ARWEAVE_GATEWAY = "https://arweave.net";
|
||||||
|
|
||||||
export const AO_URL = "https://push-1.forward.computer";
|
export const AO_URL = "https://push-1.forward.computer";
|
||||||
|
|||||||
74
src/lib.ts
74
src/lib.ts
@@ -1,7 +1,8 @@
|
|||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { marked, type Tokens } from "marked";
|
import { marked, type Tokens } from "marked";
|
||||||
import { parse as parseYaml } from "yaml";
|
import { parse as parseYaml } from "yaml";
|
||||||
import { ARWEAVE_GATEWAY, MANIFEST_TX_ID } from "./config";
|
import { ReferenceClient } from "@permaweb/references";
|
||||||
|
import { ARWEAVE_GATEWAY, MANIFEST_REFERENCE_NAME, MANIFEST_TX_ID } from "./config";
|
||||||
import { parseManifest, type Frontmatter, type ManifestPost } from "./types";
|
import { parseManifest, type Frontmatter, type ManifestPost } from "./types";
|
||||||
|
|
||||||
const formatDate = (date: string): string =>
|
const formatDate = (date: string): string =>
|
||||||
@@ -72,13 +73,80 @@ export const getReadableDate = (date?: string | null): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const loadManifest = async (): Promise<ManifestPost[]> => {
|
export const loadManifest = async (): Promise<ManifestPost[]> => {
|
||||||
const response = await fetch(arweaveUrl(MANIFEST_TX_ID));
|
return (await loadBestManifest()).posts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const timestampMs = (value: unknown): number => {
|
||||||
|
if (typeof value !== "string" || !value) return 0;
|
||||||
|
const parsed = Date.parse(value);
|
||||||
|
return Number.isNaN(parsed) ? 0 : parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifestFreshness = (payload: unknown, posts: ManifestPost[]): number => {
|
||||||
|
const root = isObject(payload) ? payload : {};
|
||||||
|
return Math.max(
|
||||||
|
timestampMs(root.updated),
|
||||||
|
timestampMs(root.date),
|
||||||
|
timestampMs(root.generatedAt),
|
||||||
|
...posts.map((post) => Math.max(timestampMs(post.updated), timestampMs(post.publishedAt)))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchManifest = async (manifestTxId: string): Promise<{
|
||||||
|
txId: string;
|
||||||
|
posts: ManifestPost[];
|
||||||
|
freshness: number;
|
||||||
|
}> => {
|
||||||
|
const response = await fetch(arweaveUrl(manifestTxId));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load manifest (${response.status})`);
|
throw new Error(`Failed to load manifest (${response.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: unknown = await response.json();
|
const payload: unknown = await response.json();
|
||||||
return parseManifest(payload);
|
const posts = parseManifest(payload);
|
||||||
|
return { txId: manifestTxId, posts, freshness: manifestFreshness(payload, posts) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveManifestCandidates = async (): Promise<string[]> => {
|
||||||
|
const candidates: string[] = [];
|
||||||
|
if (MANIFEST_REFERENCE_NAME) {
|
||||||
|
try {
|
||||||
|
const names = new ReferenceClient({ gateway: ARWEAVE_GATEWAY });
|
||||||
|
const value = await names.resolveName(MANIFEST_REFERENCE_NAME);
|
||||||
|
if (typeof value === "string" && value.length > 0) candidates.push(value);
|
||||||
|
else throw new Error(`Reference ${MANIFEST_REFERENCE_NAME} did not resolve to a manifest id`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Could not resolve manifest reference ${MANIFEST_REFERENCE_NAME}; trying configured manifest`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MANIFEST_TX_ID) candidates.push(MANIFEST_TX_ID);
|
||||||
|
return [...new Set(candidates)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadBestManifest = async () => {
|
||||||
|
const candidates = await resolveManifestCandidates();
|
||||||
|
if (!candidates.length) throw new Error("No manifest reference or fallback manifest configured");
|
||||||
|
|
||||||
|
const loaded = [];
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
loaded.push(await fetchManifest(candidate));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Could not load manifest ${candidate}; trying other configured candidates`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loaded.length) throw new Error("No configured manifest could be loaded");
|
||||||
|
loaded.sort((a, b) => b.freshness - a.freshness);
|
||||||
|
return loaded[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveManifestTxId = async (): Promise<string> => {
|
||||||
|
return (await loadBestManifest()).txId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
|||||||
Reference in New Issue
Block a user