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

Books, units and book pages

A book is an ordered, three-level structure: the book, its units and the pages in each unit. When reading order makes a book the right choice, the templates a site provides for each level, the methods for walking a book, and a table of contents with previous and next links in both engines.

Who this is for. The content modeller with a course, a manual or a guide to publish, and the theme developer writing its templates. Afterwards you can decide between a book and a page tree, and render a book's contents and its reading order.

When a book is the right choice

Choose a book when reading order matters and the reader progresses through the content: a course, a manual, a multi-part guide. The guides on this site are books. If readers arrive at one page from search and leave again, the order is decoration, and a page tree serves the same content. Since books, units and book pages share the pages table (One table, four types), the choice costs nothing in capability.

The structure and its URLs

LevelURLTemplateVariables
Book/books/<book>books/books/show.htmlbook, record
Unit/books/<book>/<unit>books/units/show.htmlunit, record
Book page/books/<book>/<unit>/<page>Its layout's template, else books/pages/show.htmlpage, record

The framework ships none of these templates; a site that uses Books provides them. Books and units always render their fixed template: the layout on their revision supplies the layout areas the editor fills in, but its template is not used, except when a signed-in editor previews a revision with ?revision=, which applies the revision's layout and so can look different from what visitors see. A book page renders through its layout, like a page. There is nothing at /books itself, and books is the module's default URL prefix, which a site can change.

Each level is published separately. A visitor sees a book page only if its unit and its book are published too, and units and pages that are not published are left out of getUnits() and getPages() for visitors. A signed-in administrator sees everything, so check a book in a private window.

Every book answers 404

When a published book is not found by visitors at all, check that visitors may read the Books module: its read permission is set in the admin's Permissions screen under Users, like any module's. People, users, groups and permissions explains the model.

Walking a book

OnCallReturns
A bookgetUnits(), getUnitsCount(), getPagesCount()The units in position order; the counts
A unitgetBook(), getPages(), getPagesCount()Its book; its pages in position order
A book pagegetUnit(), getPositionInBook(), getOtherLanguageVersions()Its unit; its page number across the whole book; its translations

There is no getAllParents() on any of the three, and no next or previous method. getParent() and getChildren() do exist, inherited from the page tree, but they resolve to the same book and unit relations as getBook(), getUnits() and getPages(), not to a page tree: a breadcrumb or child-navigation template written for pages compiles and runs against a book, a unit or a book page too, reading that type's own book and unit relations rather than a page tree. Write book templates against getBook(), getUnits() and getPages(), not the inherited page-tree methods. getPositionInBook() is the number of pages in the units before this one plus the page's own position, which is what previous and next are built on.

A table of contents, from a book page:

Template code books/pages/show.html: the book's contents
{$book = $page->getUnit()->getBook()}
<nav class="book-contents">
  <a href="{$book->getFullPath()}">{$book->getName()|escape}</a>
  {foreach $book->getUnits() as $chapter}
    <h2><a href="{$chapter->getFullPath()}">{$chapter->getName()|escape}</a></h2>
    <ol>
      {foreach $chapter->getPages() as $article}
        <li><a href="{$article->getFullPath()}"{if $article->getId() == $page->getId()} aria-current="page"{/if}>{$article->getName()|escape}</a></li>
      {/foreach}
    </ol>
  {/foreach}
</nav>
{% set book = page.getUnit().getBook() %}
<nav class="book-contents">
  <a href="{{ book.getFullPath() }}">{{ book.getName()|e }}</a>
  {% for chapter in book.getUnits() %}
    <h2><a href="{{ chapter.getFullPath() }}">{{ chapter.getName()|e }}</a></h2>
    <ol>
      {% for article in chapter.getPages() %}
        <li><a href="{{ article.getFullPath() }}"{% if article.getId() == page.getId() %} aria-current="page"{% endif %}>{{ article.getName()|e }}</a></li>
      {% endfor %}
    </ol>
  {% endfor %}
</nav>

In the book template, start from $book; in a unit template, from $unit->getBook(). Name loop variables something other than page, unit and book: in Smarty, {foreach ... as $page} overwrites the record being rendered for the rest of the template.

Previous and next

Walk the book once and compare page numbers. For a book of tens or a few hundred pages this is cheap; the units and pages are loaded once.

Template code books/pages/show.html: previous and next links
{$book = $page->getUnit()->getBook()}
{$current = $page->getPositionInBook()}
{$previousPage = null}
{$nextPage = null}
{foreach $book->getUnits() as $chapter}
  {foreach $chapter->getPages() as $article}
    {$number = $article->getPositionInBook()}
    {if $number == $current - 1}{$previousPage = $article}{/if}
    {if $number == $current + 1}{$nextPage = $article}{/if}
  {/foreach}
{/foreach}
{if $previousPage}<a rel="prev" href="{$previousPage->getFullPath()}">{$previousPage->getName()|escape}</a>{/if}
{if $nextPage}<a rel="next" href="{$nextPage->getFullPath()}">{$nextPage->getName()|escape}</a>{/if}
{% set book = page.getUnit().getBook() %}
{% set current = page.getPositionInBook() %}
{% set previousPage = null %}
{% set nextPage = null %}
{% for chapter in book.getUnits() %}
  {% for article in chapter.getPages() %}
    {% set number = article.getPositionInBook() %}
    {% if number == current - 1 %}{% set previousPage = article %}{% endif %}
    {% if number == current + 1 %}{% set nextPage = article %}{% endif %}
  {% endfor %}
{% endfor %}
{% if previousPage %}<a rel="prev" href="{{ previousPage.getFullPath() }}">{{ previousPage.getName()|e }}</a>{% endif %}
{% if nextPage %}<a rel="next" href="{{ nextPage.getFullPath() }}">{{ nextPage.getName()|e }}</a>{% endif %}

Positions are counted from the stored positions. After deleting a page from the middle of a unit, reorder the unit's pages in the admin to keep the numbering and the previous and next links continuous.

Settings that describe the whole book

Put what describes the whole book, its audience or its level, in areas or custom fields on the book, and read it from the unit and page templates through getBook(), so a page cannot contradict its book. Books, units and book pages also take custom fields, which suit a value the layout does not need to show. Name switch areas for the case that is not the default, hideContents rather than showContents, because an unset switch and one that is off read the same.

PDF versions

A book page's URL with .pdf appended answers with that page as a PDF. A whole book can also be exported to one PDF, from the admin or the API; the book template can then link to it with getExportPdfPath(), and getExportPdfStatus() and getExportPdfDate() say whether and when the export finished.

Search, the sitemap and template queries

Books, units and book pages are in the site search index, and a template can fetch them with _query. They are left out of /sitemap.xml unless the Books settings Generate books in sitemap, Generate units in sitemap and Generate pages in sitemap are switched on; all three are off by default.

Go deeper

Was this page helpful?

Your answer goes to the docs team.