with one click
page-builder
// Patterns for building list and detail pages with forms, filters, and data fetching
// Patterns for building list and detail pages with forms, filters, and data fetching
Create SvelteKit components using Remote Functions for type-safe client-server communication. Use when building components that need to fetch data, submit forms, or execute server commands. Remote Functions work at the component level, not page level.
Create admin dashboard pages with tables, forms, and actions
Create UI components using tailwind-variants for type-safe styling. Use when creating or editing components in src/lib/ui/.
Create Playwright E2E tests using Page Object Model pattern with database isolation
Demonstrates progressive disclosure by linking to reference files. Use this pattern when your skill has detailed content that should load on-demand.
A minimal example skill demonstrating the required structure. Use this as a template when creating new skills.
| name | Page Builder |
| description | Patterns for building list and detail pages with forms, filters, and data fetching |
Universal patterns for building pages. These apply to both public-facing and admin pages.
Remote-First: Put logic in data.remote.ts, keep pages as pure renderers.
See using-remote-functions/REMOTE-FIRST.md for the full pattern.
| Type | Purpose | Reference |
|---|---|---|
| List Page | Display items with filters/search | LIST-PAGE.md |
| Detail Page | View/edit single item with form | DETAIL-PAGE.md |
| Feed Page | Mixed-type items with components | FEED-PAGE.md |
src/routes/
āāā [section]/
āāā +page.svelte # List page
āāā data.remote.ts # Remote functions (queries, forms)
āāā [slug]/
āāā +page.svelte # Detail page
Map data types to components for clean rendering:
<script>
const components = new Map([
['article', ArticleCard],
['video', VideoCard],
['cta', CTABanner]
])
</script>
{#each items as item, index (index)}
{@const Component = components.get(item.type)}
<Component {...item.props} />
{/each}
Native forms that update URL params on input:
<script>
let form: HTMLFormElement
function submitForm() { form.requestSubmit() }
function debouncedSubmit() {
clearTimeout(timer)
timer = setTimeout(submitForm, 300)
}
</script>
<form bind:this={form}>
<input name="search" oninput={debouncedSubmit} />
<select name="status" onchange={submitForm} />
</form>
Populate forms with existing data:
<script>
import { initForm } from '$lib/utils/form.svelte'
initForm(updateItem, () => ({
id: item.id,
name: item.name ?? '',
status: item.status ?? 'draft'
}))
</script>
<script>
import Pagination from '$lib/ui/Pagination.svelte'
</script>
<Pagination count={totalItems} perPage={20} />
| Component | Purpose |
|---|---|
Input | Text, email, password inputs with validation |
Textarea | Multi-line text with validation |
Select | Dropdown with options |
Checkbox | Boolean toggle |
All accept:
label - Field labeldescription - Helper textissues - Validation errors from field.issues()...rest - Spread from field.as('type')const filtersSchema = z.object({
search: z.string().catch(''),
status: z.string().catch(''),
page: z.number().catch(1)
})
export const getItems = query('unchecked', async (searchParams: URLSearchParams) => {
const { locals } = getRequestEvent()
const filters = parseSearchParams(filtersSchema, searchParams)
return locals.service.getFiltered(filters)
})
export const updateItem = form(
z.object({
id: z.string(),
name: z.string().min(1, 'Required'),
status: z.enum(['draft', 'published'])
}),
async (data) => {
const { locals } = getRequestEvent()
await locals.service.update(data.id, data)
return { success: true }
}
)