Skip to content

Sections system¶

The sections system is part of the foundation of the Brekz Website project. It's used a lot throughout the project and thus important to fully understand how it works, which is what this document is about.


TL;DR¶

Since this system can be a bit complex to understand, here are some important takeaways.

Tip

It's still highly recommended to read this entire page at least once to get a better understanding of the system and why it is the way it is.

  • The sections system allows you to partially cache (pre-render) sections of a page, without losing support for dynamic content.
  • A page is divided in 1 or more sections.
    • You have static and dynamic sections.
    • Static sections allow you to render the section beforehand, reducing the request time and server load.
    • Dynamic sections allow you to execute PHP and render dynamic results.
    • Properly dividing the page in sections allows you to have the fastest possible page time, while still allowing for dynamic content.
  • New pages can be made inside Brekz CMS.
    • For a normal page, you can use the contentpage blueprint ("Pagina - Contentpagina" in the Statamic CP).
    • If you want to make multiple pages with the same layout, it's recommended to create a new collection and use section mapping instead.
      • Section mapping is a way to create the sections programmatically inside the blueprint.
      • You can think of this approach like defining a page template, this template will then automatically be applied to each page inside the collection.
  • Brekz Website relies on the URL key-value store to figure out if the page exist.
    • If it exists, it will check if sections for this page exist.
      • If the sections are available, it will use those to assemble the page.
      • If the sections aren't available, it will attempt to "rescue" the request by automatically asking Brekz CMS to render the sections immediately (Also known as on-demand sections).
        • If this succeeds, the user's request should complete normally, just slower than usual.
        • If this fails, the request will fail with a 404 error code.
    • If something fails with the section system during a request (ContentController.php), you can check the X-DCL-FAIL (stands for: Dynamic Content Load failure) response header for the failure code.

What is the sections system?¶

The sections system is responsible for building the webpage the user sees, but in a faster way. The sections system allows us to cache (also called pre-rendering) specific parts of the page, without having to rely on full page caching solutions like Cloudflare for speed.


Why the sections system?¶

It allows us to have faster response times, without having the limitation of not being able to have dynamic content provided by the server.

It's of course possible to work around the dynamic content issue with full page caching by relying on API calls, but this will require the client to wait for another request before parts of the page are ready. With the sections system, the dynamic content is already processed inside the initial page request, making it immediately available for the client.


What type of sections are there and when do you use them?¶

There are 2 section types, static and dynamic.

It's possible to use each section type multiple times on a page, allowing you to put dynamic content between static content if needed.

By properly using these section types, you can achieve the best page performance, without losing the support for dynamic content or having to do an API request as soon as the page loads.

Static 📄¶

The static section type means the content doesn't have any dynamic parts and can be safely pre-rendered to save time during the request.

Dynamic Javascript content

Please note that when we talk about dynamic parts here, we mean dynamic parts handled by the server, not the client.

It's still possible to have some dynamic content inside a static section, for example a VueJS component that calls an API during runtime.

Example use case

You can use the static section type for the PDP to show the product name, images and description, as those will be the same for each user.

This allows you to fetch the data and render it beforehand, removing the requirement to do this for each request to this page, reducing the request time and server load.

Creating a custom static component¶

You can register new custom static components by adding them to the resources/fieldsets/sections/static.yaml fieldset in Brekz CMS.

Specifically around the <<: &bard_recursive_safe_sets YAML anchor. This will cause your component to become available in the Statamic editor and within the sections system.

What are YAML anchors?

YAML anchors allows us to reuse certain sections of this fieldset, for example adding every single component inside the column_grid, without having to manually copy and paste all of them.

If you aren't familiar with them, I highly recommend reading this article by Atlassian that explains it pretty well.

Infinite recursion loop

Certain components are not recursive safe, this is because Antlers sometimes gets confused while trying to resolve a variable and might suddenly jump to the root of the entry.

This can cause an infinite render loop. Which is why for example the column_grid component is stored outside the YAML anchor.

If your custom component is susceptible to this issue, please define it outside of the <<: &bard_recursive_safe_sets YAML anchor.

