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:
@@ -177,6 +177,10 @@ async function listWiki(req, res) {
|
||||
const category = await wiki.getCategoryBySlug(req.query.category)
|
||||
filters.categoryId = category ? category.id : -1 // unknown → match nothing
|
||||
}
|
||||
if (req.query.tag) {
|
||||
const tag = await wiki.getTagBySlug(req.query.tag)
|
||||
filters.tagId = tag ? tag.id : -1 // unknown → match nothing
|
||||
}
|
||||
if (req.query.status === 'draft') filters.published = false
|
||||
if (req.query.status === 'published') filters.published = true
|
||||
return res.json(await wiki.listAll(filters))
|
||||
@@ -221,6 +225,7 @@ async function createWiki(req, res) {
|
||||
excerpt: req.body.excerpt || null,
|
||||
categoryId: cat.value,
|
||||
published: req.body.published !== false,
|
||||
tags: Array.isArray(req.body.tags) ? req.body.tags : undefined,
|
||||
updatedBy: req.user.id,
|
||||
})
|
||||
await activity.log({ req, action: 'wiki.create', detail: { slug: page.slug } })
|
||||
@@ -241,6 +246,7 @@ async function updateWiki(req, res) {
|
||||
if ('body' in req.body) input.body = req.body.body || null
|
||||
if ('excerpt' in req.body) input.excerpt = req.body.excerpt || null
|
||||
if ('published' in req.body) input.published = Boolean(req.body.published)
|
||||
if ('tags' in req.body) input.tags = Array.isArray(req.body.tags) ? req.body.tags : []
|
||||
if ('category_id' in req.body) {
|
||||
const cat = await resolveCategoryId(req.body)
|
||||
if (!cat.ok) return res.status(400).json({ message: 'Unknown category' })
|
||||
@@ -282,6 +288,15 @@ async function deleteWiki(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wiki tags ──────────────────────────────────────────────────────────
|
||||
async function listWikiTags(req, res) {
|
||||
try {
|
||||
return res.json(await wiki.listTags())
|
||||
} catch (err) {
|
||||
return res.status(500).json({ message: 'Internal Server Error' })
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wiki categories ────────────────────────────────────────────────────
|
||||
async function listWikiCategories(req, res) {
|
||||
try {
|
||||
@@ -473,6 +488,7 @@ module.exports = {
|
||||
updateWiki,
|
||||
publishWiki,
|
||||
deleteWiki,
|
||||
listWikiTags,
|
||||
listWikiCategories,
|
||||
createWikiCategory,
|
||||
updateWikiCategory,
|
||||
|
||||
@@ -90,6 +90,9 @@ adminRouter.put(
|
||||
)
|
||||
adminRouter.delete('/wiki/categories/:id', param('id').isInt(), validate, ctrl.deleteWikiCategory)
|
||||
|
||||
// ── Wiki tags ──────────────────────────────────────────────────────────
|
||||
adminRouter.get('/wiki/tags', ctrl.listWikiTags)
|
||||
|
||||
// ── Wiki pages ─────────────────────────────────────────────────────────
|
||||
adminRouter.get('/wiki', ctrl.listWiki)
|
||||
adminRouter.post(
|
||||
@@ -99,6 +102,7 @@ adminRouter.post(
|
||||
body('excerpt').optional({ values: 'falsy' }).isString().isLength({ max: 400 }),
|
||||
body('category_id').optional({ values: 'null' }).isInt(),
|
||||
body('published').optional().isBoolean(),
|
||||
body('tags').optional().isArray(),
|
||||
validate,
|
||||
ctrl.createWiki,
|
||||
)
|
||||
@@ -109,6 +113,7 @@ adminRouter.put(
|
||||
body('excerpt').optional({ values: 'falsy' }).isString().isLength({ max: 400 }),
|
||||
body('category_id').optional({ values: 'null' }).isInt(),
|
||||
body('published').optional().isBoolean(),
|
||||
body('tags').optional().isArray(),
|
||||
validate,
|
||||
ctrl.updateWiki,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user