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">