Back to Arky

Content Website

Build a blog or content site with the CMS module

This guide shows how to build a content-driven website using the Arky CMS module.

Client Boundaries

Provision collections and edit content from a trusted Admin process. Keep its Personal API Token out of browser bundles. Public website reads use a separate storefront client initialized with the Store’s publishable key:

import { createAdmin } from 'arky-sdk/admin';
import { initialize } from 'arky-sdk/storefront';

const storeId = process.env.ARKY_STORE_ID!;
const sdk = createAdmin({
  baseUrl: process.env.ARKY_API_URL!,
  storeId,
  market: 'us',
  apiToken: process.env.ARKY_PERSONAL_API_TOKEN!,
});

const arky = initialize(import.meta.env.PUBLIC_ARKY_PUBLISHABLE_KEY, {
  locale: 'en',
  market: 'us',
});

The sdk examples below are trusted provisioning calls. The arky examples are safe public storefront reads and do not need a Store ID or private token.

Overview

Arky CMS is built around two primitives:

  • Collections define the shape of content, similar to a database table.
  • Entries hold the actual content inside a collection, similar to rows.

Taxonomies stay separate. Use taxonomies for hierarchical classification and filters on products, services, providers, and content-adjacent records. Use collection schemas and blocks for CMS data modeling.

Content Structure

A blog can be modeled as a blog_posts collection:

const posts = await sdk.cms.collection.create({
  store_id: storeId,
  key: 'blog_posts',
  schema: [
    {
      id: 'title',
      key: 'title',
      type: 'localized_text',
      required: true,
      properties: {},
      children: []
    },
    {
      id: 'excerpt',
      key: 'excerpt',
      type: 'localized_text',
      required: false,
      properties: {},
      children: []
    },
    {
      id: 'body',
      key: 'body',
      type: 'markdown',
      required: true,
      properties: {},
      children: []
    },
    {
      id: 'hero',
      key: 'hero',
      type: 'media',
      required: false,
      properties: { on_delete: 'set_null' },
      children: []
    }
  ]
});

Then create entries in that collection:

const post = await sdk.cms.entry.create({
  store_id: storeId,
  collection_id: posts.id,
  key: 'launch-notes',
  slug: { en: 'launch-notes' },
  blocks: [
    {
      id: crypto.randomUUID(),
      key: 'title',
      type: 'localized_text',
      properties: {},
      value: { en: 'Launch Notes' }
    },
    {
      id: crypto.randomUUID(),
      key: 'body',
      type: 'markdown',
      properties: {},
      value: { en: '## What changed\n\nWe shipped a cleaner CMS model.' }
    }
  ]
});

Fetching Content

List active entries for a collection:

const { items, cursor } = await arky.cms.entry.find({
  collection_id: posts.id,
  status: 'active',
  sort_field: 'created_at',
  sort_direction: 'desc',
  limit: 10
});

Get one entry by ID:

const entry = await arky.cms.entry.get({
  id: post.id
});

For unique pages such as a homepage, create a pages collection and store one entry with a stable key:

const pages = await sdk.cms.collection.create({
  store_id: storeId,
  key: 'pages',
  schema: [
    {
      id: 'heading',
      key: 'heading',
      type: 'localized_text',
      required: false,
      properties: {},
      children: []
    },
    {
      id: 'body',
      key: 'body',
      type: 'markdown',
      required: false,
      properties: {},
      children: []
    }
  ]
});
Tip

You do not need a special singleton type. A collection can contain one entry when the content is singular, or many entries when it is repeatable.

Entry Structure

Collections define a schema for entry blocks. Entries store blocks that are validated against that schema, while collection-level blocks can hold singleton-style content for pages or website settings.

Blocks can reference media, entries, products, and digital products by ID. Reference targets remain separate domain objects and are fetched explicitly.

Media Fields

Media stays its own primitive. CMS entries reference media through media blocks:

await sdk.cms.entry.update({
  store_id: storeId,
  id: post.id,
  blocks: [
    {
      id: crypto.randomUUID(),
      key: 'hero',
      type: 'media',
      properties: { on_delete: 'set_null' },
      value: 'media_hero123'
    }
  ]
});

Storefront responses retain the media ID. Fetch all referenced media in one explicit batch so entry reads never trigger hidden nested reads:

import { collectBlockReferences } from 'arky-sdk';

const references = collectBlockReferences(post.blocks);
const { data: media } = await arky.cms.media.findByIds({
  ids: references.mediaIds
});

Entries are indexed through the same event and search pipeline as the rest of the store. Use query for text search and field filters for exact CMS-specific filters:

const results = await arky.cms.entry.find({
  collection_id: posts.id,
  query: 'launch',
  status: 'active',
  limit: 20
});

Taxonomies

Use taxonomies when the value is primarily classification:

  • Product categories
  • Service categories
  • Provider specialties
  • Region or location groupings
  • Filter facets that should remain outside CMS schemas

Use collections and entries when the value is content data:

  • Authors
  • Testimonials
  • Case studies
  • Page sections