Example static component

TODO: Write an example and document this

Dynamic ⚡¶

The dynamic section type means the content has dynamic parts and thus can't be rendered beforehand, it will need to be rendered again for each request.

Dynamic sections have the ability to execute PHP code and render content (returned as a string containing HTML code).

A component inside a dynamic section is called dynamic content, which is handled by a dynamic content handler. These are stored inside the app/Services/PrerenderedSectionsLoader/DynamicContent directory.

Example use case

You can use the dynamic section type on the PDP for providing an accurate delivery estimate, without having to deal with the client's time zone or duplicating this logic for the back-end and front-end.

Since it's executed for each request, it should always be up-to-date with the latest estimation.

Creating dynamic content handlers¶

To add a new dynamic content handler, create a new PHP class inside the app/Services/PrerenderedSectionsLoader/DynamicContent directory.

This class needs to implement the IDynamicContentHandler interface, this will tell you what you have to implement. The interface also includes some PHPDoc, which can help you understand what each function should do and return.

The definition of the dynamic content inside the section is available via $this->content. This allows you to pass configurations or other data to your dynamic content handler.

When your dynamic content handler has been made, you will have to register your handler in PrerenderedSectionsLoader. This is done by simply adding your class to the $DYNAMIC_CONTENT_HANDLERS array as a ::class.

It should now automatically call your handler's render function when your matches function returns true.

Example content handler

In this example we will create a CurrentTimeHandler that is triggered by the current_time content type.

You are able to configure the output format by defining format inside the attrs section of your content.

<?php

namespace App\Services\PrerenderedSectionsLoader\DynamicContent;

use Carbon\Carbon;

class CurrentTimeHandler implements IDynamicContentHandler
{
    private array $content;

    public static function matches(string $contentType): bool
    {
        return $contentType === 'current_time';
    }

    public function setContent(array $content): void
    {
        $this->content = $content;
    }

    public function getContent(): array
    {
        return $this->content;
    }

    public function render(): string
    {
        $formattedTime = Carbon::now()->format($this->content['attrs']['format']);

        return "<section class=\"flex w-full my-4\"><div class=\"px-4 py-2 bg-red-100 mx-auto rounded-md\"><p>PHP told me the time is currently <strong>{$formattedTime}</strong>.</p></div></section>";
    }
}

Then register our dynamic content handler in PrerenderedSectionsLoader by adding it to the $DYNAMIC_CONTENT_HANDLERS array.

<?php

namespace App\Services\PrerenderedSectionsLoader;

class PrerenderedSectionsLoader
{
    private static array $DYNAMIC_CONTENT_HANDLERS = [
        \App\Services\PrerenderedSectionsLoader\DynamicContent\CurrentTimeHandler::class,
        // ...
    ];

    // ...
}

We are now able to use this type on our pages.

---
title: Example page
blueprint: contentpage
sections:
  -
    enabled: true
    type: dynamic
    content:
      -
        type: "current_time",
        attrs:
          format: "Y-m-d H:i:s"

If you also want to make it accessible in the Statamic control panel, simply create a fieldset definition for your component in the resources/fieldsets/sections/dynamic.yaml fieldset in Brekz CMS.

main:
  display: Main
  sets:
    # ...
    current_time:
      display: "Current time (EXAMPLE)"
      icon: time-clock
      fields:
        - handle: format
          field:
            type: text
            display: 'Date format'
            instructions: 'Define the format as a PHP date format, see https://www.php.net/manual/en/function.date.php#example-3'
            placeholder: 'Y-m-d H:i:s'
            localizable: false
            validate:
              - required
    # ...

How does it work when looking at it from a page perspective?¶

In the following example, you see a page that has static content, but with some dynamic content in between. This allows you to better see the advantage of the sections system.

Brekz Search products Static section Dynamic section Static section

Note that if you don't need any dynamic content on that page, it's perfectly acceptable to have only 1 static section. You can use as many (or little) sections as you want.


How can I create a page using the sections system?¶

New pages can easily be created and managed inside Brekz CMS. Here you can use Statamic's bard component to use almost any component that is available in Brekz to build new pages.

