Browse the docs ▾

Tutorials

Build a CMS-driven site

Model the content once and design one page. Every item then gets its own address, and lists anywhere on the site keep themselves up to date.

The brief: "We run walking tours. Each tour has a price, a duration, a day-by-day itinerary and a region. I want a page for each tour and a page listing them all."
You'll end with /tours listing every tour, /tours/<slug> for each one, a home-page row of the latest tours, and "similar tours" under each.
You need the content:schema permission and an Owner or Admin role for fields. Custom types need a plan that includes them (Pro or Scale), so check before you design around them. Simple text, link and image fields don't use custom types.
The order matters here, because the design can only bind to fields that already exist. Do these steps in order, each in its own call.

1. Create the content type

Send add_item_page {name: "Tours", url: "/tours"} through apply_site_ops. This creates a page design shared by every tour. Call list_content_types next, to get the new content_type_id.

2. Model the structured parts

An itinerary day has its own fields, so it's a custom type. Use preview_custom_type and save_custom_type to make Itinerary day with a title, a description and a photo. A field whose type is another custom type's id is always a list of it, so a second type can hold the list of days.

3. Add fields to the content type

Use preview_content_fields and save_content_fields. Show the owner the change, then save. A field of type string can also sort lists, and numbers and dates in it compare properly, so you can list tours by price or date. Type text can't sort.

4. Write the items

Use create_item with title, slug, summary, cover_url, tags and fields, and an idempotency_key on each. A custom-type value is an object of that type's field keys. For more than a few items, use import_items and a file. Publish them after the owner agrees, since lists and screenshots show only published items.

5. Design the page for each tour

Read it with get_page_outline. It starts with the title, cover and body. Add elements, then connect them to data with bind_field:

Built-in

content.title, content.body, content.cover_img_url.

A field

store.price, with a format if you need one: store.price|currency.

A value inside a custom type

store.itinerary.days.
Repeat a card for each day with add_element kind: "list", source: "store.itinerary.days" and list_of: "cdt_<type_id>", then bind the card's elements to listData.<key>.

6. Build the index page

The items use /tours/<slug>, so /tours itself is free for a page. Add it with add_page, then set_data_source {page, name: "tours", content_type_id, size: 24} to fetch items, and add_element kind: "list" with source: "tours.list". Bind the cards to listData.title, listData.summary, listData.cover_img_url and listData.<field key>. Each card already opens its item, so don't put a link inside it.

7. Add lists elsewhere

Latest on the home page

Its own data source on that page, size: 3. A source name is used once across the site.

One region

A source with tags: ["Kerala"].

Similar tours

On the tour page, a source with same_tag_as_item: true and exclude_current_item: true.

More than one page of items

Next and Previous buttons, using set_handlers with api.fetch.target.

A page for each tag

The add_tag_page op, then preview_tag_pages. See the blog tutorial.

8. Check and publish

get_page_screenshot with the page's key shows the newest published item, or the one you give as item_id. Then follow Publish safely.
Adding a tour later needs no design work. Create the item, publish it, and it appears in every list. That's what makes this worth setting up, and the owner can do it in the dashboard too.

1. The content type

{ "op": "add_item_page",
  "name": "Tours",
  "url": "/tours" }

4. An item

{
  "name": "create_item",
  "arguments": {
    "org_id": 1042,
    "content_type_id": 311,
    "title": "Old Town at Dawn",
    "slug": "old-town-at-dawn",
    "summary": "Three quiet hours.",
    "tags": ["Kerala"],
    "fields": {
      "price": "1500",
      "itinerary": { "days": [
        { "title": "Start at the ghat",
          "description": "Meet at 6." }
      ] }
    },
    "idempotency_key": "old-town-dawn"
  }
}

6. The index

{ "op": "set_data_source",
  "page": "all_tours",
  "name": "tours",
  "content_type_id": 311,
  "size": 24,
  "sort": "price:asc" }

{ "op": "add_element",
  "kind": "list",
  "to_ref": "all_tours/tours-section",
  "position": "last",
  "source": "tours.list" }