> ## Documentation Index
> Fetch the complete documentation index at: https://quickbutik.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Server-side rendering

> Use @quickbutik/kit on Node, Hono, Express, Astro and Cloudflare Workers with per-request cookie storage.

The root entry runs the same on a server as in a browser. Server rendering keeps your catalog indexable and lets the server and the browser share **one cart** through cookies.

## One config, one client per request

A server serves many shoppers, so a client must be bound to **this request's cookies**. Keep one long-lived config and bind a client per request with `withStorage()`:

```ts src/kit.server.ts theme={null}
import { createShopkitClient, createRequestCookieStorage } from "@quickbutik/kit"

const base = { publishableKey: process.env.QUICKBUTIK_PUBLISHABLE_KEY! }

export function kitFor(request: Request, setCookies: string[]) {
  return createShopkitClient(base).withStorage(
    createRequestCookieStorage(request, {
      collect: setCookies,                              // writes become Set-Cookie strings
      attributes: { secure: true, sameSite: "lax" },    // no ambient `location` on a server: pass secure explicitly
    }),
  )
}
```

`createRequestCookieStorage(source, options)` reads cookies straight off a `Request`, a `Headers` object or a raw `Cookie` header string. With `collect`, every write is appended as a serialized `Set-Cookie` string for you to attach to the response. `withStorage()` returns a copy of the client bound to that storage.

<Warning>
  **Without per-request storage, the server forgets the cart.** A client with no cookie accessor on a server resolves to memory storage, so `cart.add()` creates a new cart each time and `checkout.start()` without `cartId` hands off an empty cart, which is refused with a `400`.
</Warning>

## Reads versus writes

| In a handler that… | Call | Why |
| - | - | - |
| Only renders (a product page, a layout, a badge) | `cart.current()` | Reads, **never creates**. A crawler or first-time visitor causes no write |
| Can set cookies (POST handler, form action) | `cart.add()`, `cart.ensure()`, `checkout.start()` | These create and remember a cart or session, which needs writable storage |

Cookies must **not** be `httpOnly` if browser code also reads the cart (the elements, `useCart`, the shared cart store). The default leaves `httpOnly` off.

## Hono and Cloudflare Workers

```ts src/index.ts theme={null}
import { Hono } from "hono"
import { kitFor } from "./kit.server"

const app = new Hono()

app.get("/products/:slug", async (c) => {
  const setCookies: string[] = []
  const kit = kitFor(c.req.raw, setCookies)
  const [product, cart] = await Promise.all([
    kit.products.getBySlug(c.req.param("slug")),
    kit.cart.current(),               // read only
  ])
  if (!product) return c.notFound()
  return c.html(renderProductPage(product, cart))
})

app.post("/cart", async (c) => {
  const setCookies: string[] = []
  const kit = kitFor(c.req.raw, setCookies)
  const form = await c.req.formData()
  await kit.cart.add({
    productId: String(form.get("productId")),
    variantId: form.get("variantId") ? Number(form.get("variantId")) : undefined,
  })
  const res = c.redirect("/cart", 303)
  for (const cookie of setCookies) res.headers.append("set-cookie", cookie)
  return res
})

app.post("/checkout", async (c) => {
  const setCookies: string[] = []
  const kit = kitFor(c.req.raw, setCookies)
  const origin = new URL(c.req.url).origin
  const { url } = await kit.checkout.start({
    successUrl: `${origin}/success`,
    backUrl: `${origin}/`,
    language: "sv",
  })
  const res = c.redirect(url, 303)      // the checkout session id is remembered in a cookie for the return leg
  for (const cookie of setCookies) res.headers.append("set-cookie", cookie)
  return res
})

export default app
```

On Workers, read the key from the env binding (`c.env.QUICKBUTIK_PUBLISHABLE_KEY`) instead of `process.env`.

## Express

Express has no `Request` object, so pass the raw `Cookie` header string as the source:

