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
contentpageblueprint ("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.
- For a normal page, you can use the
- Brekz Website relies on the URL key-value store to figure out if the page exists.
- 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 fetching the entry data from Brekz CMS and rendering the sections immediately on the Website (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 theX-DCL-FAIL(stands for: Dynamic Content Load failure) response header for the failure code.- For more information, see Dynamic content load failure codes.
- If it exists, it will check if sections for this page exist.
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¶
Static components are rendered by Brekz Website using Blade templates. Adding a new component requires two steps: creating the template and registering it.
Step 1 — Create the Blade template
Create a new Blade file inside the appropriate subdirectory of app/Services/PrerenderedSectionsLoader/Views/Static/Components/Custom/ in Brekz Website.
Why the Custom directory?
All the components we (Flooris) made should be stored there.
The only non-custom components we have, are basic components that are default Statamic components. Think about thinks like headings, paragraphs, images, etc...
The subdirectories are organized by category, for example:
| Directory | Purpose |
|---|---|
Breadcrumbs/ |
Breadcrumb navigation components |
Category/ |
Category page specific components |
Grids/ |
Grid and layout components |
Marketing/ |
General marketing/content components |
Pdp/ |
Product detail page specific components |
Technical/ |
Non-visual technical components (meta tags, GTM events, Vue component loader, ...) |
Your template receives the component's configuration via the $attrs variable. Use @props(['attrs' => []]) at the top of your template.
Example Blade template
@props(['attrs' => []])
@php
// We assign it to variables in this way so we can also set a default value in case that attribute isn't set
// It's important to do this, as otherwise we might run into runtime errors about missing variables
$title = $attrs['title'] ?? '';
$body = $attrs['body'] ?? '';
@endphp
@if(!empty($title))
<section class="py-8 px-4">
<h2 class="text-2xl font-bold">{{ $title }}</h2>
@if(!empty($body))
<p class="mt-2">{{ $body }}</p>
@endif
</section>
@endif
Step 2 — Register the component
Add your component to the $STATIC_CONTENT_TEMPLATES array in PrerenderedSectionsLoader.php, so the render system is aware of your component.
The key is the component type name (as used in the blueprint or CMS), and the value is the path to the Blade file relative to the PrerenderedSectionsLoader class.
To make it easier to find components, we recommend grouping them by what sort of component it is. For example if you have a marketing related component, store it in the marketing directory and register it in the same "block" of code as the other marketing components.
private static array $STATIC_CONTENT_TEMPLATES = [
// ...
// Custom components (marketing)
'my_new_component' => '/Views/Static/Components/Custom/Marketing/my_new_component.blade.php',
// ...
];
Step 3 (optional) — Register the component in the Statamic editor
If you also want content editors to be able to use the component from the Statamic Control Panel (rather than only via section mapping in a blueprint), add a set definition for it in the resources/fieldsets/sections/static.yaml fieldset in Brekz CMS.
Info
This step only affects the Statamic editor UI. The Website rendering is driven entirely by the $STATIC_CONTENT_TEMPLATES registration — not by anything in CMS.
Never edit static.yaml through the Statamic Control Panel
The static.yaml fieldset uses YAML anchors to share component definitions across multiple places (for example, making every component available inside column_grid without having to list them all manually).
The Statamic Control Panel does not understand YAML anchors and will silently expand or strip them when it saves the file. This permanently destroys the anchor structure and makes the fieldset much harder to maintain going forward.
Always edit static.yaml directly in a code editor (e.g. via the repository), never through the Statamic UI.
What are YAML anchors?¶
YAML anchors let you define a block of content once and reuse it elsewhere in the same file, without copy-pasting. In static.yaml this is used to automatically make every component available inside container components like column_grid.
# Define the anchor — every set listed here is "recorded"
sets: &bard_recursive_safe_sets
my_new_component:
display: My new component
fields:
- handle: title
field:
type: text
# Reuse the anchor — column_grid automatically gets all the same sets
column_grid:
display: Column grid
fields:
- handle: columns
field:
type: replicator
sets:
<<: *bard_recursive_safe_sets # expands to all sets defined above
Tip
If you aren't familiar with YAML anchors, this article by Atlassian explains them well.
Infinite recursion loop
Certain components are not safe to include inside the recursive anchor because Statamic's Antlers template engine can get confused resolving variables and jump to the root of the entry, causing an infinite render loop.
If your component is susceptible to this, define it outside the &bard_recursive_safe_sets anchor so it is not pulled into recursive containers like column_grid.
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/Views/Dynamic directory in Brekz Website.
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/Views/Dynamic directory in Brekz Website.
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\Views\Dynamic;
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\Views\Dynamic\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.
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.
Section mapping to rendered page visual¶
Here is a visual how the section mapping on your blueprint can grab values from your Statamic entry, convert it into resolved sections which can then be used to render the page.
The section mapping inside the blueprint will be reused for every entry that uses that blueprint. This allows you to apply the same layout to an entire collection, without having to update every single entry of that collection by hand every time you change something.
Re-rendering the collection
If you changed something in the section mapping, don't forget to re-render every page that uses it.
You can do this in bulk using the php artisan prerendered-sections:render-collection command.
Otherwise you won't see your change, unless you have force page rendering enabled. (Development environment)
Special value prefixes¶
Inside a section mapping you can use special prefixes to resolve values dynamically at render time:
| Prefix | Example | Description |
|---|---|---|
field: |
field:description |
Reads the value of a field from the CMS entry |
trans: |
trans:some.translation.key |
Resolves a Laravel translation key |
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 sections, 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 separately, 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
Artisan commands¶
Brekz Website provides Artisan commands to pre-render sections directly, without waiting for a user request to trigger on-demand rendering or forcing a page to be re-rendered.
prerendered-sections:render¶
Renders and stores the sections for a single page.
php artisan prerendered-sections:render {site} {pageType} {slug}
| Argument | Description |
|---|---|
site |
The Statamic site handle (e.g. nl) |
pageType |
The page type value from PageTypeEnum (e.g. pages, pdp, plp, meta, category_pages) |
slug |
The URL slug of the page |
Example command
php artisan prerendered-sections:render nl pages homepage
prerendered-sections:render-collection¶
Renders and stores the sections for all entries in a given page type's collection.
php artisan prerendered-sections:render-collection {site} {pageType} [--continue-on-error]
| Argument / Option | Description |
|---|---|
site |
The Statamic site handle (e.g. nl) |
pageType |
The page type value from PageTypeEnum |
--continue-on-error |
Skip failed entries and continue, instead of aborting on the first error |
Example command
php artisan prerendered-sections:render-collection nl pdp --continue-on-error
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"| RENDER_ON_DEMAND_SECTIONS
RENDER_ON_DEMAND_SECTIONS("Fetch entry data from CMS and render sections on Website")
RENDER_ON_DEMAND_SECTIONS --> CHOICE_DID_WE_RECEIVE_PAGE_SECTIONS
CHOICE_DID_WE_RECEIVE_PAGE_SECTIONS{"Were the sections successfully rendered?"}
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 rendering)
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 entry data and blueprint for slug
BrekzCMS-->>-BrekzWebsite: Entry data and blueprint
loop For each section
BrekzWebsite->>BrekzWebsite: Render section using Blade templates
BrekzWebsite->>Filesystem: Write rendered section (HTML/JSON)
end
BrekzWebsite-->>-Client: Return redirect to same page (retry)