Wiki Phase 3: internal links, backlinks, and tags

Connectivity phase of the wiki upgrade (see WIKI_UPGRADE.md).

Schema (additive new tables): wiki_tags, wiki_page_tags, wiki_links.

Internal links & backlinks:
- new wiki.links.js parses a saved body for /wiki/<slug> (and data-wiki-slug)
  targets; wiki_links is rebuilt on every save
- article shows a "Linked from" section (published backlinks) and renders
  links to non-existent pages as red links (server returns missing_links)
- editor gains an internal-link picker listing existing pages

Tags:
- pages accept a tags[] array; tags upsert on save, page tag-set is replaced,
  and orphaned tags are auto-pruned (on save and delete)
- public/admin list filter by ?tag=; /wiki/tags lists tags with published counts
- article shows tag chips; the index has a flat tag-filtered view; editor has a
  comma-separated tags field

Verified end-to-end: A->B backlink appears, red link detected, link index
rebuilds on edit, tag filtering + chips + pruning all work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 11:25:15 -05:00
parent 4a7dbf0085
commit 7c081ae749
15 changed files with 516 additions and 70 deletions

View File

@@ -58,15 +58,28 @@ async function getWikiCategories(req, res) {
}
}
async function getWikiTags(req, res) {
try {
return res.json(await wiki.listTags())
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function getWikiList(req, res) {
try {
let categoryId = null
const filters = {}
if (req.query.category) {
const category = await wiki.getCategoryBySlug(req.query.category)
if (!category) return res.json([]) // unknown category → no pages
categoryId = category.id
filters.categoryId = category.id
}
return res.json(await wiki.listPublished(categoryId))
if (req.query.tag) {
const tag = await wiki.getTagBySlug(req.query.tag)
if (!tag) return res.json([]) // unknown tag → no pages
filters.tagId = tag.id
}
return res.json(await wiki.listPublished(filters))
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
@@ -100,6 +113,7 @@ module.exports = {
getPosts,
getPost,
getWikiCategories,
getWikiTags,
getWikiList,
getWikiPage,
contact,

View File

@@ -25,8 +25,9 @@ publicRouter.post(
publicRouter.get('/posts/:category', siteMode, ctrl.getPosts)
publicRouter.get('/posts/:category/:idOrSlug', siteMode, ctrl.getPost)
publicRouter.get('/wiki', siteMode, ctrl.getWikiList)
// Static path must precede the :slug route so it isn't captured as a slug.
// Static paths must precede the :slug route so they aren't captured as a slug.
publicRouter.get('/wiki/categories', siteMode, ctrl.getWikiCategories)
publicRouter.get('/wiki/tags', siteMode, ctrl.getWikiTags)
publicRouter.get('/wiki/:slug', siteMode, ctrl.getWikiPage)
module.exports = publicRouter