Content Management
SvelteForge Admin includes a built-in CMS for creating and managing pages, powered by SvelteKit form actions and Drizzle ORM. Content is stored in SQLite with full status workflow support, template selection, and automatic author tracking — all driven by Svelte 5 reactivity.
Page Editor
The page editor provides a straightforward interface for creating and editing content pages. Each page consists of a title, slug, content body, template type, and publication status.
Creating a Page
- Title — The page title, displayed in the content table and used for auto-generating the slug.
- Slug — Auto-generated from the title on creation. The slug is editable before saving and must be unique across all pages.
- Content — A textarea for the page body. Accepts plain text or HTML content.
- Template — Select from Default, Landing, or Blog. Stored as an enum in the Drizzle schema.
- Status — Set the initial status: Draft, Published, or Archived.
Auto-Generated Slugs
When you type a title, the slug field is automatically populated with a URL-friendly version (lowercase, hyphens replacing spaces, special characters stripped). You can manually edit the slug before saving. Slug uniqueness is validated server-side — the form action returns an error if a duplicate slug exists.
Templates
| Template | Use Case |
|---|---|
| Default | Standard content pages with a sidebar layout |
| Landing | Full-width pages for marketing or promotional content |
| Blog | Article-style pages with author attribution and date display |
Status Workflow
Pages follow a three-stage lifecycle:
Draft --> Published --> Archived - Draft — Work in progress. Not visible to the public. Default status for new pages.
- Published — Live and accessible. When a page transitions to "published", the
publishedAttimestamp is automatically set to the current date and time. - Archived — Removed from active listings but preserved in the database for reference.
Status transitions are unrestricted — you can move a page directly from Draft to Archived or revert a Published page back to Draft.
Content Table
The content table lists all pages with relevant metadata. Data is loaded server-side via a Drizzle query that performs a left join with the users table to resolve the author name.
Displayed Columns
- Title — The page title, linked to the edit view
- Slug — The URL-friendly identifier
- Status — A color-coded badge (Draft, Published, Archived)
- Template — The assigned template type
- Author — Resolved from the users table via left join
- Created / Updated — Timestamps for tracking content changes
Table Features
- Sortable columns — Click headers to sort by title, status, author, or date
- Status filtering — Filter the table to show only Draft, Published, or Archived pages
- Pagination — Uses the
data-table-paginationcomponent with configurable page sizes - Checkbox selection — Select individual or all rows for bulk operations
// content/+page.server.ts — load function with author join
export const load: PageServerLoad = async () => {
const allPages = db
.select({
id: pages.id,
title: pages.title,
slug: pages.slug,
status: pages.status,
template: pages.template,
authorName: users.name,
createdAt: pages.createdAt,
updatedAt: pages.updatedAt,
publishedAt: pages.publishedAt
})
.from(pages)
.leftJoin(users, eq(pages.authorId, users.id))
.orderBy(desc(pages.createdAt))
.all();
return { pages: allPages };
}; Creating Content
New pages are created through a form that submits to the create form action in content/+page.server.ts. The form uses SvelteKit's use:enhance for progressive
enhancement.
Server-Side Processing
- All fields are validated on the server (title required, slug format and uniqueness)
- The
authorIdis set from the current user session — it cannot be spoofed from the client - If the status is set to "published" on creation,
publishedAtis populated automatically - The page ID is generated using
generateId()from#lib/server/id.ts
Editing Content
The same form component renders in edit mode when an existing page is selected. All fields are editable, including the slug (with uniqueness re-validated against other pages).
- The
updateform action processes changes and updates theupdatedAttimestamp. - Status transitions are tracked — changing from any status to "published" sets the
publishedAttimestamp if it was not previously set.
Deleting Content
Single Delete
Deleting a page requires confirmation through a dialog. The delete form action removes
the page and its associated data from the database.
Bulk Delete
Select multiple pages via checkboxes and delete them in one operation. The bulkDelete form action processes all selected page IDs in a single database transaction.
// Form actions for content deletion
delete: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id') as string;
db.delete(pages).where(eq(pages.id, id)).run();
},
bulkDelete: async ({ request }) => {
const formData = await request.formData();
const ids = JSON.parse(formData.get('ids') as string);
for (const id of ids) {
db.delete(pages).where(eq(pages.id, id)).run();
}
} Export
Content data can be exported using the shared utility functions from #lib/utils/export.ts:
- CSV export — Generates a properly escaped CSV file with headers matching the table columns. Handles commas, quotes, and newlines within content fields.
- JSON export — Outputs a pretty-printed JSON array of all page objects with 2-space indentation.
Both export functions trigger an automatic file download in the browser with a timestamped filename.
Database Schema Reference
The pages table in src/lib/server/db/schema.ts defines the following columns:
| Column | Type | Notes |
|---|---|---|
id | text (primary key) | Generated via generateId() |
title | text (not null) | Page title |
slug | text (unique, not null) | URL-friendly identifier |
content | text | Page body content |
template | text | Enum: default, landing, blog |
status | text | Enum: draft, published, archived |
authorId | text | Foreign key to users.id |
createdAt | integer (timestamp) | Set on creation |
updatedAt | integer (timestamp) | Updated on every edit |
publishedAt | integer (timestamp) | Set when status becomes "published" |
Svelte 5 Patterns Used
The content management module leverages Svelte 5 runes throughout:
$state— Reactive state for form data bindings, selected checkbox items, status filter, current page, and page size.$derived— Computed values for filtered content views (e.g., showing only published pages) that automatically re-evaluate when the filter state changes.use:enhance— SvelteKit's progressive enhancement directive on all forms, enabling non-blocking submissions. The page updates reactively without a full reload.
// Svelte 5 reactive patterns in the content page
let statusFilter = $state('all');
let selectedIds = $state<string[]>([]);
let currentPage = $state(1);
let filteredPages = $derived(
statusFilter === 'all'
? allPages
: allPages.filter((p) => p.status === statusFilter)
);
let allSelected = $derived(
filteredPages.length > 0 &&
selectedIds.length === filteredPages.length
); Need a Full-Featured CMS?
DashboardPack premium admin templates include a complete CMS with rich text editor, file manager, media library, and SEO tools — all built with Svelte 5 and SvelteKit, production-ready out of the box.
Explore DashboardPack Premium