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 }
}
)