Compare commits

...

13 Commits

Author SHA1 Message Date
3618d0aa2d Resolve manifest via the mystic reference only
Drop the hardcoded MANIFEST_TX_ID fallback from the app, the deploy script
and the metadata prerender. The mystic reference now points at the manifest
that includes arweave-for-ao, so the UI picks up new ships without a code
change.
2026-09-03 16:50:16 +01:00
e57f80bad5 fix: titlebar 2026-06-10 16:52:57 +01:00
7db8aff97f Add artifacts content stream 2026-06-10 16:42:24 +01:00
4f0ba35da0 fix: permalink 2026-06-09 12:28:26 +01:00
79a7355668 Bind fetch for browser reference resolution 2026-06-08 19:03:47 +01:00
71a8a23054 Prefer reference manifest over fallback 2026-06-08 18:59:34 +01:00
d02bca8646 Use reference-backed hyperzine manifest 2026-06-08 18:43:12 +01:00
ac3b7d7ce9 Update fallback manifest id 2026-05-28 20:02:06 +01:00
e7361cd907 content: lapee 2026-05-28 18:58:31 +01:00
1f9f4a08a0 chore: update fallback manifest ID 2026-05-06 11:25:11 +01:00
e99f7ffd20 content: ao-the-asset pt3 2026-04-27 17:41:13 +01:00
5a952a8809 content: ao-the-asset pt2 2026-04-27 16:55:22 +01:00
7c9dc57c29 content: ao-the-asset 2026-04-27 15:17:32 +01:00
8 changed files with 495 additions and 148 deletions

