import fs from "node:fs/promises"; import path from "node:path"; const ROOT = process.cwd(); const DIST_DIR = path.join(ROOT, "dist"); const DIST_INDEX_HTML = path.join(DIST_DIR, "index.html"); const SRC_CONFIG = path.join(ROOT, "src", "config.ts"); const DEFAULTS = { BLOG_NAME: "hyperzine", BLOG_SITE_URL: "https://hyperzine.xyz", BLOG_DEFAULT_DESCRIPTION: "hyperzine", MANIFEST_TX_ID: "" }; const asObject = (value) => (typeof value === "object" && value !== null ? value : null); const asString = (value) => (typeof value === "string" && value.trim().length > 0 ? value : ""); const htmlEscape = (value) => value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); const normalizeSlashPath = (input) => { const cleaned = input.trim().replace(/^\/+|\/+$/g, ""); return cleaned ? `/${cleaned}` : "/"; }; const toAbsoluteUrl = (siteUrl, inputPath) => new URL(normalizeSlashPath(inputPath), siteUrl).toString(); const parseConfig = async () => { let source = ""; try { source = await fs.readFile(SRC_CONFIG, "utf8"); } catch { return DEFAULTS; } const pick = (key) => { const match = source.match( new RegExp(`export\\s+const\\s+${key}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "m") ); const value = match?.[1] || match?.[2]; return value ?? DEFAULTS[key]; }; return { BLOG_NAME: pick("BLOG_NAME"), BLOG_SITE_URL: pick("BLOG_SITE_URL"), BLOG_DEFAULT_DESCRIPTION: pick("BLOG_DEFAULT_DESCRIPTION"), MANIFEST_TX_ID: pick("MANIFEST_TX_ID") }; }; const parseFrontmatter = (input) => { const obj = asObject(input); if (!obj) return {}; const bannerValue = obj.banner; const bannerObj = asObject(bannerValue); const banner = asString(bannerValue) || asString(bannerObj?.txId) || asString(bannerObj?.id) || asString(bannerObj?.src) || asString(bannerObj?.url); return { title: asString(obj.title), desc: asString(obj.desc), description: asString(obj.description), excerpt: asString(obj.excerpt), slug: asString(obj.slug), banner, date: asString(obj.date), updated: asString(obj.updated) }; }; const parseManifest = (input) => { const root = asObject(input); const posts = Array.isArray(root?.posts) ? root.posts : []; return posts .map((entry) => { const post = asObject(entry); if (!post) return null; const frontmatter = parseFrontmatter(post.frontmatter); const bannerTxIdValue = post.bannerTxId; const bannerTxIdObject = asObject(bannerTxIdValue); const bannerTxId = asString(bannerTxIdValue) || asString(bannerTxIdObject?.txId) || asString(bannerTxIdObject?.id) || asString(bannerTxIdObject?.src) || asString(bannerTxIdObject?.url); const slug = asString(post.slug) || frontmatter.slug; const postTxId = asString(post.postTxId); if (!slug || !postTxId) return null; return { slug, title: asString(post.title) || frontmatter.title || "Untitled", description: asString(post.description) || frontmatter.desc || frontmatter.description || "", excerpt: asString(post.excerpt) || frontmatter.excerpt || "", bannerTxId: bannerTxId || frontmatter.banner || "", publishedAt: asString(post.publishedAt) || frontmatter.date || "", updated: asString(post.updated) || frontmatter.updated || "" }; }) .filter(Boolean) .sort((a, b) => { const aDate = a.publishedAt || ""; const bDate = b.publishedAt || ""; return aDate < bDate ? 1 : -1; }); }; 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 buildMetaTags = ({ title, description, canonicalUrl, siteName, imageUrl, type = "website", publishedTime, modifiedTime }) => { const tags = [ `${htmlEscape(title)}`, ``, ``, ``, ``, ``, ``, ``, ``, ``, `` ]; if (imageUrl) { tags.push(``); tags.push(``); } if (type === "article" && publishedTime) { tags.push(``); } if (type === "article" && modifiedTime) { tags.push(``); } return tags.join("\n "); }; const injectMeta = (html, metaBlock) => { const marker = ""; if (html.includes(marker)) { return html.replace(marker, `${marker}\n ${metaBlock}`); } const stripped = html .replace(/[\s\S]*?<\/title>/gi, "") .replace(/<link[^>]+rel=["']canonical["'][^>]*>/gi, "") .replace(/<meta[^>]+(?:name|property)=["'](?:description|og:[^"']+|twitter:[^"']+|article:[^"']+)["'][^>]*>/gi, ""); return stripped.replace("</head>", ` ${metaBlock}\n </head>`); }; const normalizeAssetPaths = (html) => html.replaceAll('src="./assets/', 'src="/assets/').replaceAll('href="./assets/', 'href="/assets/'); const writeRouteHtml = async (baseHtml, routePath, metaBlock) => { const routeDir = routePath === "/" ? DIST_DIR : path.join(DIST_DIR, routePath.slice(1)); const routeHtmlPath = path.join(routeDir, "index.html"); await fs.mkdir(routeDir, { recursive: true }); let html = injectMeta(baseHtml, metaBlock); if (routePath !== "/") html = normalizeAssetPaths(html); await fs.writeFile(routeHtmlPath, html, "utf8"); }; const main = async () => { const config = await parseConfig(); const baseHtml = await fs.readFile(DIST_INDEX_HTML, "utf8"); let posts = []; try { posts = await fetchManifestPosts(config.MANIFEST_TX_ID); } catch (error) { console.warn( `[prerender-meta] Could not fetch manifest; generating root metadata only: ${error.message}` ); } const featured = posts[0]; const rootTitle = config.BLOG_NAME; const rootDescription = config.BLOG_DEFAULT_DESCRIPTION || featured?.description || featured?.excerpt || `Posts from ${config.BLOG_NAME}.`; const rootImage = featured?.bannerTxId ? `https://arweave.net/${featured.bannerTxId}` : ""; const rootCanonical = toAbsoluteUrl(config.BLOG_SITE_URL, "/"); const rootMeta = buildMetaTags({ title: rootTitle, description: rootDescription, canonicalUrl: rootCanonical, siteName: config.BLOG_NAME, imageUrl: rootImage, type: "website" }); await writeRouteHtml(baseHtml, "/", rootMeta); for (const post of posts) { const postPath = normalizeSlashPath(post.slug); const postTitle = `${post.title} | ${config.BLOG_NAME}`; const postDescription = post.description || post.excerpt || config.BLOG_DEFAULT_DESCRIPTION || `Post on ${config.BLOG_NAME}`; const postImage = post.bannerTxId ? `https://arweave.net/${post.bannerTxId}` : ""; const canonicalUrl = toAbsoluteUrl(config.BLOG_SITE_URL, postPath); const postMeta = buildMetaTags({ title: postTitle, description: postDescription, canonicalUrl, siteName: config.BLOG_NAME, imageUrl: postImage, type: "article", publishedTime: post.publishedAt, modifiedTime: post.updated }); await writeRouteHtml(baseHtml, postPath, postMeta); } console.log(`[prerender-meta] Generated metadata pages: ${posts.length + 1}`); }; void main();