My personal website!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Holy moly bro

Koehn 39751fd8 d84d15a6

+363 -380
+3 -3
package.json
··· 16 16 }, 17 17 "dependencies": { 18 18 "express": "^5.2.1", 19 - "front-matter": "^4.0.2", 20 - "marked": "17.0.6", 19 + "marked": "18.0.2", 21 20 "sanitize-html": "^2.17.2", 22 21 "zod": "^4.3.6" 23 22 }, 24 23 "scripts": { 25 24 "start": "tsx --watch --env-file=.env src/main.ts" 26 - } 25 + }, 26 + "packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8" 27 27 }
+5 -43
pnpm-lock.yaml
··· 11 11 express: 12 12 specifier: ^5.2.1 13 13 version: 5.2.1 14 - front-matter: 15 - specifier: ^4.0.2 16 - version: 4.0.2 17 14 marked: 18 - specifier: 17.0.6 19 - version: 17.0.6 15 + specifier: 18.0.2 16 + version: 18.0.2 20 17 sanitize-html: 21 18 specifier: ^2.17.2 22 19 version: 2.17.2 ··· 289 286 resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} 290 287 engines: {node: '>= 0.6'} 291 288 292 - argparse@1.0.10: 293 - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} 294 - 295 289 body-parser@2.2.2: 296 290 resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} 297 291 engines: {node: '>=18'} ··· 397 391 resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 398 392 engines: {node: '>=10'} 399 393 400 - esprima@4.0.1: 401 - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 402 - engines: {node: '>=4'} 403 - hasBin: true 404 - 405 394 etag@1.8.1: 406 395 resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 407 396 engines: {node: '>= 0.6'} ··· 421 410 fresh@2.0.0: 422 411 resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} 423 412 engines: {node: '>= 0.8'} 424 - 425 - front-matter@4.0.2: 426 - resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} 427 413 428 414 fsevents@2.3.3: 429 415 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} ··· 481 467 is-promise@4.0.0: 482 468 resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 483 469 484 - js-yaml@3.14.2: 485 - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} 486 - hasBin: true 487 - 488 - marked@17.0.6: 489 - resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} 470 + marked@18.0.2: 471 + resolution: {integrity: sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==} 490 472 engines: {node: '>= 20'} 491 473 hasBin: true 492 474 ··· 609 591 source-map-js@1.2.1: 610 592 resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 611 593 engines: {node: '>=0.10.0'} 612 - 613 - sprintf-js@1.0.3: 614 - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} 615 594 616 595 statuses@2.0.2: 617 596 resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} ··· 812 791 mime-types: 3.0.2 813 792 negotiator: 1.0.0 814 793 815 - argparse@1.0.10: 816 - dependencies: 817 - sprintf-js: 1.0.3 818 - 819 794 body-parser@2.2.2: 820 795 dependencies: 821 796 bytes: 3.1.2 ··· 931 906 932 907 escape-string-regexp@4.0.0: {} 933 908 934 - esprima@4.0.1: {} 935 - 936 909 etag@1.8.1: {} 937 910 938 911 express@5.2.1: ··· 983 956 984 957 fresh@2.0.0: {} 985 958 986 - front-matter@4.0.2: 987 - dependencies: 988 - js-yaml: 3.14.2 989 - 990 959 fsevents@2.3.3: 991 960 optional: true 992 961 ··· 1049 1018 1050 1019 is-promise@4.0.0: {} 1051 1020 1052 - js-yaml@3.14.2: 1053 - dependencies: 1054 - argparse: 1.0.10 1055 - esprima: 4.0.1 1056 - 1057 - marked@17.0.6: {} 1021 + marked@18.0.2: {} 1058 1022 1059 1023 math-intrinsics@1.1.0: {} 1060 1024 ··· 1195 1159 side-channel-weakmap: 1.0.2 1196 1160 1197 1161 source-map-js@1.2.1: {} 1198 - 1199 - sprintf-js@1.0.3: {} 1200 1162 1201 1163 statuses@2.0.2: {} 1202 1164
src/.DS_Store

This is a binary file and will not be displayed.

