content: ao-the-asset

This commit is contained in:
fn
2026-04-27 15:17:32 +01:00
parent 3f059905d8
commit 7c9dc57c29
4 changed files with 125 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
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, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom";
import { import {
BLOG_DEFAULT_DESCRIPTION, BLOG_DEFAULT_DESCRIPTION,
@@ -18,6 +18,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,10 +40,59 @@ 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";
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 toPermalinkUrl = (path = "/"): string => new URL(path, BLOG_PERMALINK_PREFIX).toString();
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;
let tag = document.head.querySelector(`meta[${attribute}="${key}"]`); let tag = document.head.querySelector(`meta[${attribute}="${key}"]`);
@@ -290,6 +349,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 +389,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 ||
@@ -403,6 +467,7 @@ function PostPage({
</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://386900368387k.arweave.net/"
export const BLOG_PERMALINK_PREFIX = "https://386464538491k.arweave.net"; export const BLOG_PERMALINK_PREFIX = "https://386900368387k.arweave.net"
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_TX_ID = "EnWM5yrSyOCGV9YSUe1bUYWsq_Yxg4uEaVF9ODdbWWk";
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,5 +1,5 @@
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 { AO_PROCESS_ID, AO_URL, ARWEAVE_GATEWAY, MANIFEST_TX_ID } from "./config";
import { parseManifest, type Frontmatter, type ManifestPost } from "./types"; import { parseManifest, type Frontmatter, type ManifestPost } from "./types";
@@ -16,6 +16,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 => {

View File

@@ -333,6 +333,15 @@ 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;