Add Wiki feature with Markdown, categories, tags, and search
Deploy to LXC / deploy (push) Failing after 4s
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:
@@ -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 };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user