import { Fragment, useEffect, useRef, useState, type ReactNode } from "react";
import { Link, useParams } from "react-router-dom";
import SkyCanvas from "../components/SkyCanvas";
import { PublicFooter, PublicNav } from "../components/PublicChrome";
import { blogPosts, getPost } from "../content/blogPosts";
import type { BlogPost, Mission } from "../content/seoTypes";
import "../styles/blog.css";
export function useMeta(title: string, description: string, canonical?: string) {
useEffect(() => {
document.title = title;
const set = (selector: string, attr: string, value: string, create: () => HTMLElement) => {
let el = document.head.querySelector(selector) as HTMLElement | null;
if (!el) {
el = create();
document.head.appendChild(el);
}
el.setAttribute(attr, value);
};
set('meta[name="description"]', "content", description, () => {
const m = document.createElement("meta");
m.setAttribute("name", "description");
return m;
});
set('link[rel="canonical"]', "href", `${window.location.origin}${canonical ?? window.location.pathname}`, () => {
const l = document.createElement("link");
l.setAttribute("rel", "canonical");
return l;
});
}, [title, description, canonical]);
}
const fmtDate = (d: string) =>
new Date(`${d}T00:00:00Z`).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" });
/** Posts are numbered as missions in publishing order: the oldest post is Mission 01. */
const missionNumber = (post: BlogPost) => {
const ordered = [...blogPosts].sort((a, b) => a.publishedAt.localeCompare(b.publishedAt));
return String(ordered.findIndex((p) => p.slug === post.slug) + 1).padStart(2, "0");
};
const missionOf = (post: BlogPost): Mission =>
post.mission ?? { question: post.title.endsWith("?") ? post.title : `${post.heroTitle}?`, answer: post.description };
const INLINE = /(\[[^\]]+\]\([^)\s]+\))|(\*\*[^*]+\*\*)/g;
/** Inline [text](url) links and **bold** inside post copy. External links open in a new tab and stay followed. */
export function Inline({ text }: { text: string }) {
const out: ReactNode[] = [];
let last = 0;
let i = 0;
for (const m of text.matchAll(INLINE)) {
const at = m.index ?? 0;
if (at > last) out.push(text.slice(last, at));
const [whole, link, bold] = m;
if (link) {
const label = link.slice(1, link.indexOf("]("));
const href = link.slice(link.indexOf("](") + 2, -1);
out.push(
href.startsWith("/") ? (
{label}
) : (
{label}
),
);
} else if (bold) {
out.push({bold.slice(2, -2)});
}
last = at + whole.length;
}
if (last < text.length) out.push(text.slice(last));
return <>{out}>;
}
const plain = (text: string) => text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1");
export function Blog() {
useMeta("Blog | Notestack", "Field notes on research, deep writing and turning your archive into audio, video and launches.");
const posts = [...blogPosts].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
return (
Mission log
Every post is a search for an answer
Pick a question. Each mission follows it through a few waypoints until the answer comes into view: research, deep writing,
audio, video and distribution.
{posts.map((p) => {
const m = missionOf(p);
return (