Search the logs
Type. Find. Pop. The whole loop.
Time to tie it together . Your content collection lives at build time. Your API endpoint exposes it as JSON. Your island gives users a way to search it.
The island fetches your endpoint on mount, holds the logs in state, and filters them as the user types.
import { useEffect, useState } from "preact/hooks";
type Log = { id: string; title: string; author: string };
export default function Search() {
const [logs, setLogs] = useState<Log[]>([]);
const [q, setQ] = useState("");
// fetch once, on mount
useEffect(() => {
fetch("/api/logs.json")
.then((r) => r.json())
.then(setLogs);
}, []);
const filtered = logs.filter((l) =>
l.title.toLowerCase().includes(q.toLowerCase()));
return (
<div>
<input value="{q}" onInput="{(e) => setQ(e.currentTarget.value)}" placeholder="search…" />
<ul>
{filtered.map((l) => <li key="{l.id}">{l.title}</li>)}
</ul>
</div>
);
} And drop it on the logs index — client:visible because it's not above the fold for everyone:
---
import Layout from "~/layouts/Layout.astro";
import Search from "~/components/Search.tsx";
---
<Layout title="Mission Log">
<h1>Mission Log</h1>
<Search client:visible />
</Layout> This little component is the whole client-side data story . Page is static HTML. Island hydrates when visible. Island fetches your own endpoint. Island re-renders on input.
For a thousand logs you'd swap filter() for a real search index. For ten, this is fine — and it's been fine for a million sites.