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
+132
View File
@@ -0,0 +1,132 @@
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db/index.js';
import { wikiPages, wikiCategories, wikiTags, wikiPageTags } from '$lib/server/db/schema.js';
import { eq, ilike, or, desc, sql, count } from 'drizzle-orm';
import { fail } from '@sveltejs/kit';
import { slugify } from '$lib/utils/slug.js';
export const load: PageServerLoad = async ({ url }) => {
const search = url.searchParams.get('q');
const tagFilter = url.searchParams.get('tag');
// Categories
const categories = await db
.select()
.from(wikiCategories)
.orderBy(wikiCategories.sortOrder, wikiCategories.name);
// Pages query
let pagesQuery;
if (tagFilter) {
// Filter by tag
const [tag] = await db.select().from(wikiTags).where(eq(wikiTags.name, tagFilter));
if (tag) {
const taggedPageIds = await db
.select({ pageId: wikiPageTags.pageId })
.from(wikiPageTags)
.where(eq(wikiPageTags.tagId, tag.id));
const ids = taggedPageIds.map((r) => r.pageId);
if (ids.length > 0) {
pagesQuery = db
.select({
id: wikiPages.id,
title: wikiPages.title,
slug: wikiPages.slug,
categoryId: wikiPages.categoryId,
categoryName: wikiCategories.name,
updatedAt: wikiPages.updatedAt,
updatedBy: wikiPages.updatedBy
})
.from(wikiPages)
.leftJoin(wikiCategories, eq(wikiPages.categoryId, wikiCategories.id))
.where(sql`${wikiPages.id} IN ${ids}`)
.orderBy(desc(wikiPages.updatedAt));
}
}
} else if (search) {
pagesQuery = db
.select({
id: wikiPages.id,
title: wikiPages.title,
slug: wikiPages.slug,
categoryId: wikiPages.categoryId,
categoryName: wikiCategories.name,
updatedAt: wikiPages.updatedAt,
updatedBy: wikiPages.updatedBy
})
.from(wikiPages)
.leftJoin(wikiCategories, eq(wikiPages.categoryId, wikiCategories.id))
.where(or(ilike(wikiPages.title, `%${search}%`), ilike(wikiPages.content, `%${search}%`)))
.orderBy(desc(wikiPages.updatedAt));
} else {
pagesQuery = db
.select({
id: wikiPages.id,
title: wikiPages.title,
slug: wikiPages.slug,
categoryId: wikiPages.categoryId,
categoryName: wikiCategories.name,
updatedAt: wikiPages.updatedAt,
updatedBy: wikiPages.updatedBy
})
.from(wikiPages)
.leftJoin(wikiCategories, eq(wikiPages.categoryId, wikiCategories.id))
.orderBy(desc(wikiPages.updatedAt));
}
const pages = pagesQuery ? await pagesQuery : [];
// Get tags for each page
const pageIds = pages.map((p) => p.id);
let tagsByPage: Record<string, string[]> = {};
if (pageIds.length > 0) {
const pageTags = await db
.select({
pageId: wikiPageTags.pageId,
tagName: wikiTags.name
})
.from(wikiPageTags)
.innerJoin(wikiTags, eq(wikiPageTags.tagId, wikiTags.id))
.where(sql`${wikiPageTags.pageId} IN ${pageIds}`);
for (const pt of pageTags) {
if (!tagsByPage[pt.pageId]) tagsByPage[pt.pageId] = [];
tagsByPage[pt.pageId].push(pt.tagName);
}
}
// All tags with counts
const allTags = await db
.select({ name: wikiTags.name, count: count() })
.from(wikiTags)
.innerJoin(wikiPageTags, eq(wikiTags.id, wikiPageTags.tagId))
.groupBy(wikiTags.name)
.orderBy(wikiTags.name);
return {
categories,
pages: pages.map((p) => ({ ...p, tags: tagsByPage[p.id] ?? [] })),
allTags,
filters: { search, tag: tagFilter }
};
};
export const actions: Actions = {
createCategory: async ({ request }) => {
const formData = await request.formData();
const name = (formData.get('name') as string)?.trim();
if (!name) return fail(400, { error: 'Category name is required' });
const slug = slugify(name);
await db.insert(wikiCategories).values({ name, slug });
return { categoryCreated: true };
},
deleteCategory: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id') as string;
await db.delete(wikiCategories).where(eq(wikiCategories.id, id));
return { categoryDeleted: true };
}
};