You should always register new pages in CMS... right?

While you should always prefer creating new pages in CMS, sometimes it really doesn't make sense as the page is just too technical (Example: My account) or you are sure it will never be changed by Brekz.

In this case you can simply use the normal Laravel way to create a new route and view. Just make sure to register this route before the catch all at the bottom of the routes file, otherwise the request will never reach your custom route.

Please note that certain tools might not be available in manually created routes, like the CMS integration and sections system.

Regular page (CMS)¶

If you just want to create a regular page, simply create a new entry in the "Pages" collection in CMS using the contentpage blueprint. In the Statamic Control Panel it's shown as "Pagina - Contentpagina".

It will ask you to input a title for the page, and then you see a replicator for the sections. When you add a new section, it will ask you what section type you want, static or dynamic.

Inside each section here will be a large bard input field. In here you can simply start typing and using formatting as you would expect from a WYSIWYG editor, all the options shown at the top should be available.

But unlike a regular WYSIWYG editor, you can also add more complex components by clicking on the "+" symbol at the left. This allows you to insert a component that is made available by us.

For example, you can insert an image slider and select the images you want to add. When you then visit the page, it will look and feel exactly the same as other images sliders on the website.

Pages with reoccurring layout (Section mapping)¶

If you have a set of pages that all need to have the same layout, you can create a new Statamic collection and use the section mapping approach instead.

With the section mapping approach, you only have to define the sections once in the blueprint, and it will automatically apply this to all the pages inside the collection. Think of it like defining a template for the page itself, it then grabs information from the entry and fill it in on the page using your template.

This approach has been done for the PDP's and PLP's, since these should all have the same layout, but just with different content inside of them.

Example blueprints¶

Basic section mapping example

In this example we will just create a simple page with some text. In this case we have 3 section, all of them are static for simplicity.

A section can have multiple components to render, in this case the second section has 2 text fields it should render in that section. Each section will be cached seperately, so in this case it will cache 3 items. But since they are all static, you could also just combine all of them into 1 larger section.

tabs:
  # We don't use any tabs or fields in this blueprint in this example, so just ignore this section ;)
sections_mapping:
  -
    type: static
    content:
      -
        mapped_type: 'text'
        text: 'This is some example text in the first section'
  -
    type: static
    content:
      -
        mapped_type: 'text'
        text: 'How about some more text?'
      -
        mapped_type: 'text'
        text: 'The twist here is that it is inside another section 👀'
      -
  -
    type: static
    content:
      -
        mapped_type: 'text'
        text: 'Use as many as you need!'
      -
        mapped_type: 'paragraph'
        content:
          -
            type: text
            text: 'You can also put some'
          -
            type: text
            marks:
              -
                type: bold
            text: 'oompf'
          -
            type: text
            text: 'in your text with marks, the same way as you would in Statamic's'
          -
            type: text
            marks:
              -
                type: link
                attrs:
                  href: 'https://prosemirror.net/'
                  rel: 'noopener noreferrer'
                  target: _blank
                  title: 'Prosemirror homepage'
              -
                type: underline
            text: 'Prosemirror'
          -
            type: text
            text: 'editor.'
Using data from the entry

In this example we will be creating a blueprint that stores name as text and description as a bard (similar to a WYSIWYG editor). The values of these fields will then be used in the sections mapping to automatically output it to the page.

The expected output on the page should be 2 headings, 1 heading with "Product name" and one with "Product description". Below these headings should be the value that you entered in the entry.

The way the sections system detects it should grab a value from the entry, is if the value is prefixed with field:. If it sees that value, it will grab the value after the : and use that as the key to search for inside the entry.

So for example field:description will cause the section system to search for the value of description inside the entry, and then inserts the value of description in your section.

tabs:
  main:
    display: Main
    sections:
      -
        fields:
          -
            handle: name
            field:
              type: text
              required: true
              localizable: false
              display: Name
              visibility: read_only
          -
            handle: description
            field:
              remove_empty_nodes: false
              type: bard
              display: Description
              localizable: false
