Get started
Install @genko-me/svelte in a Svelte SPA without SvelteKit and display a list and a single entry in the browser.
This guide uses a Vite Svelte SPA to display a list and a single entry.
To create a workspace and blog API, follow steps 1–3 of the quickstart.
Prerequisites
- A genko.me account and workspace. You need Owner access to create an API and issue an API key.
- An API with at least one saved, published entry. The delivery API does not return drafts by default.
- A Svelte 5 project, such as Vite's
svelteorsvelte-tstemplate fromnpm create vite@latest.@genko-me/sveltelists Svelte 5 as a peer dependency. - Node.js 20.12 or later for building the app.
Replace your-workspace with your workspace ID and blog with your endpoint in the examples below.
Install
npm install @genko-me/svelte @genko-me/sdkThis page covers @genko-me/[email protected]. This setup does not use @genko-me/svelte/server; see the overview.
Set the API key and workspace ID
Issue an API key in the dashboard under Settings and plan → API keys (/w/your-workspace/settings/api-keys). Restrict it to selected APIs whose content can be public. The key is bundled into this app and anyone can read it.
In Vite, only environment variables prefixed with VITE_ are exposed to browser code through import.meta.env.
VITE_GENKO_WORKSPACE=your-workspaceVITE_GENKO_API_KEY=your-issued-keyDefine your API and types. Match field IDs and types to the schema in the dashboard. You can generate them with @genko-me/cli init, but its output assumes a Next.js setup, so writing them directly is simpler here.
import { listApi } from "@genko-me/sdk";
export type Blog = { title: string; body: string };
export const apis = { blog: listApi<Blog>({ fields: [ { id: "title", type: "text" }, { id: "body", type: "richtext" }, ], }),};Create the connecting client in one place.
import { createGenko } from "@genko-me/sdk";import { apis } from "./schema";
export const genko = createGenko({ workspace: import.meta.env.VITE_GENKO_WORKSPACE, apiKey: import.meta.env.VITE_GENKO_API_KEY, apis,});
export const client = genko.apis;Show a list
@genko-me/svelte components do not depend on SvelteKit, so you can use ListClient here. Your app does the fetch: call client.blog.list() in onMount and shape the result for ListClient.
You do not need to fetch fields and schemas (schema information used by ListItem) each time. getApiBinding reads them directly from your schema.ts definition.
<script lang="ts"> import { onMount } from "svelte"; import { getApiBinding } from "@genko-me/sdk"; import { ListClient, ListItem, Error, Null, Loading } from "@genko-me/svelte"; import type { ListClientProps } from "@genko-me/svelte"; import { client } from "../lib/genko";
let listProps = $state<Omit<ListClientProps, "children"> | undefined>(undefined); let failed = $state(false);
onMount(async () => { try { const binding = getApiBinding(client.blog); const initial = await client.blog.list({ limit: 10, orders: "-publishedAt" }); listProps = { initial, initialPage: 1, fields: binding.fields, schemas: binding.schemas, href: "/blog/{id}" }; } catch { failed = true; } });</script>
<h1>Blog</h1>
{#if failed} <Error template />{:else if !listProps} <Loading template />{:else} <ListClient {...listProps}> {#snippet children()} <article> <h2><ListItem id="title" /></h2> <time><ListItem id="publishedAt" format="yyyy/MM/dd" timeZone="Asia/Tokyo" /></time> </article> {/snippet} {#snippet empty()}<Null>No published entries yet.</Null>{/snippet} </ListClient>{/if}You can refer to built-in entry fields (id, createdAt, updatedAt, and publishedAt) from ListItem and ViewItem without listing them in schema.ts. Only add API-specific fields such as body or image there.
Because {id} in an href template resembles a Svelte expression, pass it as a string: href="/blog/{id}". The default time zone is UTC. Start the development server; seeing entries confirms the connection.
Show a single entry
Fetch one entry the same way and pass it to EntryProvider. Here, BlogPost.svelte receives an id prop.
<script lang="ts"> import { getApiBinding } from "@genko-me/sdk"; import { EntryProvider, ViewItem, Error } from "@genko-me/svelte"; import type { EntryProviderProps } from "@genko-me/svelte"; import { client } from "../lib/genko";
let { id }: { id: string } = $props();
let entryProps = $state<Omit<EntryProviderProps, "children"> | undefined>(undefined); let notFound = $state(false); let failed = $state(false);
$effect(() => { entryProps = undefined; notFound = false; failed = false; const binding = getApiBinding(client.blog); client.blog .getOrNull(id) .then((entry) => { if (!entry) { notFound = true; return; } entryProps = { entry, fields: binding.fields, schemas: binding.schemas }; }) .catch(() => { failed = true; }); });</script>
{#if failed} <Error template />{:else if notFound} <p>Entry not found.</p>{:else if entryProps} <EntryProvider {...entryProps}> {#snippet children()} <article> <h1><ViewItem id="title" /></h1> <ViewItem id="body" /> </article> {/snippet} </EntryProvider>{/if}getOrNull() returns null only for a 404. Authentication and server errors still throw, so handle them in catch. With $effect, the app refetches whenever the router changes the id prop.
Set body heading and paragraph spacing and image maximum width in your site's CSS. ViewItem adds no default styling.
article :global(h2) { margin-block: 2em 0.75em; font-size: 1.5em; }article :global(p) { margin-block: 1em; }article :global(img) { max-width: 100%; height: auto; }Troubleshooting
| Symptom | Check |
|---|---|
| Empty list | Is an entry saved and Published? Has its publication end time passed? |
401 | Is the key still valid, is its name prefixed with VITE_, and did you restart the development server? |
403 | Does the key belong to the workspace you are connecting to? |
404 | Are the workspace ID and endpoint correct, and does the key allow this API? |
Place field components inside List/View. | Is ListItem or ViewItem outside the children snippet of ListClient or EntryProvider? |
Next, use the tutorial to add pagination and draft previews.
Last updated: October 16, 2026