Devlog-Suche (FTS5) + öffentlicher RSS-Feed

- FTS5-Volltextindex mit Trigger-Sync und einmaligem Backfill bestehender Devlogs
- /api/devlogs?q= — Präfix-Suche, injection-sicher (Phrasen-Quoting)
- Suchfeld mit Entprellung und Treffer-Zähler auf der Devlog-Seite
- /feed.xml — RSS 2.0 der letzten 20 Devlogs (escaped, deutsche Titel)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 10:34:19 +02:00
co-authored by Claude Opus 4.8
parent f2cae33b8f
commit 5a57108230
4 changed files with 141 additions and 6 deletions
+28 -3
View File
@@ -44,11 +44,23 @@ export default function Devlogs({ me }) {
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
const [error, setError] = useState(false);
const [qInput, setQInput] = useState('');
const [q, setQ] = useState('');
// Sucheingabe entprellen (300 ms), bei neuer Suche zurück auf Seite 1
useEffect(() => {
const t = setTimeout(() => {
setQ(qInput.trim());
setPage(1);
}, 300);
return () => clearTimeout(t);
}, [qInput]);
const load = useCallback(() => {
setError(false);
apiGet(`/api/devlogs?page=${page}`).then(setData).catch(() => setError(true));
}, [page]);
const search = q ? `&q=${encodeURIComponent(q)}` : '';
apiGet(`/api/devlogs?page=${page}${search}`).then(setData).catch(() => setError(true));
}, [page, q]);
useEffect(() => {
load();
@@ -86,9 +98,22 @@ export default function Devlogs({ me }) {
</section>
<section className="content">
<div className="search">
<span className="search-icon"></span>
<input
type="search"
placeholder="Devlogs durchsuchen …"
value={qInput}
onChange={(e) => setQInput(e.target.value)}
/>
{q && data && <span className="search-count">{data.total} Treffer</span>}
</div>
{error && <p className="notice">Devlogs konnten nicht geladen werden.</p>}
{!error && !data && <p className="notice">Lade </p>}
{data?.total === 0 && <p className="notice">Noch keine Devlogs archiviert.</p>}
{data?.total === 0 && (
<p className="notice">{q ? `Keine Treffer für „${q}".` : 'Noch keine Devlogs archiviert.'}</p>
)}
{data?.total > 0 && (
<div className="timeline">
+24
View File
@@ -182,6 +182,30 @@ body::after {
.notice.center { text-align: center; padding: 3.5rem 0; }
.notice.center p { margin-bottom: 1.25rem; }
/* ── SUCHE ─────────────────────────────────────────── */
.search {
display: flex; align-items: center; gap: .6rem;
background: var(--bg2);
border: 1px solid rgba(255, 255, 255, .1);
padding: .45rem .9rem;
margin-bottom: 1.75rem;
transition: border-color .2s;
}
.search:focus-within { border-color: var(--neon); }
.search-icon { color: var(--muted2); font-size: 1.1rem; }
.search input {
flex: 1;
background: none; border: none; outline: none;
color: var(--text);
font-family: var(--body); font-size: 1rem;
}
.search input::placeholder { color: var(--muted2); }
.search-count {
font-family: var(--mono);
font-size: .65rem; letter-spacing: .15em;
color: var(--neon); white-space: nowrap;
}
/* ── DEVLOG-TIMELINE ───────────────────────────────── */
.timeline {
display: flex; flex-direction: column; gap: 1.5rem;