sections_mapping:
  -
    type: static
    content:
      -
        mapped_type: heading
        attrs:
          level: 2
        content:
          -
            type: text
            text: 'Product name'
      -
        mapped_type: text
        text: 'field:name' # This will become the value of field "name"
      -
        mapped_type: heading
        attrs:
          level: 2
        content:
          -
            type: text
            text: 'Product description'
      -
        mapped_type: set
        is-prose: true              # If this is set to true, it will apply some default styling to make CMS content look better, should be disabled for non-CMS content
        values: 'field:description' # This will become the value of field "description"
Calling a Vue component

One of the more "advanced" components that is available, is our custom vue_component component. This allows you to tell to the sections system that you want to load a specific Vue component here, in this example we want to load the SearchWrapperCategory Vue component.

You can also provide props if your component needs them, and just like before, you can prefix them with field: if you want to grab a specific value from the entry itself.

If you want your value to be passed as a dynamic prop, just add : to the key of the prop, the same as you would also do in VueJS.

tabs:
  main:
    display: Main
    sections:
      -
        fields:
          -
            handle: search_category_id
            field:
              type: text
              localizable: false
              display: 'Category ID (search)'
              instructions: 'Category ID for filtering products'
sections_mapping:
  -
    type: static
    content:
      -
        mapped_type: set
        values:
          - type: vue_component
            component: SearchWrapperCategory
            props:
              "lang-iso": 'nl-NL' # A hardcoded static prop
              ":category-filters": '[]' # A hardcoded dynamic prop
              ":category-id": 'field:search_category_id' # A dynamic prop filled by our entry

Diagrams¶

To explain the more technical side of this system, a couple of diagrams have been provided that can assist in understanding the system.

Flowchart¶

In this flowchart you can see how a page render is executed.

Show diagram
---
config:
    theme: redux
    flowchart:
        curve: bumpY
---
flowchart TD
    INCOMING_REQUEST(("Incoming request"))


    INCOMING_REQUEST --> CHOICE_SLUG_IN_KEY_VALUE_STORE

    CHOICE_SLUG_IN_KEY_VALUE_STORE{"Is the URL slug in the key-value store?"}
    CHOICE_SLUG_IN_KEY_VALUE_STORE -->|"Yes"| CHOICE_DO_WE_UNDERSTAND_THE_PAGE_TYPE
    CHOICE_SLUG_IN_KEY_VALUE_STORE -->|"No"| ABORT_REQUEST

    CHOICE_DO_WE_UNDERSTAND_THE_PAGE_TYPE{"Do we understand this page type?"}
    CHOICE_DO_WE_UNDERSTAND_THE_PAGE_TYPE -->|"Yes"| CHOICE_DO_WE_HAVE_THE_PAGE_SECTIONS
    CHOICE_DO_WE_UNDERSTAND_THE_PAGE_TYPE -->|"No"| ABORT_REQUEST

    CHOICE_DO_WE_HAVE_THE_PAGE_SECTIONS{"Do we have the sections for this page?"}
    CHOICE_DO_WE_HAVE_THE_PAGE_SECTIONS -->|"Yes"| LOAD_AND_RENDER_SECTIONS
    CHOICE_DO_WE_HAVE_THE_PAGE_SECTIONS -->|"No"| CHOICE_DID_WE_ALREADY_REQUEST_SECTIONS_IN_PREVIOUS_ATTEMPT

    CHOICE_DID_WE_ALREADY_REQUEST_SECTIONS_IN_PREVIOUS_ATTEMPT{"Did we already request on-demand sections in the previous attempt?"}
    CHOICE_DID_WE_ALREADY_REQUEST_SECTIONS_IN_PREVIOUS_ATTEMPT -->|"Yes"| ABORT_REQUEST
    CHOICE_DID_WE_ALREADY_REQUEST_SECTIONS_IN_PREVIOUS_ATTEMPT -->|"No"| REQUEST_ON_DEMAND_SECTIONS_FROM_CMS

    REQUEST_ON_DEMAND_SECTIONS_FROM_CMS("Request on-demand sections from CMS")
    REQUEST_ON_DEMAND_SECTIONS_FROM_CMS --> CHOICE_DID_WE_RECEIVE_PAGE_SECTIONS

    CHOICE_DID_WE_RECEIVE_PAGE_SECTIONS{"Did we receive the page sections?"}
    CHOICE_DID_WE_RECEIVE_PAGE_SECTIONS -->|"Yes"| RETRY_REQUEST
    CHOICE_DID_WE_RECEIVE_PAGE_SECTIONS -->|"No"| ABORT_REQUEST

    LOAD_AND_RENDER_SECTIONS("Load pre-rendered sections and execute dynamic sections")
    LOAD_AND_RENDER_SECTIONS ---> FINISH_REQUEST


    subgraph FLOW_CHART_END_ALIGNMENT[" "]
        direction LR
        style FLOW_CHART_END_ALIGNMENT fill:transparent,stroke:transparent

        FINISH_REQUEST((("Return completed page")))
        RETRY_REQUEST((("Retry the request")))
        ABORT_REQUEST((("Abort request")))
    end

