SEO: OG por post, RSS feed, sitemap, FTS5 busqueda, View Transitions, Cloudflare Polish

This commit is contained in:
Renato
2026-08-04 17:47:05 +08:00
parent 1825dfae4e
commit ef1f06d465
7 changed files with 219 additions and 14 deletions
+57 -11
View File
@@ -43,8 +43,44 @@ function migrate(db: Database.Database) {
CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published);
CREATE INDEX IF NOT EXISTS idx_posts_created ON posts(created_at DESC);
-- FTS5: búsqueda full-text sobre título/título_jpn/artista/parodia/tags.
-- Tabla externa (content=posts) para que se sincronice con los triggers.
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
title, title_jpn, artist, parody, tags,
content='posts',
content_rowid='id',
tokenize='unicode61'
);
`)
// Triggers de sincronización FTS (solo si no existen).
db.exec(`
CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN
INSERT INTO posts_fts(rowid, title, title_jpn, artist, parody, tags)
VALUES (new.id, new.title, new.title_jpn, new.artist, new.parody, new.tags);
END;
CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN
INSERT INTO posts_fts(posts_fts, rowid, title, title_jpn, artist, parody, tags)
VALUES ('delete', old.id, old.title, old.title_jpn, old.artist, old.parody, old.tags);
END;
CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN
INSERT INTO posts_fts(posts_fts, rowid, title, title_jpn, artist, parody, tags)
VALUES ('delete', old.id, old.title, old.title_jpn, old.artist, old.parody, old.tags);
INSERT INTO posts_fts(rowid, title, title_jpn, artist, parody, tags)
VALUES (new.id, new.title, new.title_jpn, new.artist, new.parody, new.tags);
END;
`)
// Reindexar FTS si la tabla externa quedó desincronizada (posts sin indexar).
try {
const ftsCount = db.prepare("SELECT COUNT(*) as c FROM posts_fts").get() as { c: number }
const postCount = db.prepare("SELECT COUNT(*) as c FROM posts").get() as { c: number }
if (ftsCount.c < postCount.c) {
db.exec("INSERT INTO posts_fts(posts_fts) VALUES('rebuild')")
}
} catch {}
try {
db.exec("ALTER TABLE posts ADD COLUMN url TEXT")
} catch {}
@@ -214,22 +250,32 @@ export function getPostCount(): number {
return row.count
}
// Búsqueda pública por título/título_jpn/artista/tags (LIKE sobre JSON).
// Búsqueda pública por título/título_jpn/artista/tags (FTS5 full-text).
// Devuelve los posts MATCH (por relevancia), filtrando publicados.
export function searchPosts(query: string, publishedOnly = true): Post[] {
const d = getDb()
const q = `%${query}%`
const where = publishedOnly ? "published = 1 AND" : ""
const q = query.trim()
if (!q) return []
// Escapar comillas para FTS5 y construir una búsqueda por prefijo en cada token.
const escaped = q.replace(/"/g, "").trim()
if (!escaped) return []
const ftsQuery = escaped
.split(/\s+/)
.filter(Boolean)
.map((tok) => `"${tok}"*`)
.join(" AND ")
const where = publishedOnly ? "AND p.published = 1" : ""
const rows = d
.prepare(
`SELECT * FROM posts WHERE ${where} (
title LIKE @q OR
title_jpn LIKE @q OR
artist LIKE @q OR
parody LIKE @q OR
tags LIKE @q
) ORDER BY published_at DESC LIMIT 100`,
`SELECT p.* FROM posts p
JOIN posts_fts f ON f.rowid = p.id
WHERE posts_fts MATCH @q ${where}
ORDER BY bm25(posts_fts) LIMIT 100`,
)
.all({ q }) as Record<string, unknown>[]
.all({ q: ftsQuery }) as Record<string, unknown>[]
return rows.map(rowToPost)
}