Content Website
Build localized custom pages with collections, entries, blocks, media and forms
Arky Content gives a custom frontend two records:
- a collection is a content type: its typed schema, and blocks of its own for site-wide text;
- an entry is one piece of content in a collection, with slugs per language.
Media, forms, products and categories are their own records, pointed at by id from blocks.
Site-Wide Text
A collection with no schema holds a site’s own text in its blocks, such as a website collection
with an info object:
const website = await sdk.content.collection.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "website",
schema: [],
blocks: [
{
id: crypto.randomUUID(),
key: "info",
type: "object",
value: [
{ id: crypto.randomUUID(), key: "title", type: "localized_text", value: { en: "Northwind", de: "Northwind" } },
{ id: crypto.randomUUID(), key: "cta_link", type: "text", value: "/shop" },
],
},
],
status: { type: "active" },
});
A Blog Collection
const posts = await sdk.content.collection.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "posts",
schema: [
{ id: crypto.randomUUID(), key: "title", type: { type: "localized_text", required: true } },
{ id: crypto.randomUUID(), key: "body", type: { type: "localized_markdown", required: true } },
{ id: crypto.randomUUID(), key: "hero", type: { type: "media", required: false } },
{ id: crypto.randomUUID(), key: "published_on", type: { type: "date", required: true } },
],
blocks: [],
status: { type: "active" },
});
const post = await sdk.content.entry.create({
store_id: storeId,
id: crypto.randomUUID(),
collection_id: posts.id,
key: "first-post",
slugs: { en: "first-post", de: "erster-beitrag" },
blocks: [
{ id: crypto.randomUUID(), key: "title", type: "localized_text", value: { en: "First post", de: "Erster Beitrag" } },
{ id: crypto.randomUUID(), key: "body", type: "localized_markdown", value: { en: "## Hello", de: "## Hallo" } },
{ id: crypto.randomUUID(), key: "hero", type: "media", value: heroMediaId },
{ id: crypto.randomUUID(), key: "published_on", type: "date", value: "2026-10-07" },
],
status: { type: "active" },
});
- Each schema type carries only its own rules: text limits and patterns, number bounds, list and
object counts, and which collections an
entryfield may point at. - Localized text and markdown hold one text per store language; a required localized field needs text in every store language.
- Keys and slugs are unique per collection; slugs per language.
- Entries are created and edited only in an
activecollection. An entry can also bescheduledwith a futurepublish_at, when Arky publishes it.
Read Content In A Storefront
import { initialize, getBlockContentValue } from "arky-sdk/storefront";
import { createOrderToken } from "arky-sdk/utils";
const arky = initialize("arky_pk_...", {
locale: "de",
createVisitorIdentity: () => ({
customer_id: crypto.randomUUID(),
session_id: crypto.randomUUID(),
}),
createSessionId: () => crypto.randomUUID(),
createId: () => crypto.randomUUID(),
createOrderToken,
});
const page = await arky.content.entry.find({ collection_id: posts.id, limit: 20 });
const title = getBlockContentValue(page.items[0], "title", "de");
const {
items: [post],
} = await arky.content.entry.find({ collection_id: posts.id, slug: "erster-beitrag" });
getBlockContentValue turns a block into plain values for one language: an object becomes a
record of its fields, an array a list, and localized text the string in that language, or null
when the block has no text in it. Arky never falls back to another language.
Load What Blocks Point At
import { collectBlockReferences, getImageUrl } from "arky-sdk/storefront";
const references = collectBlockReferences(post?.blocks ?? []);
const { items: media } = await arky.media.find({ ids: references.media_ids });
const hero = media.find((item) => item.id === heroMediaId);
const url = getImageUrl(hero, "large");
const alt = hero?.alt?.de ?? "";
Responses return ids, never the records they point at. Read each kind once and index it locally;
media.find takes any number of ids and reads them 100 at a time.
content.load does this for one collection or entry: it reads the media its blocks point at, at
most 100 ids a read, and answers the blocks in the client’s locale, or in the locale of the call:
import { getImageUrl } from "arky-sdk/storefront";
const loaded = await arky.content.load({ entry: { collection_id: posts.id, slug: "erster-beitrag" } });
const heading = loaded.text("title");
const image = getImageUrl(loaded.media("hero"), "large");
text is null when the block has no value in that language, an accessor for a block of another
type throws, and a failed read throws. content.loadEntries({ collection_id }) reads every entry of
a collection the same way.
Update Safely
await sdk.content.entry.update({
store_id: storeId,
id: post.id,
expected_updated_at: post.updated_at,
blocks: nextBlocks,
});
The update carries the updated_at it read; someone else’s newer edit refuses it with 409.
A media file, a form or another entry can’t be deleted while a block points at it.
Use blocks for authored values. Use categories when a value is a shared classification that storefronts filter by.