```ts theme={null}
import express from "express"
import { createShopkitClient, createRequestCookieStorage } from "@quickbutik/kit"

const base = { publishableKey: process.env.QUICKBUTIK_PUBLISHABLE_KEY! }
const app = express()

app.post("/checkout", async (req, res) => {
  const setCookies: string[] = []
  const kit = createShopkitClient(base).withStorage(
    createRequestCookieStorage(req.headers.cookie ?? "", { collect: setCookies, attributes: { secure: true } }),
  )
  const origin = `${req.protocol}://${req.get("host")}`
  const { url } = await kit.checkout.start({ successUrl: `${origin}/success`, backUrl: `${origin}/` })
  for (const cookie of setCookies) res.append("Set-Cookie", cookie)
  res.redirect(303, url)
})
```

<Note>
  Deriving `origin` from the `Host` header is convenient for `successUrl`. For canonical URLs in SEO tags, use a fixed configured origin instead, because a canonical built from an attacker-supplied `Host` is a known SEO-poisoning vector.
</Note>

## Astro

Fetch in the frontmatter with a request-bound client, put the `buildSeo()` output in the layout `<head>`, and use the web components in a client `<script>` for the interactive parts:

```astro src/pages/products/[slug].astro theme={null}
---
import { createShopkitClient, createRequestCookieStorage, buildSeo, serializeJsonLd } from "@quickbutik/kit"

const kit = createShopkitClient({ publishableKey: import.meta.env.PUBLIC_QUICKBUTIK_PUBLISHABLE_KEY })
  .withStorage(createRequestCookieStorage(Astro.request))

const product = await kit.products.getBySlug(Astro.params.slug!)
if (!product) return Astro.redirect("/404")

const seo = buildSeo({
  product,
  baseUrl: "https://myshop.com",
  currency: "SEK",
  productPath: "/products/{slug}",
})
---
<html lang="sv">
  <head>
    <title>{seo.title}</title>
    <meta name="description" content={seo.description} />
    <link rel="canonical" href={seo.canonical} />
    {seo.jsonLd.map((node) => <script type="application/ld+json" set:html={serializeJsonLd(node)} />)}
  </head>
  <body>
    <h1>{product.name}</h1>
    <!-- Server-rendered HTML above; interactive picker and cart below -->
    <qb-product product-id={product.id}>
      <qb-add-to-cart><button type="button">Add to cart</button></qb-add-to-cart>
    </qb-product>

    <script>
      // Browser only: never import /elements in the frontmatter
      import { configure, defineElements } from "@quickbutik/kit/elements"
      configure({ publishableKey: import.meta.env.PUBLIC_QUICKBUTIK_PUBLISHABLE_KEY, currency: "SEK", locale: "sv-SE" })
      defineElements()
    </script>
  </body>
</html>
```

Server-rendered HTML with the elements on top is the combination that keeps the catalog indexable and the cart interactive. `serializeJsonLd()` escapes every `<`, so merchant content can't close the script tag. For a cart that works without JavaScript, post forms to an [API endpoint](https://docs.astro.build/en/guides/endpoints/) that uses the Hono-style pattern above.

## Build time: static params and sitemaps

`products.listAll()` pages through the whole catalog (bounded at 200 pages). It's for build time and sitemaps, not for a request path:

```ts theme={null}
const products = await kit.products.listAll()
const urls = products
  .filter((p) => p.slug)
  .map((p) => `https://myshop.com/products/${p.slug}`)
```

Building a slug → id map at deploy time this way also avoids `getBySlug()` walking pages on a hot path. After that, look products up with `products.get(id)`, which is a single request.

## Bring your own storage adapter

Any object with `kind`, `get`, `set` and `remove` works as storage, and each method may return a promise. That covers a signed cookie, a Redis or KV session, or a test double:

```ts theme={null}
import type { StorageAdapter } from "@quickbutik/kit"

const kvStorage: StorageAdapter = {
  kind: "kv",
  get: (key) => env.SESSIONS.get(`${sessionId}:${key}`),
  set: (key, value, options) =>
    env.SESSIONS.put(`${sessionId}:${key}`, value, { expirationTtl: options?.ttlSeconds }),
  remove: (key) => env.SESSIONS.delete(`${sessionId}:${key}`),
}

createShopkitClient({ publishableKey, storage: kvStorage })
```

More on cookies, adapters and how `"auto"` storage resolves: [Storage & SSR](/kit/concepts/storage-and-ssr).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.