+58
src/components.ts
··· 1 + import { banner } from "./utils/structs"; 2 + 3 + export function createBanner( 4 + bannerStruct: unknown, 5 + options?: { home: boolean }, 6 + ) { 7 + try { 8 + let validatedData = banner.parse(bannerStruct); 9 + if (options?.home !== false) { 10 + validatedData = [{ link: "/", title: "home" }, ...validatedData]; 11 + } 12 + 13 + const message: string[] = []; 14 + 15 + validatedData.map((b) => { 16 + if (Array.isArray(b)) { 17 + return message.push( 18 + `[ ${b 19 + .map((l) => `<a href="${l.link}">${l.title}</a>`) 20 + .join(" | ")} ]`, 21 + ); 22 + } else { 23 + return message.push(`[ <a href="${b.link}">${b.title}</a> ]`); 24 + } 25 + }); 26 + 27 + return `<nav class="banner">${message.join(" · ")}</nav>`; 28 + } catch (error) { 29 + console.error("Validation failed:", error); 30 + } 31 + } 32 + 33 + export function createFooter() { 34 + return ` 35 + <footer role="contentinfo"> 36 + © 2026 <a href="mailto:hello@fromkoehn.com" rel="me">Koehn Humphries</a> · <a href="/public/key.txt">signature</a> 37 + </footer> 38 + `; 39 + } 40 + 41 + export function createHead(options?: { title?: string; stylesheet?: string }) { 42 + const stylesheet = options?.stylesheet || "index"; 43 + const title = options?.title || "From, Koehn."; 44 + 45 + return ` 46 + <head> 47 + <meta charset="utf-8" /> 48 + <title>${title}</title> 49 + <link rel="stylesheet" href="/public/${stylesheet}.css" /> 50 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 51 + <meta name="fediverse:creator" content="@koehn@famichiki.jp"> 52 + <link rel="me" href="https://famichiki.jp/@koehn" /> 53 + <link rel="alternate" type="application/atom+xml" title="From, Koehn" href="/feed.xml"> 54 + <link rel="human-json" href="/.well-known/human.json"> 55 + <link rel="icon" type="image/x-icon" href="/public/favicon.ico"> 56 + </head> 57 + `; 58 + }
-15
src/components/banner.ts
··· 1 - import { Banner } from "../utils/structs"; 2 - 3 - export function banner(bannerStruct: unknown, options?: { home: boolean }) { 4 - try { 5 - let validatedData = Banner.parse(bannerStruct); 6 - 7 - if (options?.home !== false) { 8 - validatedData = [{ link: "/", title: "home" }, ...validatedData]; 9 - } 10 - 11 - return ` <nav class="banner">${validatedData.map((i) => `[ <a href="${i.link}">${i.title}</a> ]`).join(" · ")}</nav>`; 12 - } catch (error) { 13 - console.error("Validation failed:", error); 14 - } 15 - }
-14
src/components/footer.ts
··· 1 - // import { execSync } from "node:child_process"; 2 - import { version } from "../../package.json"; 3 - 4 - export async function footer() { 5 - // const longCommit = execSync("git rev-parse HEAD").toString().trim(); 6 - // const shortCommit = execSync("git rev-parse --short HEAD").toString().trim(); 7 - // <a href="https://tangled.org/fromkoehn.com/www/commit/${longCommit}">${shortCommit}</a> 8 - 9 - return ` 10 - <footer role="contentinfo"> 11 - © 2026 <a href="mailto:hello@fromkoehn.com" rel="me">Koehn Humphries</a> · <a href="/public/key.txt">signature</a> · v${version} 12 - </footer> 13 - `; 14 - }
-18
src/components/head.ts
··· 1 - export function head(options?: { title?: string; stylesheet?: string }) { 2 - const stylesheet = options?.stylesheet || "index"; 3 - const title = options?.title || "From, Koehn."; 4 - 5 - return ` 6 - <head> 7 - <meta charset="utf-8" /> 8 - <title>${title}</title> 9 - <link rel="stylesheet" href="/public/${stylesheet}.css" /> 10 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 11 - <meta name="fediverse:creator" content="@koehn@famichiki.jp"> 12 - <link rel="me" href="https://famichiki.jp/@koehn" /> 13 - <link rel="alternate" type="application/atom+xml" title="From, Koehn" href="/feed.xml"> 14 - <link rel="human-json" href="/.well-known/human.json"> 15 - <link rel="icon" type="image/x-icon" href="/public/favicon.ico"> 16 - </head> 17 - `; 18 - }
+9 -50
src/main.ts
··· 1 - import fs from "node:fs/promises"; 2 1 import path from "node:path"; 3 2 import express from "express"; 4 - import type { z } from "zod"; 5 - import { banner } from "./components/banner"; 6 - import { footer } from "./components/footer"; 7 - import { head } from "./components/head"; 8 - import type { Post } from "./utils/structs"; 3 + import { mainRoute } from "./routes/main"; 4 + import { healthReport } from "./routes/health"; 5 + import { handlePost } from "./routes/post"; 6 + import { render404 } from "./routes/404"; 9 7 10 8 const app = express(); 11 9 const port = process.env.PORT || 8080; 12 10 13 11 app.use("/public", express.static(path.join(path.resolve(), "src/public"))); 14 - app.get("/", async (_req, res) => { 15 - const content: z.infer<typeof Post>[] = JSON.parse( 16 - await fs.readFile(path.join(path.resolve(), "data", "db.json"), "utf-8"), 17 - ); 18 - 19 - res.send(` 20 - <!DOCTYPE html> 21 - <html lang="en-US"> 22 - ${head()} 23 - <body> 24 - <main> 25 - <h1>From, <i class="me">Koehn</i></h1> 26 - ${( 27 - await Promise.all( 28 - content 29 - .filter((p) => !p.flags?.includes("private")) 30 - .sort((a, b) => +b.published - +a.published) 31 - .map(async (post) => { 32 - let decoration: string = ""; 33 - 34 - if (!post.flags.includes("source:local")) { 35 - const flag = post.flags.find((f) => 36 - f.startsWith("source:"), 37 - ); 38 - decoration = ` <span class="banner">[ ${flag?.split(":")[1]} ]</span>`; 39 - } 40 - 41 - return `<p role="article"><time class="mute" datetime="${post.published}">${ 42 - new Date(post.published).toISOString().split("T")[0] 43 - }</time>${decoration} <a href="${post.slug}">${post.title}</a></p>`; 44 - }), 45 - ) 46 - ).join(" ")} 47 - </main> 48 - ${banner( 49 - [ 50 - { title: "about", link: "infer" }, 51 - { title: "time machine", link: "infer" }, 52 - ], 53 - { home: false }, 54 - )} 55 - ${await footer()} 56 - </html> 57 - `); 12 + app.get(["/", "/home"], async (_req, res) => res.send(await mainRoute())); 13 + app.get("/health", async (_req, res) => res.json(await healthReport())); 14 + app.get("/posts/:slug", async (req, res) => await handlePost(req, res)); 15 + app.use((_req, res) => { 16 + res.status(404).send(render404()); 58 17 }); 59 18 60 19 app.listen(port, () => {
src/public/.DS_Store

This is a binary file and will not be displayed.

+61 -18
src/public/index.css
··· 1 - @font-face { 1 + /* @font-face { 2 2 font-family: "Aman"; 3 3 src: url("/public/fonts/Aman-Variable[wght,opsz,slnt].woff2") 4 4 format("woff2-variations"); ··· 6 6 } 7 7 8 8 @font-face { 9 - font-family: "Apoc JP"; 10 - src: url("/public/fonts/ApocJPRevelations-Regular.otf") format("opentype"); 9 + font-family: "MPLUS2"; 10 + src: url("/public/fonts/MPLUS2[wght].ttf") format("truetype-variations"); 11 + font-weight: 100 900; 11 12 font-style: normal; 13 + } */ 14 + 15 + @font-face { 16 + font-family: "ETBembo"; 17 + src: url("/public/fonts/et-book-roman-line-figures.woff") format("woff"); 18 + font-style: normal; 19 + } 20 + 21 + @font-face { 22 + font-family: "ETBembo"; 23 + src: url("/public/fonts/et-book-bold-line-figures.woff") format("woff"); 24 + font-style: normal; 25 + font-weight: bold; 12 26 } 13 27 14 28 :root { 15 29 color-scheme: light dark; 30 + --standard-font: "ETBembo", ui-seif; 31 + /* --label-font: "MPLUS2", monospace; */ 16 32 --background-color: light-dark( 17 33 oklch(0.9889 0.0053 17.25), 18 34 oklch(0.1957 0.0062 337.89) ··· 24 40 ); 25 41 26 42 --text-color: light-dark(oklch(0.16 0.012 260), oklch(0.93 0.01 20)); 27 - 28 - --accent-color: light-dark(oklch(0.68 0.16 360), oklch(0.72 0.17 360)); 43 + --label-text-color: oklch(0.16 0.012 260); 44 + --accent-color: oklch(0.798 0.1065 357.75); 29 45 30 46 --complementary-color: oklch(0.75 0.17 165); 31 47 ··· 43 59 text-align: left; 44 60 max-width: 40rem; 45 61 margin: 0 auto; 46 - font-family: "Aman", "Apoc JP", ui-serif; 47 - font-variation-settings: 62 + font-family: var(--standard-font); 63 + /* font-variation-settings: 48 64 "wght" 400, 49 - "opsz" 9; 65 + "opsz" 9; */ 50 66 background-color: var(--background-color); 51 67 color: var(--text-color); 52 - font-size: 1.5em; 68 + font-size: 1.3em; 53 69 text-decoration-thickness: 0.14em; 54 70 text-underline-offset: 0.18em; 55 71 } ··· 74 90 "wght" 400, 75 91 "opsz" 9, 76 92 "slnt" -4.4; 77 - } 78 - 79 - .bar { 80 - border-left: 3px solid var(--secondary-background-color); 81 - margin: 1.5em 10px; 82 - padding: 0.5em 10px; 83 93 } 84 94 85 95 h1, ··· 91 101 "slnt" -4.4; 92 102 } 93 103 94 - a[href^="https://"] { 104 + a[href^="https://"], 105 + a.external { 95 106 text-decoration-color: var(--complementary-color); 96 107 text-decoration-style: dashed; 97 108 } 98 109 99 - a[href^="https://"]:hover { 110 + a[href^="https://"]:hover, 111 + a.external:hover { 100 112 color: var(--complementary-color); 101 113 text-decoration-style: solid; 102 114 } ··· 113 125 color: var(--secondary-background-color); 114 126 } 115 127 128 + .block { 129 + display: flex; 130 + justify-content: space-between; 131 + align-items: center; 132 + margin-bottom: 0.5rem; 133 + } 134 + 135 + .labels { 136 + display: flex; 137 + gap: 0.5em; 138 + } 139 + 140 + .content { 141 + display: flex; 142 + justify-content: space-between; 143 + gap: 0.5rem; 144 + } 145 + 146 + .label { 147 + margin-top: 5px; 148 + padding: 5px; 149 + background-color: var(--accent-color); 150 + width: fit-content; 151 + color: var(--label-text-color); 152 + } 153 + 116 154 footer { 117 155 bottom: 0; 118 156 } 119 157 120 158 header { 121 - top: 0; 159 + p { 160 + margin-top: 0; 161 + } 162 + h1 { 163 + margin-bottom: 0; 164 + } 122 165 }
+4
src/routes/404.ts
··· 1 + export function render404() { 2 + return `This is a 404 idk 3 + `; 4 + }
+20
src/routes/health.ts
··· 1 + import { version } from "../../package.json"; 2 + import { fetchPosts } from "../utils/posts"; 3 + import { pdsHealth } from "../utils/structs"; 4 + 5 + export async function healthReport() { 6 + const content = await fetchPosts(); 7 + 8 + const pds = await fetch(`${process.env.ATPROTO_PDS}/xrpc/_health`); 9 + 10 + if (!pds.ok) throw Error(`[ERROR] XRPC Error: ${pds.status}`); 11 + 12 + const pdsVersion = pdsHealth.parse(await pds.json()); 13 + 14 + return { 15 + status: "ok", 16 + version: version, 17 + postCount: content.length, 18 + pdsVersion: pdsVersion.version, 19 + }; 20 + }
+43
src/routes/main.ts
··· 1 + import { createBanner, createFooter, createHead } from "../components"; 2 + import { fetchPosts } from "../utils/posts"; 3 + 4 + export async function mainRoute() { 5 + const content = await fetchPosts(); 6 + 7 + return ` 8 + <!DOCTYPE html> 9 + <html lang="en-US"> 10 + ${createHead()} 11 + <body> 12 + <main> 13 + <div class="labels"><div class="label">koehn</div></div> 14 + <p>Hi, I'm Koehn and I am a rising freshman at Virginia Tech. I plan to study International Relations there and am interested in coffee, tea, pottery, and the outdoors.</p> 15 + <div class="block"> 16 + <b>Posts</b> 17 + <a href="/posts">All posts</a> 18 + </div> 19 + <div> 20 + ${( 21 + await Promise.all( 22 + content 23 + .sort( 24 + (a, b) => 25 + +new Date(b.publishedAt) - 26 + +new Date(a.publishedAt), 27 + ) 28 + .map(async (post) => { 29 + return `<div class="content"><a href="${post.path}">${post.title}</a><time class="mute" datetime="${post.publishedAt}">${ 30 + new Date(post.publishedAt) 31 + .toISOString() 32 + .split("T")[0] 33 + }</time></div>`; 34 + }), 35 + ) 36 + ).join(" ")} 37 + </div> 38 + </main> 39 + ${createBanner([{ title: "about", link: "infer" }], { home: false })} 40 + ${createFooter()} 41 + </html> 42 + `; 43 + }
+61
src/routes/post.ts
··· 1 + import { raw, type Request, type Response } from "express"; 2 + import { fetchPosts } from "../utils/posts"; 3 + import { render404 } from "./404"; 4 + import { createBanner, createFooter, createHead } from "../components"; 5 + import sanitize from "sanitize-html"; 6 + import * as marked from "marked"; 7 + 8 + export async function handlePost(req: Request, res: Response) { 9 + const transformedSlug = req.params.slug as string; 10 + const slug = transformedSlug.replace(".txt", ""); 11 + const rawPosts = await fetchPosts(); 12 + const posts = rawPosts.sort( 13 + (a, b) => +new Date(a.publishedAt) - +new Date(b.publishedAt), 14 + ); 15 + 16 + const postIndex = posts.findIndex((p) => p.path === `/posts/${slug}`); 17 + 18 + if (!posts[postIndex]) return res.status(404).send(render404()); 19 + 20 + const post = posts[postIndex]; 21 + 22 + if (transformedSlug.endsWith(".txt")) { 23 + res.type("text/plain"); 24 + return res.send(`${post.title}\n\n${post.textContent}`); 25 + } 26 + 27 + const markdown = sanitize(`${marked.parse(post.content.markdown)}`); 28 + 29 + const buttons = []; 30 + 31 + if (postIndex > 0) { 32 + buttons.push({ title: "prev", link: posts[postIndex - 1].path }); 33 + } 34 + 35 + if (posts.length > postIndex + 1) { 36 + buttons.push({ title: "next", link: posts[postIndex + 1].path }); 37 + } 38 + 39 + const body = ` 40 + <!DOCTYPE html> 41 + <html lang="en-US"> 42 + ${createHead()} 43 + <body> 44 + <div class="labels"><div class="label">koehn</div><div class="label">blog</div></div> 45 + <article> 46 + <header> 47 + <h1>${post.title}</h1> 48 + <p class="mute">${post.description} · <time datetime="${post.publishedAt}">${ 49 + new Date(post.publishedAt).toISOString().split("T")[0] 50 + }</time></p> 51 + </header> 52 + ${markdown} 53 + </article> 54 + ${post.tags ? `<div>${post.tags?.map((t) => `#<a href="/tags/${t}">${t}</a>`).join(", ")}</div>` : ""} 55 + ${createBanner([buttons, { title: "plain text", link: `${post.path}.txt` }])} 56 + ${createFooter()} 57 + </html> 58 + `; 59 + 60 + res.send(body); 61 + }
-40
src/utils/mergePosts.ts
··· 1 - import fs from "node:fs"; 2 - import path from "node:path"; 3 - import type { z } from "zod"; 4 - import { standardSite } from "./sources/standard.site"; 5 - import type { Collection, Post } from "./structs"; 6 - import { localPosts } from "./sources/local"; 7 - 8 - const proposedIndexes: z.output<typeof Post>[] = []; 9 - 10 - function push(index: z.output<typeof Collection>) { 11 - console.log(`[INFO] Collecting posts from ${index.collection}...`); 12 - 13 - index.data.forEach((p) => { 14 - proposedIndexes.push(p); 15 - console.log(`[INFO] Pushed ${p.id}`); 16 - }); 17 - } 18 - 19 - push(await standardSite()); 20 - push(await localPosts()); 21 - 22 - export async function merge() { 23 - try { 24 - const sorted = proposedIndexes.toSorted( 25 - (a, b) => 26 - new Date(b.published).getTime() - new Date(a.published).getTime(), 27 - ); 28 - 29 - await fs.promises.writeFile( 30 - path.join(path.resolve(), "data", "db.json"), 31 - JSON.stringify(sorted), 32 - { flag: "w+" }, 33 - ); 34 - console.log("[INFO] Wrote to database!"); 35 - } catch (err) { 36 - console.error(`[ERROR] Failed to write to file: ${err}`); 37 - } 38 - } 39 - 40 - await merge();
+39
src/utils/posts.ts
··· 1 + import { 2 + protocolResponse, 3 + type StandardDocument, 4 + standardDocument, 5 + } from "./structs"; 6 + 7 + const cache: { data: StandardDocument[] | null; fetchedAt: number } = { 8 + data: null, 9 + fetchedAt: 0, 10 + }; 11 + 12 + export async function fetchPosts() { 13 + const now = Date.now(); 14 + if (cache.data && now - cache.fetchedAt < 5 * 60 * 1000) { 15 + return cache.data; 16 + } 17 + 18 + const pdsUrl = new URL( 19 + `${process.env.ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords`, 20 + ); 21 + 22 + pdsUrl.searchParams.set("repo", process.env.ATPROTO_DID); 23 + pdsUrl.searchParams.set("collection", "site.standard.document"); 24 + 25 + const records = await fetch(pdsUrl); 26 + 27 + if (!records.ok) throw Error(`[ERROR] XRPC Error: ${records.status}`); 28 + 29 + const data = protocolResponse.parse(await records.json()); 30 + 31 + const posts = data.records.map((r) => { 32 + return standardDocument.parse(r.value); 33 + }); 34 + 35 + cache.data = posts; 36 + cache.fetchedAt = now; 37 + 38 + return posts; 39 + }
-41
src/utils/sources/local.ts
··· 1 - import fs from "node:fs"; 2 - import path from "node:path"; 3 - import sanitizeHtml from "sanitize-html"; 4 - import fm from "front-matter"; 5 - import { marked } from "marked"; 6 - import type { z } from "zod"; 7 - import { Post } from "../structs"; 8 - 9 - export async function localPosts() { 10 - const finalDocuments: z.output<typeof Post>[] = []; 11 - 12 - for (const file of await fs.promises.readdir( 13 - path.join(path.resolve(), "data", "posts"), 14 - )) { 15 - const post = await fs.promises.readFile( 16 - path.join(path.resolve(), "data", "posts", file), 17 - "utf-8", 18 - ); 19 - 20 - const { attributes, body } = fm(post); 21 - 22 - const finalAttribs = attributes as Partial<z.infer<typeof Post>>; 23 - 24 - const stage = { 25 - title: finalAttribs.title, 26 - description: finalAttribs.description, 27 - published: new Date(finalAttribs.published ?? Date.now()).toISOString(), 28 - tags: finalAttribs.tags, 29 - id: finalAttribs.id, 30 - flags: [`source:local`, ...(finalAttribs.flags ?? [])], 31 - slug: `/posts/${file.replace(".md", "")}`, 32 - content: sanitizeHtml(await marked.parse(body)), 33 - }; 34 - 35 - const finalDoc = Post.parse(stage); 36 - 37 - finalDocuments.push(finalDoc); 38 - } 39 - 40 - return { collection: "local", data: finalDocuments }; 41 - }
-98
src/utils/sources/standard.site.ts
··· 1 - import fs from "node:fs"; 2 - import path from "node:path"; 3 - import type { z } from "zod"; 4 - import { 5 - ATProtoResponse, 6 - type LocalPublicationEntry, 7 - type Post, 8 - StandardDocument, 9 - StandardPublication, 10 - } from "../structs"; 11 - 12 - export async function standardSite() { 13 - const records = await fetch( 14 - `${process.env.ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${process.env.ATPROTO_DID}&collection=site.standard.document`, 15 - ); 16 - 17 - const publications = await fetch( 18 - `${process.env.ATPROTO_PDS}/xrpc/com.atproto.repo.listRecords?repo=${process.env.ATPROTO_DID}&collection=site.standard.publication`, 19 - ); 20 - 21 - const finalDocuments: z.output<typeof Post>[] = []; 22 - const knownPublications: z.output<typeof LocalPublicationEntry>[] = []; 23 - 24 - if (publications.ok) { 25 - const data = ATProtoResponse.parse(await publications.json()); 26 - 27 - data.records.forEach(async (r) => { 28 - const pub = StandardPublication.parse(r.value); 29 - 30 - knownPublications.push({ uri: r.uri, ...pub }); 31 - 32 - if ( 33 - !(await fs.promises 34 - .access( 35 - path.join( 36 - path.resolve(), 37 - "data", 38 - "artifacts", 39 - `${r.uri.split("/").at(-1)}.json`, 40 - ), 41 - ) 42 - .then(() => true) 43 - .catch(() => false)) 44 - ) { 45 - try { 46 - await fs.promises.writeFile( 47 - path.join( 48 - path.resolve(), 49 - "data", 50 - "artifacts", 51 - `${r.uri.split("/").at(-1)}.json`, 52 - ), 53 - JSON.stringify(pub), 54 - { flag: "w+" }, 55 - ); 56 - console.log( 57 - `[INFO] Created artifact: "${pub.name}" as ${r.uri.split("/").at(-1)}`, 58 - ); 59 - } catch (err) { 60 - console.error(`[ERROR] Failed to write to file: ${err}`); 61 - } 62 - } 63 - }); 64 - } 65 - 66 - if (records.ok) { 67 - const data = ATProtoResponse.parse(await records.json()); 68 - 69 - data.records.forEach(async (r) => { 70 - const doc = StandardDocument.parse(r.value); 71 - 72 - if (doc.content.$type === "pub.leaflet.content") { 73 - const pub = knownPublications.find((p) => p.uri === doc.site); 74 - 75 - const stage = { 76 - title: doc.title, 77 - description: doc.description, 78 - published: doc.publishedAt, 79 - tags: doc.tags, 80 - id: `${doc.path.replace("/", "")}`, 81 - flags: [ 82 - `source:leaflet`, 83 - `publication:${doc.site.split("/").at(-1)}`, 84 - ], 85 - slug: `${pub?.url}${doc.path}`, 86 - }; 87 - 88 - finalDocuments.push(stage); 89 - } else { 90 - console.warn( 91 - `[WARN] Type of "${doc.title}" is not leaflet (${doc.content.$type}), skipping`, 92 - ); 93 - } 94 - }); 95 - } 96 - 97 - return { collection: "site.standard", data: finalDocuments }; 98 - }
+55 -40
src/utils/structs.ts
··· 1 1 import { z } from "zod"; 2 2 3 - export const Post = z.object({ 4 - title: z.string(), 5 - description: z.string().optional(), 6 - published: z.iso.datetime().transform((str) => new Date(str)), 7 - flags: z.array(z.string()), 8 - tags: z.array(z.string()).optional(), 9 - slug: z.string().optional(), 10 - content: z.string().optional(), 11 - id: z.string(), 3 + export const record = z.object({ 4 + uri: z.string(), 5 + cid: z.string(), 6 + value: z.unknown(), 12 7 }); 13 8 14 - export const Collection = z.object({ 15 - collection: z.string(), 16 - data: z.array(Post), 9 + export const pdsHealth = z.object({ 10 + version: z.string(), 17 11 }); 18 12 19 - export const Record = z.object({ 20 - uri: z.string(), 21 - cid: z.string(), 22 - value: z.unknown(), 13 + export const standardDocumentContent = z.object({ 14 + $type: z.literal("com.fromkoehn.blog.content.markdown"), 15 + markdown: z.string(), 16 + wordCount: z.number().int().nonnegative().optional(), 23 17 }); 24 18 25 - export const StandardDocument = z.object({ 19 + export const standardDocument = z.object({ 20 + $type: z.literal("site.standard.document"), 21 + site: z.string().min(1), 26 22 title: z.string(), 27 23 description: z.string().optional(), 28 - publishedAt: z.iso 29 - .datetime({ offset: true }) 30 - .transform((str) => new Date(str)), 31 - site: z.string(), 32 - path: z.string(), 24 + publishedAt: z.iso.datetime(), 25 + updatedAt: z.iso.datetime().optional(), 26 + path: z.stringFormat("path", /^\/posts\/\w+$/), 27 + content: standardDocumentContent, 28 + textContent: z.string(), 33 29 tags: z.array(z.string()).optional(), 34 - content: z.object({ $type: z.string() }), 35 - $type: z.string(), 36 30 }); 37 31 38 - export const StandardPublication = z.object({ 32 + export const standardPublication = z.object({ 33 + $type: z.literal("site.standard.publication"), 39 34 url: z.string(), 40 35 name: z.string(), 41 - $type: z.string(), 42 36 }); 43 37 44 - export const LocalPublicationEntry = z.object({ 38 + export const localPublicationEntry = z.object({ 45 39 uri: z.string(), 46 40 url: z.string(), 47 41 name: z.string(), 48 42 $type: z.string(), 49 43 }); 50 44 51 - export const ATProtoResponse = z.object({ 52 - records: z.array(Record), 45 + export const protocolResponse = z.object({ 46 + records: z.array(record), 53 47 cursor: z.string(), 54 48 }); 55 49 56 - export const Banner = z.array( 57 - z 58 - .object({ 59 - link: z.string(), 60 - title: z.string(), 61 - type: z.literal("variable").optional(), 62 - }) 63 - .transform((item) => 64 - item.link === "infer" 65 - ? { ...item, link: `/${item.title.toLowerCase().replace(/\s+/g, "-")}` } 66 - : item, 50 + export const banner = z.array( 51 + z.union([ 52 + z 53 + .object({ 54 + link: z.string(), 55 + title: z.string(), 56 + }) 57 + .transform((item) => 58 + item.link === "infer" 59 + ? { 60 + ...item, 61 + link: `/${item.title.toLowerCase().replace(/\s+/g, "-")}`, 62 + } 63 + : item, 64 + ), 65 + z.array( 66 + z 67 + .object({ 68 + link: z.string(), 69 + title: z.string(), 70 + }) 71 + .transform((item) => 72 + item.link === "infer" 73 + ? { 74 + ...item, 75 + link: `/${item.title.toLowerCase().replace(/\s+/g, "-")}`, 76 + } 77 + : item, 78 + ), 67 79 ), 80 + ]), 68 81 ); 82 + 83 + export type StandardDocument = z.infer<typeof standardDocument>;
+4
well-known/human.json
··· 6 6 { 7 7 "url": "https://neatnik.net", 8 8 "vouched_at": "2026-03-11" 9 + }, 10 + { 11 + "url": "https://www.disnetdev.com/", 12 + "vouched_at": "2026-04-24" 9 13 } 10 14 ] 11 15 }
+1
well-known/site.standard.publication
··· 1 + at://did:plc:ex55cz2xbuihypjbwowctvaw/site.standard.publication/3mkbjvsdcic2b