Add Wiki feature with Markdown, categories, tags, and search
Deploy to LXC / deploy (push) Failing after 4s

- wiki_categories, wiki_pages, wiki_tags, wiki_page_tags tables
- /wiki overview with category-grouped pages, tag sidebar, search
- /wiki/new create page with Markdown editor and live preview
- /wiki/[slug] view page with rendered Markdown, tags, edit/delete
- /wiki/[slug]/edit with pre-populated editor
- Tag input with existing tag suggestions (click to add)
- Category management (add/delete) in sidebar
- Tailwind Typography plugin for prose styling
- marked package for Markdown rendering
- Sidebar nav item with book icon

Run db:push on server to create wiki tables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 15:42:34 +07:00
parent 6252041631
commit 4322383ed8
14 changed files with 938 additions and 1 deletions
+5
View File
@@ -58,6 +58,11 @@
label: 'Gallery',
icon: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
},
{
href: '/wiki',
label: 'Wiki',
icon: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253'
},
{
href: '/feature-requests',
label: 'Requests',
+51
View File
@@ -352,3 +352,54 @@ export const featureRequests = pgTable('feature_requests', {
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
});
// ─── Wiki ───────────────────────────────────────────────────────────
export const wikiCategories = pgTable('wiki_categories', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
slug: text('slug').unique().notNull(),
description: text('description'),
sortOrder: integer('sort_order').default(0).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
});
export const wikiPages = pgTable(
'wiki_pages',
{
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
slug: text('slug').unique().notNull(),
content: text('content').notNull(),
categoryId: uuid('category_id').references(() => wikiCategories.id, { onDelete: 'set null' }),
createdBy: text('created_by'),
updatedBy: text('updated_by'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
},
(table) => [
index('wiki_pages_category_idx').on(table.categoryId),
index('wiki_pages_slug_idx').on(table.slug)
]
);
export const wikiTags = pgTable('wiki_tags', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').unique().notNull()
});
export const wikiPageTags = pgTable(
'wiki_page_tags',
{
pageId: uuid('page_id')
.notNull()
.references(() => wikiPages.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id')
.notNull()
.references(() => wikiTags.id, { onDelete: 'cascade' })
},
(table) => [
index('wiki_page_tags_page_idx').on(table.pageId),
index('wiki_page_tags_tag_idx').on(table.tagId)
]
);
+9
View File
@@ -0,0 +1,9 @@
export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_]+/g, '-')
.replace(/^-+|-+$/g, '')
.substring(0, 100);
}