19
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
@@ -222,12 +223,25 @@ async function createUploader(uploadMode, arweave, jwk) {
}; };
} }
async function resolveBlogManifestTxId() { async function resolveBlogManifestReferenceName() {
const source = await fs.readFile(sourceConfigPath, "utf8"); const source = await fs.readFile(sourceConfigPath, "utf8");
const match = source.match(/MANIFEST_TX_ID\s*=\s*"([^"]+)"/); const match = source.match(/MANIFEST_REFERENCE_NAME\s*=\s*"([^"]+)"/);
return match?.[1] || ""; 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 +322,35 @@ async function getLatestManifestFromAo() {
} }
} }
async function resolvePostSlugs(gateway) { async function fetchPostSlugs(gateway, blogManifestTxId) {
const blogManifestTxId = (await getLatestManifestFromAo()) || (await resolveBlogManifestTxId()); if (!blogManifestTxId) return null;
if (!blogManifestTxId) return [];
try { const response = await fetch(`${gateway.replace(/\/+$/, "")}/${blogManifestTxId}`);
const response = await fetch(`${gateway.replace(/\/+$/, "")}/${blogManifestTxId}`); if (!response.ok) return null;
if (!response.ok) return []; const payload = await response.json();
const payload = await response.json(); if (!Array.isArray(payload?.posts)) return null;
if (!Array.isArray(payload?.posts)) return [];
return payload.posts return payload.posts
.map((post) => (typeof post?.slug === "string" ? post.slug.trim() : "")) .map((post) => (typeof post?.slug === "string" ? post.slug.trim() : ""))
.filter((slug) => slug.length > 0); .filter((slug) => slug.length > 0);
} catch { }
return [];
async function resolvePostSlugs(gateway) {
const candidates = [
await getLatestManifestFromReference(gateway),
await getLatestManifestFromAo()
].filter(Boolean);
for (const blogManifestTxId of candidates) {
try {
const slugs = await fetchPostSlugs(gateway, blogManifestTxId);
if (slugs) return slugs;
} catch {
// Try the next manifest source.
}
} }
return [];
} }
async function main() { async function main() {
@@ -396,8 +424,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 = {

View File

@@ -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,7 +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",
MANIFEST_TX_ID: "" ARWEAVE_GATEWAY: "https://arweave.net",
MANIFEST_REFERENCE_NAME: ""
}; };
const asObject = (value) => (typeof value === "object" && value !== null ? value : null); const asObject = (value) => (typeof value === "object" && value !== null ? value : null);
@@ -50,7 +52,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"),
MANIFEST_TX_ID: pick("MANIFEST_TX_ID") ARWEAVE_GATEWAY: pick("ARWEAVE_GATEWAY"),
MANIFEST_REFERENCE_NAME: pick("MANIFEST_REFERENCE_NAME")
}; };
}; };
@@ -116,13 +119,28 @@ const parseManifest = (input) => {
}); });
}; };
const fetchManifestPosts = async (manifestTxId) => { const fetchManifestPosts = async (config, manifestTxId) => {
if (!manifestTxId) return []; if (!manifestTxId) return [];
const url = `https://arweave.net/${manifestTxId}`; const gateway = (config.ARWEAVE_GATEWAY || DEFAULTS.ARWEAVE_GATEWAY).replace(/\/+$/, "");
const response = await fetch(url, { cache: "no-store" }); const response = await fetch(`${gateway}/${manifestTxId}`, { 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 };
};
const resolveManifestTxId = async (config) => {
if (!config.MANIFEST_REFERENCE_NAME) return "";
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`);
};
const fetchCurrentManifestPosts = async (config) => {
const manifestTxId = await resolveManifestTxId(config);
if (!manifestTxId) return [];
return (await fetchManifestPosts(config, manifestTxId)).posts;
}; };
const buildMetaTags = ({ const buildMetaTags = ({
@@ -197,7 +215,7 @@ const main = async () => {
let posts = []; let posts = [];
try { try {
posts = await fetchManifestPosts(config.MANIFEST_TX_ID); posts = await fetchCurrentManifestPosts(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}`

View File

@@ -1,9 +1,15 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Link, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom"; import {
Link,
NavLink,
Route,
Routes,
useLocation,
useParams
} from "react-router-dom";
import { import {
BLOG_DEFAULT_DESCRIPTION, BLOG_DEFAULT_DESCRIPTION,
BLOG_NAME, BLOG_NAME,
BLOG_PERMALINK_PREFIX,
BLOG_SITE_URL, BLOG_SITE_URL,
BLOG_TWITTER_HANDLE BLOG_TWITTER_HANDLE
} from "./config"; } from "./config";
@@ -18,6 +24,16 @@ import load6 from "./load6.png";
type LoadState = "idle" | "loading" | "error"; type LoadState = "idle" | "loading" | "error";
declare global {
interface Window {
twttr?: {
widgets?: {
load: (element?: HTMLElement | null) => void | Promise<unknown>;
};
};
}
}
type MetaInput = { type MetaInput = {
title: string; title: string;
description: string; description: string;
@@ -30,9 +46,65 @@ type MetaInput = {
const LOADER_FRAMES = [load1, load2, load3, load4, load5, load6]; const LOADER_FRAMES = [load1, load2, load3, load4, load5, load6];
const FORCE_LOADER_PREVIEW = false; const FORCE_LOADER_PREVIEW = false;
const TWITTER_WIDGETS_SCRIPT_ID = "twitter-widgets-script";
const ARTIFACT_CATEGORY = "artifact";
const ARTIFACTS_PATH = "/artifacts";
let twitterWidgetsPromise: Promise<void> | null = null;
const toAbsoluteUrl = (path = "/"): string => new URL(path, BLOG_SITE_URL).toString(); const toAbsoluteUrl = (path = "/"): string => new URL(path, BLOG_SITE_URL).toString();
const toPermalinkUrl = (path = "/"): string => new URL(path, BLOG_PERMALINK_PREFIX).toString();
const hasCategory = (categories: string[] | undefined, category: string): boolean =>
(categories ?? []).some((entry) => entry.trim().toLowerCase() === category);
const isArtifactPost = (post: ManifestPost): boolean =>
hasCategory(post.categories, ARTIFACT_CATEGORY);
const ensureTwitterWidgets = (): Promise<void> => {
if (window.twttr?.widgets?.load) return Promise.resolve();
if (twitterWidgetsPromise) return twitterWidgetsPromise;
twitterWidgetsPromise = new Promise((resolve, reject) => {
const existingScript = document.getElementById(
TWITTER_WIDGETS_SCRIPT_ID
) as HTMLScriptElement | null;
if (existingScript) {
existingScript.addEventListener("load", () => resolve(), { once: true });
existingScript.addEventListener(
"error",
() => reject(new Error("Failed to load tweet embeds")),
{ once: true }
);
return;
}
const script = document.createElement("script");
script.id = TWITTER_WIDGETS_SCRIPT_ID;
script.src = "https://platform.twitter.com/widgets.js";
script.async = true;
script.charset = "utf-8";
script.addEventListener("load", () => resolve(), { once: true });
script.addEventListener(
"error",
() => reject(new Error("Failed to load tweet embeds")),
{ once: true }
);
document.head.appendChild(script);
});
return twitterWidgetsPromise;
};
const renderTweetEmbeds = (element: HTMLElement | null): void => {
if (!element?.querySelector(".twitter-tweet")) return;
void ensureTwitterWidgets()
.then(() => window.twttr?.widgets?.load(element))
.catch(() => {
twitterWidgetsPromise = null;
});
};
const setMetaTag = (attribute: "name" | "property", key: string, content?: string): void => { const setMetaTag = (attribute: "name" | "property", key: string, content?: string): void => {
if (!content) return; if (!content) return;
@@ -155,6 +227,9 @@ function App() {
}; };
}, []); }, []);
const articlePosts = useMemo(() => posts.filter((post) => !isArtifactPost(post)), [posts]);
const artifactPosts = useMemo(() => posts.filter(isArtifactPost), [posts]);
return ( return (
<div className="page"> <div className="page">
<header className="header"> <header className="header">
@@ -169,6 +244,24 @@ function App() {
</span> </span>
<span className="brand-text">{BLOG_NAME}</span> <span className="brand-text">{BLOG_NAME}</span>
</Link> </Link>
<nav className="site-nav" aria-label="Primary">
<NavLink
to="/"
end
className={({ isActive }) => `nav-link${isActive ? " nav-link-active" : ""}`}
>
articles
</NavLink>
<span className="nav-separator" aria-hidden="true">
//
</span>
<NavLink
to={ARTIFACTS_PATH}
className={({ isActive }) => `nav-link${isActive ? " nav-link-active" : ""}`}
>
artifacts
</NavLink>
</nav>
</div> </div>
</header> </header>
<main className="content"> <main className="content">
@@ -178,13 +271,20 @@ function App() {
<Routes> <Routes>
<Route <Route
path="/" path="/"
element={<IndexPage posts={posts} state={state} error={error} />} element={<IndexPage posts={articlePosts} state={state} error={error} />}
/>
<Route
path={ARTIFACTS_PATH}
element={<ArtifactsPage posts={artifactPosts} state={state} error={error} />}
/> />
<Route <Route
path="/:slug" path="/:slug"
element={<PostPage posts={posts} state={state} manifestError={error} />} element={<PostPage posts={posts} state={state} manifestError={error} />}
/> />
<Route path="*" element={<Navigate to="/" replace />} /> <Route
path="/*"
element={<PostPage posts={posts} state={state} manifestError={error} />}
/>
</Routes> </Routes>
)} )}
</main> </main>
@@ -273,6 +373,55 @@ function IndexPage({
); );
} }
function ArtifactsPage({
posts,
state,
error
}: {
posts: ManifestPost[];
state: LoadState;
error: string;
}) {
usePageMetadata({
title: `Artifacts | ${BLOG_NAME}`,
description: "Raw, unpolished LLM outputs.",
path: ARTIFACTS_PATH,
type: "website"
});
if (state === "loading") return <BlisterLoader />;
if (state === "error") return <p className="status">Error: {error}</p>;
if (posts.length === 0) return <p className="status">No artifacts yet.</p>;
return (
<section className="artifact-index">
<header className="artifact-index-header">
<h1>Artifacts</h1>
<p>Raw, unpolished LLM outputs.</p>
</header>
<ol className="artifact-list">
{posts.map((post) => {
const publishedDate = getReadableDate(post.publishedAt);
const description = post.description || post.excerpt || "No description provided.";
return (
<li key={post.postTxId} className="artifact-list-item">
<Link to={`/${post.slug}`} className="artifact-link">
{post.title}
</Link>
<div className="artifact-item-meta">
{publishedDate && <span>{publishedDate}</span>}
<span>codex</span>
</div>
<p>{description}</p>
</li>
);
})}
</ol>
</section>
);
}
function PostPage({ function PostPage({
posts, posts,
state, state,
@@ -282,7 +431,8 @@ function PostPage({
state: LoadState; state: LoadState;
manifestError: string; manifestError: string;
}) { }) {
const { slug = "" } = useParams(); const params = useParams();
const slug = params.slug ?? params["*"] ?? "";
const location = useLocation(); const location = useLocation();
const post = useMemo(() => posts.find((entry) => entry.slug === slug), [posts, slug]); const post = useMemo(() => posts.find((entry) => entry.slug === slug), [posts, slug]);
const [html, setHtml] = useState<string>(""); const [html, setHtml] = useState<string>("");
@@ -290,6 +440,7 @@ function PostPage({
const [postState, setPostState] = useState<LoadState>("idle"); const [postState, setPostState] = useState<LoadState>("idle");
const [postError, setPostError] = useState<string>(""); const [postError, setPostError] = useState<string>("");
const [showPostLoader, setShowPostLoader] = useState<boolean>(false); const [showPostLoader, setShowPostLoader] = useState<boolean>(false);
const articleRef = useRef<HTMLElement | null>(null);
useEffect(() => { useEffect(() => {
if (postState !== "loading") { if (postState !== "loading") {
@@ -329,6 +480,10 @@ function PostPage({
}; };
}, [post]); }, [post]);
useEffect(() => {
renderTweetEmbeds(articleRef.current);
}, [html]);
const title = postFrontmatter.title || post?.title || "Post"; const title = postFrontmatter.title || post?.title || "Post";
const description = const description =
postFrontmatter.desc || postFrontmatter.desc ||
@@ -337,12 +492,13 @@ function PostPage({
postFrontmatter.excerpt || postFrontmatter.excerpt ||
post?.excerpt || post?.excerpt ||
BLOG_DEFAULT_DESCRIPTION; BLOG_DEFAULT_DESCRIPTION;
const categories = postFrontmatter.categories?.length
? postFrontmatter.categories
: post?.categories ?? [];
const isArtifact = hasCategory(categories, ARTIFACT_CATEGORY);
const bannerTxId = postFrontmatter.banner || post?.frontmatter?.banner || post?.bannerTxId; const bannerTxId = postFrontmatter.banner || post?.frontmatter?.banner || post?.bannerTxId;
const publishedDate = getReadableDate(postFrontmatter.date || post?.publishedAt); const publishedDate = getReadableDate(postFrontmatter.date || post?.publishedAt);
const updatedDate = getReadableDate(postFrontmatter.updated || post?.updated || undefined); const updatedDate = getReadableDate(postFrontmatter.updated || post?.updated || undefined);
const permalink = post
? toPermalinkUrl(`/${post.slug}`)
: toPermalinkUrl(location.pathname);
const metadataTitle = const metadataTitle =
state === "loading" state === "loading"
? `${BLOG_NAME} | Loading` ? `${BLOG_NAME} | Loading`
@@ -364,7 +520,7 @@ function PostPage({
title: metadataTitle, title: metadataTitle,
description: metadataDescription, description: metadataDescription,
path: location.pathname, path: location.pathname,
image: bannerTxId ? arweaveUrl(bannerTxId) : undefined, image: bannerTxId && !isArtifact ? arweaveUrl(bannerTxId) : undefined,
type: post ? "article" : "website", type: post ? "article" : "website",
publishedTime: post ? postFrontmatter.date || post.publishedAt : undefined, publishedTime: post ? postFrontmatter.date || post.publishedAt : undefined,
modifiedTime: post ? postFrontmatter.updated || post.updated || undefined : undefined modifiedTime: post ? postFrontmatter.updated || post.updated || undefined : undefined
@@ -377,19 +533,17 @@ function PostPage({
if (postState === "error") return <p className="status">Error: {postError}</p>; if (postState === "error") return <p className="status">Error: {postError}</p>;
return ( return (
<article className="post"> <article className={`post${isArtifact ? " post-artifact" : ""}`}>
<header className="post-header"> <header className="post-header">
<h1>{title}</h1> <h1>{title}</h1>
<p>{description}</p> <p>{description}</p>
<div className="meta-row post-meta-row"> <div className="meta-row post-meta-row">
{publishedDate && <span>Published {publishedDate}</span>} {publishedDate && <span>Published {publishedDate}</span>}
{updatedDate && <span>Updated {updatedDate}</span>} {updatedDate && <span>Updated {updatedDate}</span>}
{isArtifact && <span>codex</span>}
{post.readingTime && <span>{post.readingTime} min read</span>} {post.readingTime && <span>{post.readingTime} min read</span>}
{post.wordCount && <span>{post.wordCount} words</span>} {post.wordCount && <span>{post.wordCount} words</span>}
<span>{post.postTxId.slice(0, 8)}...</span> <span>{post.postTxId.slice(0, 8)}...</span>
<span>
<a href={permalink}>[permalink]</a>
</span>
<span> <span>
<a href={arweaveUrl(post.postTxId)} target="_blank" rel="noreferrer"> <a href={arweaveUrl(post.postTxId)} target="_blank" rel="noreferrer">
[arweave] [arweave]
@@ -397,12 +551,13 @@ function PostPage({
</span> </span>
</div> </div>
</header> </header>
{bannerTxId && ( {bannerTxId && !isArtifact && (
<div className="post-hero"> <div className="post-hero">
<img className="post-banner" src={arweaveUrl(bannerTxId)} alt={`${title} banner`} /> <img className="post-banner" src={arweaveUrl(bannerTxId)} alt={`${title} banner`} />
</div> </div>
)} )}
<section <section
ref={articleRef}
className="article" className="article"
dangerouslySetInnerHTML={{ __html: html }} dangerouslySetInnerHTML={{ __html: html }}
/> />

View File

@@ -1,10 +1,10 @@
export const BLOG_NAME = "hyperzine"; export const BLOG_NAME = "hyperzine";
export const BLOG_SITE_URL = "https://zpz3tbjvlwkkrkn2talxgib6plcj62d3r3gpjebyinbcu7oa7bjq.arweave.net"; export const BLOG_SITE_URL = "https://hyperzine.xyz";
export const BLOG_PERMALINK_PREFIX = "https://386464538491k.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 = "BHVszJ1X-i3hnlNRflQccP_ESXZbWsSCJOfBOuNDePQ"; export const MANIFEST_REFERENCE_NAME = "mystic";
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";

View File

@@ -1,7 +1,8 @@
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { marked } from "marked"; import { marked, type Tokens } from "marked";
import { parse as parseYaml } from "yaml"; import { parse as parseYaml } from "yaml";
import { AO_PROCESS_ID, AO_URL, ARWEAVE_GATEWAY, MANIFEST_TX_ID } from "./config"; import { ReferenceClient } from "@permaweb/references";
import { ARWEAVE_GATEWAY, MANIFEST_REFERENCE_NAME } 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 =>
@@ -16,6 +17,52 @@ marked.setOptions({
breaks: false breaks: false
}); });
const escapeHtml = (value: string): string =>
value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
const getTweetEmbedUrl = (value: string): string | null => {
let url: URL;
try {
url = new URL(value.trim());
} catch {
return null;
}
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
const hostname = url.hostname.toLowerCase().replace(/^www\./, "");
const isTweetHost =
hostname === "x.com" || hostname === "twitter.com" || hostname === "mobile.twitter.com";
if (!isTweetHost) return null;
const [handle, statusSegment, statusId] = url.pathname.split("/").filter(Boolean);
if (!handle || !statusSegment || !statusId) return null;
if (!/^[a-z0-9_]{1,15}$/i.test(handle)) return null;
if (statusSegment !== "status" && statusSegment !== "statuses") return null;
if (!/^\d+$/.test(statusId)) return null;
return `https://twitter.com/${handle}/status/${statusId}`;
};
marked.use({
renderer: {
paragraph(this, token: Tokens.Paragraph): string {
const tweetUrl = getTweetEmbedUrl(token.text);
if (!tweetUrl) {
return `<p>${this.parser.parseInline(token.tokens)}</p>\n`;
}
const href = escapeHtml(tweetUrl);
const link = `<a href="${href}">${href}</a>`;
return `<blockquote class="twitter-tweet" data-theme="light" data-dnt="true">${link}</blockquote>\n`;
}
}
});
export const arweaveUrl = (txId: string): string => `${ARWEAVE_GATEWAY}/${txId}`; export const arweaveUrl = (txId: string): string => `${ARWEAVE_GATEWAY}/${txId}`;
export const getReadableDate = (date?: string | null): string | null => { export const getReadableDate = (date?: string | null): string | null => {
@@ -25,112 +72,50 @@ export const getReadableDate = (date?: string | null): string | null => {
return formatDate(date); return formatDate(date);
}; };
const MANIFEST_ID_CACHE_TTL_MS = 60_000;
let manifestIdCache: { expiresAt: number; manifestId: string } | null = null;
const asObject = (value: unknown): Record<string, unknown> | null =>
typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
const asString = (value: unknown): string | null =>
typeof value === "string" && value.length > 0 ? value : null;
const findManifestIdInTags = (tagsValue: unknown): string | null => {
const tags = Array.isArray(tagsValue) ? tagsValue : [];
for (const tag of tags) {
const tagObject = asObject(tag);
if (!tagObject) continue;
const name = asString(tagObject.name) ?? asString(tagObject.Name);
const value = asString(tagObject.value) ?? asString(tagObject.Value);
if ((name === "LatestManifestId" || name === "ManifestId") && value) return value;
}
return null;
};
const extractManifestId = (payload: unknown): string | null => {
const root = asObject(payload);
const results = asObject(root?.results);
if (!results) return null;
const outbox = asObject(results.outbox);
if (outbox) {
for (const message of Object.values(outbox)) {
const messageObject = asObject(message);
if (!messageObject) continue;
const direct = asString(messageObject.LatestManifestId) ?? asString(messageObject.ManifestId);
if (direct) return direct;
const tagged = findManifestIdInTags(messageObject.Tags);
if (tagged) return tagged;
}
}
const raw = asObject(results.raw);
const rawMessages = Array.isArray(raw?.Messages) ? raw.Messages : [];
for (const message of rawMessages) {
const messageObject = asObject(message);
if (!messageObject) continue;
const tagged = findManifestIdInTags(messageObject.Tags);
if (tagged) return tagged;
}
const json = asObject(results.json);
const body = json?.body;
const parsedBody =
typeof body === "string" ? asObject(JSON.parse(body)) : asObject(body);
const jsonMessages = Array.isArray(parsedBody?.Messages) ? parsedBody.Messages : [];
for (const message of jsonMessages) {
const messageObject = asObject(message);
if (!messageObject) continue;
const tagged = findManifestIdInTags(messageObject.Tags);
if (tagged) return tagged;
}
return null;
};
const getLatestManifestFromAo = async (): Promise<string | null> => {
try {
const now = Date.now();
if (manifestIdCache && manifestIdCache.expiresAt > now) {
return manifestIdCache.manifestId;
}
const url = `${AO_URL}/${AO_PROCESS_ID}~process@1.0/compute?Action=Get&require-codec=application/json&accept-bundle=true`;
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) return null;
const payload: unknown = await response.json();
const manifestId = extractManifestId(payload);
if (!manifestId) return null;
manifestIdCache = {
expiresAt: now + MANIFEST_ID_CACHE_TTL_MS,
manifestId
};
return manifestId;
} catch {
return null;
}
};
export const loadManifest = async (): Promise<ManifestPost[]> => { export const loadManifest = async (): Promise<ManifestPost[]> => {
const loadByTxId = async (manifestTxId: string): Promise<ManifestPost[]> => { return (await loadCurrentManifest()).posts;
const response = await fetch(arweaveUrl(manifestTxId)); };
if (!response.ok) {
throw new Error(`Failed to load manifest (${response.status})`);
}
const payload: unknown = await response.json();
return parseManifest(payload);
};
try { const fetchManifest = async (manifestTxId: string): Promise<{
return await loadByTxId(MANIFEST_TX_ID); txId: string;
} catch (manifestError) { posts: ManifestPost[];
const latestManifestTxId = await getLatestManifestFromAo(); }> => {
if (!latestManifestTxId || latestManifestTxId === MANIFEST_TX_ID) { const response = await fetch(arweaveUrl(manifestTxId));
throw manifestError; if (!response.ok) {
} throw new Error(`Failed to load manifest (${response.status})`);
return loadByTxId(latestManifestTxId);
} }
const payload: unknown = await response.json();
const posts = parseManifest(payload);
return { txId: manifestTxId, posts };
};
const resolveReferenceManifestTxId = async (): Promise<string> => {
if (!MANIFEST_REFERENCE_NAME) return "";
const names = new ReferenceClient({
gateway: ARWEAVE_GATEWAY,
fetch: (...args) => fetch(...args)
});
const value = await names.resolveName(MANIFEST_REFERENCE_NAME);
if (typeof value !== "string" || value.length === 0) {
throw new Error(`Reference ${MANIFEST_REFERENCE_NAME} did not resolve to a manifest id`);
}
return value;
};
const loadReferenceManifest = async () => {
if (!MANIFEST_REFERENCE_NAME) throw new Error("No manifest reference configured");
return fetchManifest(await resolveReferenceManifestTxId());
};
const loadCurrentManifest = async () => {
return loadReferenceManifest();
};
export const resolveManifestTxId = async (): Promise<string> => {
return (await loadCurrentManifest()).txId;
}; };
const isObject = (value: unknown): value is Record<string, unknown> => const isObject = (value: unknown): value is Record<string, unknown> =>

View File

@@ -60,6 +60,8 @@ a:hover {
.header-row { .header-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 18px;
min-height: 72px; min-height: 72px;
padding: 0 24px; padding: 0 24px;
} }
@@ -113,6 +115,30 @@ a:hover {
background: var(--brand-green); background: var(--brand-green);
} }
.site-nav {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.8rem;
letter-spacing: 0.08em;
}
.nav-link {
color: #444444;
text-decoration: none;
border-bottom: 1px solid transparent;
}
.nav-link:hover,
.nav-link-active {
color: #111111;
border-bottom-color: #111111;
}
.nav-separator {
color: #444444;
}
.content { .content {
max-width: 100%; max-width: 100%;
} }
@@ -224,6 +250,68 @@ a:hover {
transform: translateY(-50%); transform: translateY(-50%);
} }
.artifact-index {
padding: 0 24px 52px;
}
.artifact-index-header {
padding: 28px 0 22px;
border-bottom: 1px solid var(--line);
}
.artifact-index-header h1 {
margin: 0;
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 500;
line-height: 1;
letter-spacing: -0.03em;
}
.artifact-index-header p {
margin: 12px 0 0;
color: #262626;
}
.artifact-list {
list-style: none;
margin: 0;
padding: 0;
}
.artifact-list-item {
padding: 18px 0 20px;
border-bottom: 1px solid var(--line-soft);
}
.artifact-link {
display: inline-block;
font-size: 1.12rem;
font-weight: 500;
line-height: 1.25;
text-decoration: none;
}
.artifact-link:hover {
text-decoration: underline;
}
.artifact-item-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 7px;
color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 0.75rem;
text-transform: lowercase;
}
.artifact-list-item p {
margin: 9px 0 0;
color: #222222;
}
.post { .post {
border-top: 0; border-top: 0;
} }
@@ -333,12 +421,49 @@ a:hover {
color: #292929; color: #292929;
} }
.article .twitter-tweet {
margin: 1.6em 0;
padding: 16px 0 16px 14px;
}
.article iframe {
max-width: 100%;
}
.article img { .article img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border: 0; border: 0;
} }
.post-artifact .post-header {
padding-bottom: 24px;
border-bottom: 1px solid var(--line);
}
.post-artifact .post-header h1 {
max-width: 30ch;
}
.post-artifact .article {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 0.94rem;
line-height: 1.62;
}
.post-artifact .article p,
.post-artifact .article li,
.post-artifact .article blockquote {
font-size: 0.94rem;
}
.post-artifact .article h2,
.post-artifact .article h3,
.post-artifact .article h4 {
font-family: "DM Sans", sans-serif;
}
@media (max-width: 860px) { @media (max-width: 860px) {
.page { .page {
width: 100%; width: 100%;
@@ -353,6 +478,7 @@ a:hover {
} }
.post-card, .post-card,
.artifact-index,
.post-header, .post-header,
.post-hero, .post-hero,
.article, .article,
@@ -373,3 +499,17 @@ a:hover {
font-size: 1.38rem; font-size: 1.38rem;
} }
} }
@media (max-width: 520px) {
.header-row {
align-items: flex-start;
flex-direction: column;
justify-content: center;
padding-top: 14px;
padding-bottom: 14px;
}
.site-nav {
gap: 14px;
}
}