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 {
|
||||
BLOG_DEFAULT_DESCRIPTION,
|
||||
@@ -18,6 +18,16 @@ import load6 from "./load6.png";
|
||||
|
||||
type LoadState = "idle" | "loading" | "error";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
twttr?: {
|
||||
widgets?: {
|
||||
load: (element?: HTMLElement | null) => void | Promise<unknown>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type MetaInput = {
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -30,10 +40,59 @@ type MetaInput = {
|
||||
|
||||
const LOADER_FRAMES = [load1, load2, load3, load4, load5, load6];
|
||||
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 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 => {
|
||||
if (!content) return;
|
||||
let tag = document.head.querySelector(`meta[${attribute}="${key}"]`);
|
||||
@@ -290,6 +349,7 @@ function PostPage({
|
||||
const [postState, setPostState] = useState<LoadState>("idle");
|
||||
const [postError, setPostError] = useState<string>("");
|
||||
const [showPostLoader, setShowPostLoader] = useState<boolean>(false);
|
||||
const articleRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (postState !== "loading") {
|
||||
@@ -329,6 +389,10 @@ function PostPage({
|
||||
};
|
||||
}, [post]);
|
||||
|
||||
useEffect(() => {
|
||||
renderTweetEmbeds(articleRef.current);
|
||||
}, [html]);
|
||||
|
||||
const title = postFrontmatter.title || post?.title || "Post";
|
||||
const description =
|
||||
postFrontmatter.desc ||
|
||||
@@ -403,6 +467,7 @@ function PostPage({
|
||||
</div>
|
||||
)}
|
||||
<section
|
||||
ref={articleRef}
|
||||
className="article"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export const BLOG_NAME = "hyperzine";
|
||||
export const BLOG_SITE_URL = "https://zpz3tbjvlwkkrkn2talxgib6plcj62d3r3gpjebyinbcu7oa7bjq.arweave.net";
|
||||
export const BLOG_PERMALINK_PREFIX = "https://386464538491k.arweave.net";
|
||||
export const BLOG_SITE_URL = "https://386900368387k.arweave.net/"
|
||||
export const BLOG_PERMALINK_PREFIX = "https://386900368387k.arweave.net"
|
||||
export const BLOG_DEFAULT_DESCRIPTION =
|
||||
"A blog about cyberspace decentralization";
|
||||
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 AO_URL = "https://push-1.forward.computer";
|
||||
|
||||
48
src/lib.ts
48
src/lib.ts
@@ -1,5 +1,5 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { marked, type Tokens } from "marked";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { AO_PROCESS_ID, AO_URL, ARWEAVE_GATEWAY, MANIFEST_TX_ID } from "./config";
|
||||
import { parseManifest, type Frontmatter, type ManifestPost } from "./types";
|
||||
@@ -16,6 +16,52 @@ marked.setOptions({
|
||||
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 getReadableDate = (date?: string | null): string | null => {
|
||||
|
||||
@@ -333,6 +333,15 @@ a:hover {
|
||||
color: #292929;
|
||||
}
|
||||
|
||||
.article .twitter-tweet {
|
||||
margin: 1.6em 0;
|
||||
padding: 16px 0 16px 14px;
|
||||
}
|
||||
|
||||
.article iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.article img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
Reference in New Issue
Block a user