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 }
}
)
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer