Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 01 · The built-in content types

Blogs and posts

A blog is a container with a listing page; its posts carry their content on revisions, like pages. Where a post's template comes from, the routes and variables of the listings, tags and categories, comments, and the pattern for one post template per kind or per brand.

Who this is for. The theme developer writing a blog's listing and post templates, and the content modeller deciding whether dated content belongs in a blog. Afterwards you can render a post and its listings in both engines and give different kinds of post their own template without branching.

The blog and its posts

A blog is a container: a name, a slug, a text, an image, its own areas, a layout for its listing page, a default layout for its posts, and its comment and RSS settings. It is the one content record on the platform that is not versioned: its layout areas sit on the blog itself, and $blog->tagline reads the blog directly.

A post is versioned like a page: its name, text, areas and layout are on the revision, and the active revision is what visitors see. The getters on the post read through the active revision, so a post template reads a post the way a page template reads a page. A post also has a published date, a featured flag, authors, tags, categories and comments.

Template code blogs/posts/show.html
<article>
  <h1>{$post->getName()|escape}</h1>
  <p class="meta">{$post->getPublished()|date_format:'%e %B %Y'}</p>
  {$post->body}
  {foreach $post->getTags() as $tag}
    <a href="{$post->getBlog()->getTagPostsUrl($tag)}">{$tag->getName()|escape}</a>
  {/foreach}
</article>
{$previous = $post->getPrevious()}
{$next = $post->getNext()}
{if $previous}<a rel="prev" href="{$previous->getFullPath()}">{$previous->getName()|escape}</a>{/if}
{if $next}<a rel="next" href="{$next->getFullPath()}">{$next->getName()|escape}</a>{/if}
<article>
  <h1>{{ post.getName()|e }}</h1>
  <p class="meta">{{ post.getPublished()|date('j F Y') }}</p>
  {{ post.body }}
  {% for tag in post.getTags() %}
    <a href="{{ post.getBlog().getTagPostsUrl(tag) }}">{{ tag.getName()|e }}</a>
  {% endfor %}
</article>
{% set previous = post.getPrevious() %}
{% set next = post.getNext() %}
{% if previous %}<a rel="prev" href="{{ previous.getFullPath() }}">{{ previous.getName()|e }}</a>{% endif %}
{% if next %}<a rel="next" href="{{ next.getFullPath() }}">{{ next.getName()|e }}</a>{% endif %}

getPrevious() and getNext() follow the order of the blog's listing, and the first call loads every post of the blog to find the neighbours: fine for a blog of hundreds of posts, worth replacing with a bounded query for one of tens of thousands.

Where a post's template comes from

A post renders the template of the first layout it finds in this order:

  1. the layout on its active revision, chosen by the author;
  2. the blog's Post's layout, the default for its posts;
  3. the layout whose template is blogs/posts/show.html.

The blog's listing page renders the template of the blog's own layout, else blogs/posts/index.html. The framework ships neither template; it ships only the RSS feed's.

The chain is what makes a template per kind of post cheap. A site with news, events and job listings runs one blog per kind, gives each blog its own post layout, and never branches in a template: every event renders the events template because its blog says so. A multi-brand site does the same per site, since a blog belongs to one site. When the kinds share one blog, the author picks the layout per post instead.

Routes and variables

The Blogs setting Operation mode is Multi blog mode by default. In multi mode every path carries the blog's slug; in Single blog mode it is left out, and the site should have one blog.

Path (multi mode)ShowsVariables
/blogs/<blog>, /blogs/<blog>/2The blog's posts, a page at a timeposts, blog, record, _pagination
/blogs/<blog>/tags/<tags>Posts with the tagsAs above, plus tags, and tag for a single tag
/blogs/<blog>/categories/<categories>Posts in the categoriesAs above, plus categories, and category for a single one
/blogs/<blog>/authors/<author>Posts by an authorAs the listing
/blogs/<blog>/rss-feedThe RSS feed, when the blog's Allow RSS is on
/blogs/<blog>/<post>One postpost, record

A listing shows ten posts a page, newest created first; both are settings of the listing (Posts per page, Order by). Tags and categories combine in the URL: summer+sale (or summer&sale) requires both, summer,sale (or summer|sale) either; mixing the two is a 400. A tag, category or author listing with no posts answers 404 rather than an empty page. blogs is the module's default URL prefix and a site can change it; a blog can also have a custom URL, which is how /blog can serve a blog whose slug is news. This site's How-to library is a blog with the custom URL /how-to.

Tags, categories and featured posts

Posts take tags and categories by default (the blog itself does not, unless switched on). The blog builds the listing URLs for you, getTagPostsUrl(tag), getCategoryPostsUrl(category) and getAuthorPostsUrl(author), and filters its own posts with getPostsByTagSlugs([...]), getPostsByCategorySlugs([...]) and their ...Names variants. For a few featured posts on another page, ask for exactly those rather than loading the blog's posts and filtering:

Template code Three featured posts of one blog
{$featured = $_query->findBy('Blogs\Entities\Post', ['blog' => $blog->getId(), 'featured' => true], ['published' => 'DESC'], 3)}
{foreach $featured as $item}
  <a href="{$item->getFullPath()}">{$item->getName()|escape}</a>
{/foreach}
{% set featured = _query.findBy('Blogs\\Entities\\Post', {'blog': blog.getId(), 'featured': true}, {'published': 'DESC'}, 3) %}
{% for item in featured %}
  <a href="{{ item.getFullPath() }}">{{ item.getName()|e }}</a>
{% endfor %}

Comments

Posts carry comments: records of their own with a text, a published state, the person who wrote them, and replies as children. A blog's Auto publish comments decides whether a new comment is published at once, and Commentator email requires an email address with each comment. Comments are moderated in the admin and are also reachable through the API (Post Comments). In a template, getPublishedComments() on a post lists the published ones and getPublishedCommentsCount() counts them.

The blog's Auto publish posts and Lock comments switches are stored but not acted on in this version: publish each post with its published date, and stop comments by not offering the form.

When a blog is the wrong choice

A blog fits dated content that is found by search or browsed newest first. It is the wrong home for content that only uses the blog for its free listing and URLs: locations, people or products end up with dates that mean nothing and a /blogs/... segment in every address. Those are custom entities. Troubleshooting answers belong in FAQ.

Go deeper

Was this page helpful?

Your answer goes to the docs team.