Back to Arky

Content

Collections with typed schemas, entries with slugs per language, and the block types every record uses

A collection is a content type. Stores use it two ways: a list with a schema, such as a reviews collection whose entries each have a name, a quote and a title; and site-wide text with no schema, such as a website collection whose text sits in the collection’s own blocks.

Blocks

Every record with blocks (collections, entries, products, variants, booking services and resources, store locations, companies, categories, customer groups and their offerings) uses the same block types:

type Block =
	| { id: string; key: string; type: "text"; value: string | null }
	| { id: string; key: string; type: "localized_text"; value: Record<string, string> | null }
	| { id: string; key: string; type: "markdown"; value: string | null }
	| { id: string; key: string; type: "localized_markdown"; value: Record<string, string> | null }
	| { id: string; key: string; type: "number"; value: number | null }
	| { id: string; key: string; type: "boolean"; value: boolean | null }
	| { id: string; key: string; type: "date"; value: string | null }
	| { id: string; key: string; type: "date_time"; value: EpochMilliseconds | null }
	| { id: string; key: string; type: "geo_location"; value: { lat: number; lon: number } | null }
	| { id: string; key: string; type: "media" | "entry" | "form" | "product"; value: string | null }
	| { id: string; key: string; type: "array"; value: Block[] }
	| { id: string; key: string; type: "object"; value: Block[] };
  • localized_text and localized_markdown hold one text per language, each one of the store’s languages.
  • date is a day (YYYY-MM-DD); date_time is a moment in epoch milliseconds.
  • media, entry, form and product hold the id of the record they point at, at any depth: a record of the same store that exists and isn’t being deleted, never the record itself. The pointed-at record can’t be deleted while the block points at it.
  • object holds a list of fields, like the top level does.
  • Under a schema, each level’s keys are unique and defined by the schema, an array item matches its item type by key, required applies to single values, and a missing list or object counts as empty, so min_items and min_fields catch it.
const title: Block = {
	id: crypto.randomUUID(),
	key: "title",
	type: "localized_text",
	value: { en: "Hello", de: "Hallo" },
};

Collections

POST /v1/stores/{store_id}/collections
SDK: sdk.content.collection.create()
GET /v1/stores/{store_id}/collections
SDK: sdk.content.collection.find()
GET /v1/stores/{store_id}/collections/{id}
SDK: sdk.content.collection.get()
GET /v1/stores/{store_id}/collections/by-key/{key}
SDK: sdk.content.collection.getByKey()
PUT /v1/stores/{store_id}/collections/{id}
SDK: sdk.content.collection.update()
DELETE /v1/stores/{store_id}/collections/{id}?expected_updated_at=
SDK: sdk.content.collection.delete()
const reviews = await sdk.content.collection.create({
	store_id: storeId,
	id: crypto.randomUUID(),
	key: "reviews",
	schema: [
		{ id: crypto.randomUUID(), key: "name", type: { type: "text", required: true, min_length: 1, max_length: 80, pattern: null } },
		{ id: crypto.randomUUID(), key: "quote", type: { type: "localized_markdown", required: true } },
		{ id: crypto.randomUUID(), key: "photo", type: { type: "media", required: false } },
	],
	blocks: [],
	status: { type: "active" },
});

Each schema type carries only its own rules:

Schema typeRules
text, markdownrequired, min_length, max_length, pattern
localized_text, localized_markdown, boolean, date, date_time, geo_location, media, form, productrequired
numberrequired, min, max
entryrequired, collections: { type: "all" } or { type: "only", collection_ids }
arrayitems (the item types, matched by key), min_items, max_items
objectfields, min_fields, max_fields

A schema field keeps its key and type for good, at any depth: a change of either under the same field id is refused. A required localized_text or localized_markdown field needs text in every store language, because no language stands in for another. An entry field limited to collections names collections of the same store. Every other schema edit is allowed, with or without entries. Entries that no longer fit, including ones without text in a language the store added later, keep their blocks and stay readable; Admin flags them, their next edit must fix them, and storefront reads leave out blocks whose field is gone.

