SEO: OG por post, RSS feed, sitemap, FTS5 busqueda, View Transitions, Cloudflare Polish
This commit is contained in:
+57
-11
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user