genko.meDocs
Frameworks and languagesVue

Get started

Install @genko-me/vue in a Vite Vue app and fetch and display a list and a single entry in the browser.

This guide uses a Vite Vue app 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 Vue 3.5 or later project with a bundler such as Vite. @genko-me/vue lists Vue 3.5 as a peer dependency.
  • Vue Router for multiple pages. This guide uses it to move between the list and detail pages.
  • 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

bash
npm install @genko-me/vue @genko-me/sdk

This page covers @genko-me/[email protected]. At build time, you may see a warning that crypto from @genko-me/sdk was externalized. You can ignore it: the browser does not call the Node API used for webhook signature verification and falls back to WebCrypto.

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.

.env.local
VITE_GENKO_WORKSPACE=your-workspaceVITE_GENKO_API_KEY=your-issued-key

Define your API and types. Match field IDs and types to the schema in the dashboard.

src/lib/schema.ts
import { listApi } from "@genko-me/sdk";
export type Blog = { title: string; body: string; publishedAt: string };
export const apis = {  blog: listApi<Blog>({    fields: [      { id: "title", type: "text" },      { id: "body", type: "richtext" },      { id: "publishedAt", type: "datetime" },    ],  }),};

Create the connecting client in one place.

src/lib/genko.ts
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 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 only adds @genko-me/react to projects that depend on next; it does not add @genko-me/vue, which you must install yourself.

To use @genko-me/vue components in templates, register them together with the plugin in main.ts. You can also import each component separately.

src/main.ts
import { createApp } from "vue";import { GenkoVuePlugin } from "@genko-me/vue";import App from "./App.vue";import { router } from "./router";
createApp(App).use(router).use(GenkoVuePlugin).mount("#app");

Show a list

Get fields and schemas synchronously from getApiBinding(client.blog); they come from the dashboard schema and require no request. Fetch the actual content in onMounted.

src/components/BlogList.vue
<script setup lang="ts">import { onMounted, ref } from "vue";import { getApiBinding } from "@genko-me/sdk";import type { GenkoListResult } from "@genko-me/sdk";import { client } from "../lib/genko";
const binding = getApiBinding(client.blog);const initial = ref<GenkoListResult<Record<string, unknown>>>();const failed = ref(false);
onMounted(async () => {  try {    initial.value = await client.blog.list({ limit: 10, orders: "-publishedAt" });  } catch {    failed.value = true;  }});</script>
<template>  <Error v-if="failed" template />  <Loading v-else-if="!initial" template />  <ListClient v-else :initial="initial" :initial-page="1" :fields="binding.fields" :schemas="binding.schemas" href="/blog/{id}">    <article>      <h2><ListItem id="title" /></h2>    </article>    <template #empty><Null>No published entries yet.</Null></template>  </ListClient></template>

list() returns contents and totalCount. The default slot of ListClient defines one item's layout. The named pagination, loading, error, and empty slots are not repeated. If you omit a state slot, nothing appears for that state.

href="/blog/{id}" renders a regular <a>. With Vue Router in history mode, navigation works without a full page reload.

Show a single entry

As on the list page, get fields and schemas from getApiBinding and fetch the entry with getOrNull().

src/router.ts
import { createRouter, createWebHistory } from "vue-router";import BlogList from "./components/BlogList.vue";import BlogPost from "./components/BlogPost.vue";
export const router = createRouter({  history: createWebHistory(),  routes: [    { path: "/blog", component: BlogList },    { path: "/blog/:id", component: BlogPost },  ],});
src/components/BlogPost.vue
<script setup lang="ts">import { onMounted, ref } from "vue";import { useRoute } from "vue-router";import { getApiBinding } from "@genko-me/sdk";import type { GenkoEntryMeta } from "@genko-me/sdk";import { client } from "../lib/genko";import type { Blog } from "../lib/schema";
const route = useRoute();const binding = getApiBinding(client.blog);const post = ref<(Blog & GenkoEntryMeta) | null>();
onMounted(async () => {  try {    post.value = await client.blog.getOrNull(route.params.id as string);  } catch {    post.value = null;  }});</script>
<template>  <p v-if="post === undefined">Loading…</p>  <p v-else-if="post === null">Entry not found.</p>  <EntryProvider v-else :entry="post" :fields="binding.fields" :schemas="binding.schemas">    <h1><ViewItem id="title" /></h1>    <ViewItem id="body" />  </EntryProvider></template>

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; ViewItem adds no default styling.

Start the development server and open /blog. If you see entries and can click one to open its body at /blog/{id}, the connection works.

Troubleshooting

SymptomCheck
Empty listIs an entry saved and Published? Has its publication end time passed?
401Is the key still valid, is its name prefixed with VITE_, and did you restart the development server?
403Does the key belong to the workspace you are connecting to?
404Are the workspace ID and endpoint correct, and does the key allow this API?
<ListItem> and others pass throughDid you register GenkoVuePlugin with app.use() or import each component?
Field component errorsIs <ListItem> or <ViewItem> outside <EntryProvider> or <ListClient>?

Next, use the tutorial to add pagination and draft previews.

Last updated: October 16, 2026

On this page