Diil Docs
  1. Docs
  2. Getting started

How content is organised: pages, sections, blocks

Updated:

Before you write a single fetch, it pays to know how Diil thinks about a website. The good news: it thinks the way you already do. A site is a set of pages, a page is a stack of sections, and a section is a handful of blocks — a title here, an image there, a list of FAQ items at the bottom. Learn these three words and the whole API fits in your head.

The mental model: page → section → block

Everything the site owner edits in the CRM (or right on the live site in Live mode) ends up in one tree. Your site reads that tree through the read-only Content API and renders it however it likes — markup, styles and framework stay 100% yours.

  • Page — one page of your site: home, pricing, contacts. It has a name, a path (href), SEO params and its sections.
  • Section — a horizontal slice of the page: the hero, the feature grid, the FAQ. It groups blocks and has a show flag and a position (index).
  • Block — the smallest editable thing: a headline, an image, a price, a button link, a whole list of testimonials. Every block has a type that decides what its content looks like.

The blog lives next to this tree, not inside it: posts have their own endpoints, slugs and pagination. More on that in Blog.

A real page, taken apart

Let's look at the home page of VERTEX, a small shop that sells wireless earbuds. Visually it has a big hero with a headline and a product shot, a row of features, and an FAQ at the bottom. In Diil it looks like this:

The tree behind the VERTEX home pagetext
page  home
├── section  hero        index 0, show: true
│   ├── block  hero_title     text    "Earbuds that mute the city"
│   └── block  hero_image     image   hero.jpg
├── section  features    index 1, show: true
│   └── block  features_list  array   [ {…}, {…}, {…} ]
└── section  faq         index 2, show: true
    └── block  faq_section    object  { title, items: [ … ] }

And this is what GET /v1/pages/home returns for it (the features section is trimmed):

curl "https://back.sitecog.com/content/v1/pages/home?lang=en,de" \
  -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
200 OKjson
{
  "id": 26,
  "marker": "home",
  "name": "Home",
  "href": "/",
  "index": 0,
  "params": {
    "title": { "en": "VERTEX Air 3 — wireless earbuds", "de": "VERTEX Air 3 — kabellose Ohrhörer" }
  },
  "content": {
    "hero": {
      "id": 41,
      "marker": "hero",
      "name": "Hero",
      "index": 0,
      "show": true,
      "content": {
        "hero_title": {
          "id": 95,
          "marker": "hero_title",
          "name": "Hero title",
          "type": "text",
          "multilang": true,
          "updatedAt": "2026-09-20T16:33:23.000Z",
          "content": {
            "en": "Earbuds that mute the city",
            "de": "Ohrhörer, die die Stadt leiser machen"
          }
        },
        "hero_image": {
          "id": 96,
          "marker": "hero_image",
          "name": "Hero image",
          "type": "image",
          "multilang": false,
          "updatedAt": "2026-09-18T09:12:40.000Z",
          "content": { "file": "https://cdn.example.com/storage/your-site/hero.jpg" }
        }
      }
    },
    "features": {
      "id": 42,
      "marker": "features",
      "name": "Features",
      "index": 1,
      "show": true,
      "content": { "…": "…" }
    },
    "faq": {
      "id": 44,
      "marker": "faq",
      "name": "FAQ",
      "index": 2,
      "show": true,
      "content": {
        "faq_section": {
          "id": 102,
          "marker": "faq_section",
          "name": "FAQ",
          "type": "object",
          "multilang": false,
          "updatedAt": "2026-09-22T11:05:10.000Z",
          "content": {
            "title": { "en": "FAQ", "de": "Häufige Fragen" },
            "items": [
              {
                "question": { "en": "How long is delivery?", "de": "Wie lange dauert der Versand?" },
                "answer": { "en": "1–3 days.", "de": "1–3 Tage." }
              },
              {
                "question": { "en": "Do they work with iPhone?", "de": "Funktionieren sie mit dem iPhone?" },
                "answer": { "en": "Yes, and with Android too.", "de": "Ja, und auch mit Android." }
              }
            ]
          }
        }
      }
    }
  }
}

Read it top to bottom and the pattern is hard to miss:

  • The page's content is an object of sections keyed by marker.
  • Each section's content is an object of blocks keyed by marker.
  • Each block's content is the value itself, shaped by the block's type.

So the German version of the first FAQ question is page.content.faq.content.faq_section.content.items[0].question.de. Long? Yes. Surprising? Never.

Markers: names your code can rely on

Pages, sections and blocks are addressed by markers — short machine names that an editor or developer sets in the CRM. home, hero and hero_title above are all markers. They are what you put in URLs (/v1/pages/home) and what you see as object keys in responses.

The rules

  • Latin letters, digits and underscores only: ^[A-Za-z0-9_]{2,40}$.
  • From 2 to 40 characters long.
  • Case-sensitive: Hero and hero are two different markers.
  • Anything else in a URL gets 400 {"message":"invalid_marker"} before we even start looking.
  • Block markers are unique within a section, not across the whole site. Two sections can both have a title block.

