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

TemplateUse Case
DefaultStandard content pages with a sidebar layout
LandingFull-width pages for marketing or promotional content
BlogArticle-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 publishedAt timestamp 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-pagination component 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 authorId is set from the current user session — it cannot be spoofed from the client
  • If the status is set to "published" on creation, publishedAt is 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 update form action processes changes and updates the updatedAt timestamp.
  • Status transitions are tracked — changing from any status to "published" sets the publishedAt timestamp 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:

ColumnTypeNotes
idtext (primary key)Generated via generateId()
titletext (not null)Page title
slugtext (unique, not null)URL-friendly identifier
contenttextPage body content
templatetextEnum: default, landing, blog
statustextEnum: draft, published, archived
authorIdtextForeign key to users.id
createdAtinteger (timestamp)Set on creation
updatedAtinteger (timestamp)Updated on every edit
publishedAtinteger (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