Get started
Install the genko.me SDK in a React app and display a list and a single entry in the browser.
This guide uses a Vite React app to display a list and a single entry. React Router and CRA setups follow the same steps apart from reading environment variables.
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 React 19 app and a bundler such as Vite.
@genko-me/reactlists React 19 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/sdk @genko-me/react@genko-me/react provides RichText for entry bodies and sharing buttons. If you only need to fetch, @genko-me/sdk is enough.
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.
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;You can also generate schema.ts from the dashboard with @genko-me/cli init or pull. Its generated client.ts reads process.env.GENKO_API_KEY, so change that to import.meta.env as above for Vite. The CLI adds @genko-me/react only to projects that depend on next.
Show a list
Fetch in useEffect and store the result in state. Handle loading, failure, and an empty result.
import { useEffect, useState } from "react";import type { GenkoEntryMeta } from "@genko-me/sdk";import { client } from "../lib/genko";import type { Blog } from "../lib/schema";
export function BlogList() { const [posts, setPosts] = useState<(Blog & GenkoEntryMeta)[] | null>(null); const [failed, setFailed] = useState(false);
useEffect(() => { let active = true; client.blog .list({ limit: 10, orders: "-publishedAt" }) .then((result) => { if (active) setPosts(result.contents); }) .catch(() => { if (active) setFailed(true); }); return () => { active = false; }; }, []);
if (failed) return <p>Could not load entries.</p>; if (!posts) return <p>Loading…</p>; if (posts.length === 0) return <p>No published entries yet.</p>;
return ( <ul> {posts.map((post) => ( <li key={post.id}> <a href={`/blog/${post.id}`}>{post.title}</a> </li> ))} </ul> );}list() returns contents and totalCount. totalCount is the total number matching the query; contents.length is the number fetched this time. See Sorting, filtering, and search for query options.
In development, React Strict Mode runs useEffect twice. Clearing active in cleanup as shown prevents an older response from replacing the current state.
Show a single entry
Pass a rich-text body to RichText. If you put an HTML string directly in JSX braces, the tags display as text.
import { useEffect, useState } from "react";import { RichText } from "@genko-me/react";import { ShareButtons } from "@genko-me/react/share-buttons";import type { GenkoEntryMeta } from "@genko-me/sdk";import { client } from "../lib/genko";import type { Blog } from "../lib/schema";
export function BlogPost({ id }: { id: string }) { const [post, setPost] = useState<(Blog & GenkoEntryMeta) | null | undefined>(undefined);
useEffect(() => { let active = true; client.blog .getOrNull(id) .then((result) => { if (active) setPost(result); }) .catch(() => { if (active) setPost(null); }); return () => { active = false; }; }, [id]);
if (post === undefined) return <p>Loading…</p>; if (post === null) return <p>Entry not found.</p>;
return ( <article> <h1>{post.title}</h1> <RichText html={post.body} as="div" className="article-body" /> <ShareButtons url={`https://your-site.example/blog/${post.id}`} title={post.title} /> </article> );}getOrNull() returns null only for a 404. Authentication and server errors still throw, so handle them in catch.
Set body heading and paragraph spacing and image maximum width in your site's CSS.
.article-body { line-height: 1.9; }.article-body h2 { margin-block: 2em 0.75em; font-size: 1.5em; }.article-body p { margin-block: 1em; }.article-body img { max-width: 100%; height: auto; }Pass the public entry URL on your site to ShareButtons url, not the delivery API URL. See Rich text and sharing components for props and CSS variables.
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? |
| Body shows HTML tags | Did you write {post.body} instead of passing it to RichText? |
Next, use the tutorial to add pagination and draft previews.
Last updated: October 6, 2026