content: ao-the-asset
This commit is contained in:
67
src/App.tsx
67
src/App.tsx
@@ -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 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
48
src/lib.ts
48
src/lib.ts
@@ -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("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """);
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user