mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Site app foundation on the content service
services/site is an Astro app rendered on request by the Node adapter. It keeps every published note in memory from the content service, refreshes in the background, and keeps serving the last good copy if the service is down. SiteIndex answers lookups, addresses, links both ways with roll-ups, the status log and safe Markdown. Vault images pass through /media. docs/site.md has the URL map and page rules. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* The site's copy of the published notes.
|
||||
*
|
||||
* The site reads only the content service, with a reader token, and only
|
||||
* published notes. It keeps every note in memory and refreshes in the
|
||||
* background, so a saved note shows within a minute and a slow or restarting
|
||||
* content service never takes a page down: the last good copy keeps serving.
|
||||
*/
|
||||
import type { NoteData, NoteType } from "@gk/schema";
|
||||
import { SiteIndex } from "./site.ts";
|
||||
|
||||
export interface Note<T extends NoteType = NoteType> {
|
||||
path: string;
|
||||
type: T;
|
||||
/** The file name without `.md`: what wikilinks point at. */
|
||||
name: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
data: NoteData<T>;
|
||||
body: string;
|
||||
acceptedAt: string;
|
||||
}
|
||||
|
||||
export interface Redirect {
|
||||
type: string;
|
||||
fromSlug: string;
|
||||
toPath: string;
|
||||
}
|
||||
|
||||
const env = (k: string, fallback?: string) => process.env[k] ?? import.meta.env[k] ?? fallback;
|
||||
const baseUrl = () => (env("CONTENT_URL", "http://localhost:4310") as string).replace(/\/$/, "");
|
||||
const token = () => env("CONTENT_TOKEN", "") as string;
|
||||
const refreshMs = () => Number(env("CONTENT_REFRESH_SECONDS", "30")) * 1000;
|
||||
|
||||
async function api<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${baseUrl()}${path}`, {
|
||||
headers: { authorization: `Bearer ${token()}` },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Content service answered ${res.status} for ${path}`);
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
async function loadAll(): Promise<SiteIndex> {
|
||||
const notes: Note[] = [];
|
||||
for (let offset = 0; ; offset += 1000) {
|
||||
const page = await api<Note[]>(`/api/notes?full=true&published=true&limit=1000&offset=${offset}`);
|
||||
notes.push(...page);
|
||||
if (page.length < 1000) break;
|
||||
}
|
||||
const redirects = await api<Redirect[]>("/api/redirects").catch(() => []);
|
||||
return new SiteIndex(notes, redirects);
|
||||
}
|
||||
|
||||
let current: SiteIndex | null = null;
|
||||
let loadedAt = 0;
|
||||
let inflight: Promise<SiteIndex> | null = null;
|
||||
|
||||
function refresh(): Promise<SiteIndex> {
|
||||
inflight ??= loadAll()
|
||||
.then((index) => {
|
||||
current = index;
|
||||
loadedAt = Date.now();
|
||||
return index;
|
||||
})
|
||||
.finally(() => {
|
||||
inflight = null;
|
||||
});
|
||||
return inflight;
|
||||
}
|
||||
|
||||
/** The current index. Waits only on the very first load. */
|
||||
export async function site(): Promise<SiteIndex> {
|
||||
if (!current) return refresh();
|
||||
if (Date.now() - loadedAt > refreshMs()) {
|
||||
refresh().catch((err) => console.error("Content refresh failed; serving the last copy.", err));
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/** Fetch a vault image from the content service, for the /media route. */
|
||||
export async function attachment(file: string): Promise<Response> {
|
||||
return fetch(`${baseUrl()}/attachments/${encodeURIComponent(file)}`, {
|
||||
headers: { authorization: `Bearer ${token()}` },
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
});
|
||||
}
|
||||
|
||||
/** Use a fixed set of notes instead of the content service, for tests. */
|
||||
export function useIndex(index: SiteIndex) {
|
||||
current = index;
|
||||
loadedAt = Number.POSITIVE_INFINITY;
|
||||
}
|
||||
Reference in New Issue
Block a user