A collection’s key is unique in the store (409 COLLECTION.KEY_TAKEN). A collection is active, draft (being set up) or archived; draft and archived hide the collection and its entries from storefronts. Deleting a collection deletes its entries: it becomes deleting, its entries are removed in batches, then the collection. It is refused while another collection’s schema limits an entry field to it or a webhook filter names it, and an entry that a record outside the collection points at stops the cleanup until that pointer is gone.

Entries

POST /v1/stores/{store_id}/entries
SDK: sdk.content.entry.create()
GET /v1/stores/{store_id}/entries?collection_id=
SDK: sdk.content.entry.find()
GET /v1/stores/{store_id}/entries/{id}
SDK: sdk.content.entry.get()
PUT /v1/stores/{store_id}/entries/{id}
SDK: sdk.content.entry.update()
DELETE /v1/stores/{store_id}/entries/{id}?expected_updated_at=
SDK: sdk.content.entry.delete()
const review = await sdk.content.entry.create({
	store_id: storeId,
	id: crypto.randomUUID(),
	collection_id: reviews.id,
	key: "ana-k",
	slugs: { en: "ana-k", de: "ana-k" },
	blocks: [
		{ id: crypto.randomUUID(), key: "name", type: "text", value: "Ana K." },
		{ id: crypto.randomUUID(), key: "quote", type: "localized_markdown", value: { en: "Lovely fabrics.", de: "Schöne Stoffe." } },
	],
	status: { type: "active" },
});

An entry is { id, store_id, collection_id, key, slugs, blocks, status, created_at, updated_at }. Its blocks follow its collection’s schema, and entries are created and edited only in an active collection. Keys and slugs are unique per collection (slugs per language; 409 ENTRY.KEY_TAKEN and 409 ENTRY.SLUG_TAKEN), so a blog and a news collection can both have launch. Updates and deletes carry the expected_updated_at they read; deleting an entry is refused while another record points at it.

An entry is active, draft, scheduled with a future publish_at, or archived. It is published only while it and its collection are active. A scheduled entry stays unpublished and editable until publish_at, when Arky makes it active as an ordinary update; setting it back to draft cancels the schedule. Publishing doesn’t re-check the schema.

Storefront Reads

import { initialize, collectBlockReferences } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";

const arky = initialize("arky_pk_...", {
	locale: "en",
	createVisitorIdentity: () => ({
		customer_id: crypto.randomUUID(),
		session_id: crypto.randomUUID(),
	}),
	createSessionId: () => crypto.randomUUID(),
	createId: () => crypto.randomUUID(),
	createOrderToken,
});

const website = await arky.content.collection.getByKey({ key: "website" });

const page = await arky.content.entry.find({ collection_id: blog.id, limit: 20 });
const {
	items: [post],
} = await arky.content.entry.find({ collection_id: blog.id, slug: "launch" });

const references = collectBlockReferences(post?.blocks ?? []);
const { items: media } = await arky.media.find({ ids: references.media_ids });
  • find takes collection_id with key, slug, query or filters, or a list of ids. A slug’s language is the client’s locale (X-Arky-Locale). collection.get reads by { id } and collection.getByKey by { key }.
  • Responses never load what blocks point at. Collect the ids with collectBlockReferences and read each kind once; media.find takes any number of ids and reads them 100 at a time. content.load({ collection }) or content.load({ entry }) reads one record with its media and answers its blocks in the locale, and content.loadEntries({ collection_id }) reads every entry of a collection that way.
  • Read localized text with the SDK’s block helpers, which select the requested language and never fall back to another one:
import { getBlockTextValue } from "arky-sdk/storefront";

const titleBlock = post?.blocks.find((block) => block.key === "title");
const text = getBlockTextValue(titleBlock, "en");
Note

Arky never falls back to another store language. Write every localized block in each language the site shows, or handle the missing text in the frontend.