Sequence diagram¶

In these diagrams you can see a couple of situations and how they interact with other services.

Show diagram (Unsuccessful flow, URL slug not in key-value store)
sequenceDiagram
    participant Client as Client
    participant BrekzWebsite as Brekz Website
    participant BrekzCMS as Brekz CMS
    participant Filesystem as Filesystem

    Client->>+BrekzWebsite: Page request

    BrekzWebsite->>+Filesystem: Read key-value store
    Filesystem-->>-BrekzWebsite: Key-value store

    BrekzWebsite-->>-Client: Return 404 page
Show diagram (Unsuccessful flow, Unknown page type)
sequenceDiagram
    participant Client as Client
    participant BrekzWebsite as Brekz Website
    participant BrekzCMS as Brekz CMS
    participant Filesystem as Filesystem

    Client->>+BrekzWebsite: Page request

    BrekzWebsite->>+Filesystem: Read key-value store
    Filesystem-->>-BrekzWebsite: Key-value store

    BrekzWebsite-->>-Client: Return 404 page
Show diagram (Successful flow, sections already available)
sequenceDiagram
    participant Client as Client
    participant BrekzWebsite as Brekz Website
    participant BrekzCMS as Brekz CMS
    participant Filesystem as Filesystem

    Client->>+BrekzWebsite: Page request

    BrekzWebsite->>+Filesystem: Read key-value store
    Filesystem-->>-BrekzWebsite: Key-value store

    BrekzWebsite->>+Filesystem: Glob JSON/HTML sections directory for slug
    Filesystem-->>-BrekzWebsite: List of JSON/HTML file paths

    loop For each section
        BrekzWebsite->>+Filesystem: Read JSON/HTML file contents
        Filesystem-->>-BrekzWebsite: JSON/HTML file contents
        BrekzWebsite->>BrekzWebsite: Execute / append section
    end

    BrekzWebsite->>BrekzWebsite: Insert sections in page template

    BrekzWebsite-->>-Client: Return complete page
Show diagram (Successful flow, sections not already available, required on-demand sections)
sequenceDiagram
    participant Client as Client
    participant BrekzWebsite as Brekz Website
    participant BrekzCMS as Brekz CMS
    participant Filesystem as Filesystem

    Client->>+BrekzWebsite: Page request

    BrekzWebsite->>+Filesystem: Read key-value store
    Filesystem-->>-BrekzWebsite: Key-value store

    BrekzWebsite->>+Filesystem: Glob JSON/HTML sections directory for slug
    Filesystem-->>-BrekzWebsite: List of JSON/HTML file paths (Empty)

    BrekzWebsite->>+BrekzCMS: Request on-sections for slug
    BrekzCMS->>BrekzCMS: Fetch entry information

    loop For each section
        BrekzCMS->>BrekzCMS: Render section
        BrekzCMS->>Filesystem: Write section
    end

    BrekzCMS->>-BrekzWebsite: Success message

    BrekzWebsite-->>-Client: Return redirect to same page (retry)