Naming conventions that age well

  • snake_case, lowercase. hero_title, not HeroTitle or heroTitle2. Since case matters, one style everywhere saves you from “why is this undefined” at 2 a.m.
  • Prefix blocks with their section. hero_title, hero_image, faq_section. A bare title is fine inside a page response, but the moment you fetch it on its own through /v1/blocks it becomes ambiguous — without ?section the oldest match wins.
  • Name the meaning, not the look. promo_banner survives a redesign; red_box_left does not.
  • Don't rename markers casually. Your code depends on them. Renaming one in the CRM is the content version of renaming a database column in production.

Why markers and not ids?

Every object also has a numeric id, and you are welcome to look at it. But ids are handed out by the database, so they differ between sites and environments, and they tell the next person reading your code nothing at all. Markers are chosen by humans and read like documentation: page.content.hero explains itself, sections[41] does not. Write your templates against markers and treat ids as trivia.

The common page: header, footer and friends

Some content belongs to every page: the logo, the menu, the phone number in the header, the footer links. The usual convention is a service page with the marker common that holds those blocks. Its href is null — nobody opens it on its own — and you simply fetch it next to the current page:

Layout data: current page + commonjs
const headers = { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' };

const [page, common] = await Promise.all([
  fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', { headers }).then((r) => r.json()),
  fetch('https://back.sitecog.com/content/v1/pages/common?lang=en', { headers }).then((r) => r.json()),
]);

const footer = common.content.footer.content; // blocks shared by every page

Both responses are cached, so the extra request is close to free. It is a convention, not magic: if your team prefers layout or shared, the API won't mind.

Languages are maps, not copies

Diil does not keep “the English page” and “the German page” as two separate things. There is one page, and every translatable value is a language map:

{ "en": "Earbuds that mute the city", "de": "Ohrhörer, die die Stadt leiser machen" }
  • Without ?lang you get every active language of the site. With ?lang=en or ?lang=en,de — only those.
  • Keys always come in the order set in the CRM, whatever order you asked for.
  • There is no fallback on the server. A language with no translation is simply missing from a block's map, and an empty translation comes back as "". Choosing a fallback is up to you — a tiny helper is waiting on Languages & fallbacks.
  • The list of languages itself comes from /v1/langs — exactly what a language switcher needs.

Block types at a glance

A block's type tells you what to expect in its content. Here is the cheat sheet; every type with full examples lives on Block types.

TypeWhat content looks likeTypical use
text{ "en": "…", "de": "…" }Headlines, short copy
html{ "en": "<p>…</p>" } — values are HTML stringsFormatted text
image, video{ "file": "https://…" }, or { "multilang": true, "en": "…", "de": "…" } when each language has its own fileProduct shots, banners, promo videos
link{ "url": "…", "target": "_self" | "_blank", "title": { "en": "…" } }Buttons, menu items
number, color, booleanThe raw value or null: 149, "#3D3D5C", truePrices, brand colours, toggles
date, date_rangeThe value as entered in the CRM, or nullSale dates, events
objectFields keyed by marker; each field follows the rules aboveA card, an FAQ block with a title
arrayA JSON array of items shaped like objects, in CRM orderFeature lists, testimonials, FAQ items

Hidden sections and the show flag

Editors can switch a section off in the CRM — say, hide the FAQ while it is being rewritten. That sets show: false, but the section is still returned, blocks and all. What to do with it is your template's call:

{page.content.faq?.show && <Faq data={page.content.faq.content} />}

Forget the check and a hidden section will happily show up on the live site. The API reports; your code decides.

Ordering with index

Pages and sections carry an index — their position in the CRM, starting at 0. Response objects usually arrive in that order already, but sorting by index is the honest way to rebuild what editors see, especially if you render sections dynamically:

const sections = Object.values(page.content)
  .filter((section) => section.show)
  .sort((a, b) => a.index - b.index);

sections.forEach((section) => render(section.marker, section.content));

Items inside an array block have no index — the array order is the CRM order. Blog posts follow the sort order chosen in the blog settings in the CRM.

Which endpoint should I use?

Short version: fetch the whole page unless you have a reason not to. Long version:

You need…UseWhy
Everything to render a pageGET /v1/pages/:markerOne request, every section and block. The default choice.
A menu or a sitemapGET /v1/pagesEvery page with href and SEO params, without content.
One section — say, a promo strip reused on several pagesGET /v1/sections/:markerA smaller response when the rest of the page comes from elsewhere.
One value — a phone number, a banner, a priceGET /v1/blocks/:marker?section=…Exactly one block. Pass section to be precise.
A list of blog posts or a single postGET /v1/blog, /v1/blog/:slugPosts live outside the page tree and come with pagination.
The site's languagesGET /v1/langsLanguage switchers, hreflang tags.

Where to next