export interface ViewedArticle { /** the article read, by its identity — the canonical version's slug. An * entry written before versions had addresses of their own may hold a * document's own slug, which the reader still resolves. */ slug: string title: string /** the language it was last read in, so a jump returns to that version. * Absent from entries written when one address stood for the whole article */ lang?: string viewedAt: number } const STORAGE_KEY = 'mathew-recently-viewed' const MAX_ENTRIES = 8 /** where a jump leads: the version the article was last read in, or — for an * entry that recorded no language — the short address, which the reader turns * onto whichever version the slug speaks */ export function viewedHref(viewed: Pick): string { return viewed.lang ? versionHref(viewed.slug, viewed.lang) : `/wiki/${encodeURIComponent(viewed.slug)}` } let loaded = false function readStored(): ViewedArticle[] { try { const raw = localStorage.getItem(STORAGE_KEY) return raw ? (JSON.parse(raw) as ViewedArticle[]) : [] } catch { /* ignore corrupt storage */ return [] } } /** * Tracks which articles the visitor has opened, in `localStorage`, so the * sidebar can offer a "recently viewed" list. Shared across pages via * `useState`, so it updates live as you browse without extra fetches. */ export function useRecentlyViewed() { const viewed = useState('recently-viewed', () => []) /* read the saved history once, after mount, so the server render and the client hydration both start from an empty list */ onMounted(() => { if (loaded) return loaded = true const stored = readStored() if (stored.length) viewed.value = stored }) function record(article: { slug: string title: string baseSlug?: string | null language?: string }) { /* filed by the article's identity and turned to the language it was read in: an article read in two languages is still one article remembered once, at the version it was last opened at */ const entry = { slug: identityOf(article), lang: articleLanguage(article), title: article.title, } /* merge with what's already saved so recording before the list has loaded (e.g. deep-linking straight to an article) can't clobber it */ const base = viewed.value.length ? viewed.value : readStored() const fresh = { ...entry, viewedAt: Date.now() } viewed.value = [fresh, ...base.filter((v) => v.slug !== entry.slug)].slice(0, MAX_ENTRIES) loaded = true try { localStorage.setItem(STORAGE_KEY, JSON.stringify(viewed.value)) } catch { /* private mode etc. — history just won't persist */ } } return { viewed, record } }