Nexus themes

31 min read

Build a custom theme

A custom theme is a folder of templates, stylesheets, scripts, images and fonts, packaged as a ZIP file. This section is for the person who builds it, such as a web developer. It assumes you know HTML and CSS.

The templates are written in a template language called Twig. Paligo gives each template the page content and information about the site, such as its title, languages and navigation. The template decides what HTML to produce around them.

The quickest way to build a theme is to start from Paligo Base:

  1. Download Paligo Base, as described in Download a theme, and unzip it.

  2. Change its templates, styles and options.

  3. Preview it on your own content with Paligo Theme Studio, or upload it to a test layout.

  4. When it is ready, upload it to the layout you publish with, or push it from Paligo Theme Studio.

You can also have an AI coding assistant, such as Claude Code or GitHub Copilot, make the changes for you. Paligo Base includes instructions that tell the assistant what a theme must contain and what it can use. See Build a theme with an AI coding assistant.

The topics in this section describe each part of a theme:

Important

A theme controls only the look and structure of the pages. It can't contain server code, and templates run with a restricted set of Twig features, so a theme can't read or change anything outside its own pages.

Start from Paligo Base

Paligo Base is a complete theme, written to be changed. Download it, as described in Download a theme, and change as much or as little as you want: its colors and fonts, its templates, or all of it. This topic describes how it is built, so that you know where to make a change.

Where things are

File

Contains

templates/base.html.twig

The page frame for every page: the header, the navigation sidebar and the footer. Read it first.

templates/page.html.twig

Topic pages: the breadcrumb trail, the content, the previous and next links, and the on-page table of contents.

templates/portal.html.twig

The portal page: the banner, the featured topics and the list of topics.

templates/search.html.twig

The search results page.

templates/partials/

The logo and site title, the portal banner, the search box and the light and dark mode button.

assets/css/main.css

All of the theme's styles. Its first section sets the colors, fonts and sizes.

assets/css/theme.css

Styles for the output of the paligo_…() components.

assets/css/code-theme.css

Code colors, used when the layout's Code colours is Theme colours.

assets/js/

Scripts for the navigation sidebar, the on-page table of contents, the light and dark mode button and the portal.

i18n/en.json

The theme's own texts, in English.

README.md

A short introduction to the theme.

AGENTS.md

Instructions for AI coding assistants. See Build a theme with an AI coding assistant.

CLAUDE.md

Points Claude Code to AGENTS.md.

Change colors, fonts and sizes

Most changes only need the CSS custom properties at the top of assets/css/main.css. Every color, font and size in the theme comes from them:

Property

Controls

--paligo-primary, --paligo-primary-hover

Links, buttons and highlights. The Accent color option sets these.

--paligo-brand, --paligo-brand-end

The two colors of the gradient on the built-in logo mark and the current navigation item.

--paligo-bg, --paligo-surface, --paligo-surface-2

The page background, and the background of the sidebar, code blocks and admonitions.

--paligo-text, --paligo-muted, --paligo-faint

Text colors, from strongest to faintest.

--paligo-border, --paligo-border-strong

Borders.

--paligo-note, --paligo-tip, --paligo-important, --paligo-caution, --paligo-warning, --paligo-danger

The color of each type of admonition.

--paligo-font, --paligo-mono

The text font and the code font.

--paligo-font-size, --paligo-line-height

The size and line spacing of text.

--paligo-radius-sm, --paligo-radius, --paligo-radius-lg

Rounded corners.

--paligo-header-h, --paligo-sidebar-w, --paligo-toc-w, --paligo-shell-max

The height of the header, the widths of the sidebar and the on-page table of contents, and the greatest width of the page.

--paligo-code-keyword, --paligo-code-string, --paligo-code-comment and the other --paligo-code-… properties

Code colors, when Code colours is Theme colours. They are in code-theme.css.

:root {
    --paligo-primary: #0b7a75;
    --paligo-primary-hover: #075955;
    --paligo-font: "Source Sans 3", system-ui, sans-serif;
}
Note

Paligo Base loads the Inter font from Google Fonts. To serve your own font instead, add it to assets/fonts/ as a WOFF2 file, declare it with @font-face, change --paligo-font, and remove the @import of Google Fonts at the top of main.css.

Light and dark mode

Paligo Base follows the reader's light or dark setting, and readers can switch with the button in the header. The dark colors are a second set of the same properties, in main.css under @media (prefers-color-scheme: dark) and :root[data-theme="dark"]. If you change a color, change it in both sets.

When a reader selects the button, the <html> element gets data-theme="light" or data-theme="dark", and the browser remembers the choice.

Change the page structure

base.html.twig is divided into Twig blocks, which the other templates replace where they need to. To change one part of every page, edit that block in base.html.twig:

Block

Contains

chrome

The header and the navigation sidebar together.

nav_toggle

The menu button for the sidebar on small screens.

topbar_search

The search box in the header.

header_tools

The language menu and the light and dark mode button.

crumb

The breadcrumb trail.

sidebar

The navigation sidebar.

main

The main area of the page.

content

The content inside the main area.

title

The page title in the browser tab.

body_class

Classes on the <body> element.

Keep {{ paligo.head }} and {{ paligo.foot }} in base.html.twig, and keep your stylesheets after {{ paligo.head }}, so that your styles take priority over Paligo's.

Style your content

Paligo writes the HTML for your content, and gives it these classes:

Content

Classes

Admonitions

.admonition with .note, .tip, .important, .caution, .warning or .danger, containing .title and .body

Figures

.figure, .informalfigure and .caption

Tables

.informaltable and .table

Code

pre, code and .code-block, with .code-copy for the copy button

Accordions

details.accordion and .accordion-body

Glossary terms

a[data-glossary-def], and .glossary-popover for the popup

Interface names and keys

.guilabel and the other .gui… classes, and .keycap

Untranslated text in a draft

.translation-missing and .translation-changed

Paligo's own stylesheet styles these, and main.css styles them again. To change them, change the rules in main.css.

Right-to-left languages

Paligo Base sets dir="rtl" on pages in right-to-left languages, but its stylesheet isn't mirrored for them. If you publish in Arabic or Hebrew, check every page type and add the rules you need, such as for [dir="rtl"] .site-nav.

Keep it working with Paligo
  • Keep the site-search__… class names if you change the search box. See Search in a theme.

  • Keep an option's key in theme.json when you change the option. Layouts store their values by key, so renaming a key loses the value set for it on every layout.

  • Check the theme in light and dark mode, and on a topic page, the portal page and the search page.

Build a theme with an AI coding assistant

An AI coding assistant, such as Claude Code or GitHub Copilot, can build or change a theme from a description of what you want. You don't need to explain Paligo's rules to it first. Paligo Base includes an AGENTS.md file with instructions for coding assistants. It describes:

  • the files a theme needs, and how to package them

  • the rules a theme must follow to work with Paligo, such as keeping paligo.head and paligo.foot and the search box's class names

  • the parts of Twig that templates can use

  • the information and components that Paligo gives a template

  • how to declare theme options

  • how to check the result

Coding assistants read AGENTS.md when they start working in a folder. Claude Code reads CLAUDE.md instead, and the CLAUDE.md in Paligo Base tells it to read AGENTS.md.

To build a theme with an AI coding assistant:

  1. Download Paligo Base, as described in Download a theme, and unzip it.

  2. Open the theme folder in Visual Studio Code, and start Paligo Theme Studio's preview, as described in Preview a theme in Paligo Theme Studio.

  3. Start your coding assistant in the theme folder.

  4. Describe the theme you want. For example:

    Change the colors and fonts to match our website, use our logo in the portal banner, and add links to our support pages to the footer.

    The preview updates each time the assistant saves a file, so you can follow its changes.

  5. Check the result in the preview: on a topic page, the portal page and the search page, and in light and dark mode. Ask the assistant to change anything that isn't right.

  6. When the theme is ready, upload it to a layout, as described in Upload a custom theme, or push it from Theme Studio, as described in Push a theme to a layout.

    If Paligo reports a problem with the theme, give the assistant the messages, and it can fix them. For what they mean, see Theme validation messages.

Tip

An assistant that can control a web browser can also look at the preview itself. Open the preview in a browser, as described in Open the preview in a browser, and the assistant can read the page, follow links and check how it looks.

Note

Keep AGENTS.md and CLAUDE.md in your theme if you go on working on it with a coding assistant. Paligo doesn't publish them.

Theme package structure

A theme is a folder with this structure. Only theme.json and templates/page.html.twig are required.

my-theme/
├── theme.json                 the theme manifest (required)
├── templates/
│   ├── page.html.twig         every topic page (required)
│   ├── base.html.twig         a shared page frame the others extend
│   ├── portal.html.twig       the portal page
│   ├── search.html.twig       the search results page
│   └── partials/              smaller templates that others include
│       └── header.html.twig
├── assets/                    stylesheets, scripts, images and fonts
│   ├── css/main.css
│   ├── js/theme.js
│   └── img/logo.svg
└── i18n/                      translations of the theme's own text
    ├── en.json
    └── de.json

Folders under templates/ and assets/ can have any name and any depth.

Create the ZIP file

Compress the contents of the theme folder, not the folder itself, so that theme.json is at the top of the ZIP file.

  • macOS: open the theme folder, select everything in it, and choose Compress.

  • Windows: open the theme folder, select everything in it, and choose Send to > Compressed (zipped) folder.

If you compress the folder itself, Paligo still accepts the theme, but shows a warning. If the files are more than one folder deep, the upload is rejected.

Files that your computer adds are ignored and don't need to be removed: __MACOSX folders, .DS_Store, files starting with ._, Thumbs.db and desktop.ini. So are .git folders and .gitignore, .gitattributes, .gitkeep and .gitmodules files.

Limits

Limit

Value

Size of the ZIP file

20 MB

Size of the files when unzipped

100 MB

Number of files and folders in the ZIP file

5,000

Allowed file types

A theme can only contain files with these extensions:

.twig .css .js .json .svg .png .jpg .jpeg .webp .woff2 .ico .txt .md .map

If the ZIP file contains any other type of file, the whole upload is rejected. A theme can never contain .php files.

These are also rejected: symbolic links, two files with the same path, and paths that start with / or a drive letter or that contain ...

The theme manifest

Every theme has a theme.json file at its top level. It names the theme, says which version of the theme contract it is written for, and lists its options.

{
    "name": "Acme Docs",
    "version": "1.0.0",
    "author": "Acme Inc.",
    "contract": "^1.0",
    "options": [
        {
            "key": "accent",
            "type": "color",
            "label": "Accent color",
            "description": "Links, buttons and the portal banner.",
            "default": "#3a46e8"
        }
    ],
    "i18n": { "sourceLanguage": "en" }
}

Field

Required

Description

name

Yes

The theme's name. It is used in the name of the downloaded ZIP file.

version

Yes

The theme's version. Use a version number such as 1.0.0; anything else gives a warning.

contract

Yes

The versions of the theme contract the theme works with. Use "^1.0".

author

No

Who made the theme.

options

No

The settings the theme offers in the Layout Editor. See Theme options reference.

i18n

No

sourceLanguage names the translation file used when a text has no translation in the page's language. The default is en. See Translate a theme.

The contract

The theme contract is the set of variables, functions and rules that Paligo gives templates, as described in this section. Its current version is 1.0. The contract field says which versions your theme works with, so that a later, incompatible version of the contract can't break a published theme.

Use "^1.0", which means any 1.x version. These forms are also accepted:

Form

Means

"1.0"

Exactly 1.0

"^1.0" or "^1"

Any 1.x version

"~1.0"

Any 1.0.x version

"1.x"

Any 1.x version

"*"

Any version

"^1.0 || ^2.0"

Either range

Comparisons such as ">=1.0" are not accepted.

Theme templates

Paligo uses up to four templates, all in the templates/ folder:

Template

Used for

Required

page.html.twig

Every topic page

Yes

portal.html.twig

The portal page, the site's front page

No

search.html.twig

The search results page

No

base.html.twig

Nothing directly. The others can extend it, so the page frame is written once

No

If the theme has no portal, search or base template, Paligo uses a plain built-in one. The search page is only produced when the layout has a search engine turned on.

Every other template, such as a partial for the header, can have any name and is used only when one of your templates includes it. Paligo checks every .twig file in the theme, including any that nothing uses.

A minimal theme

This page.html.twig produces a complete page:

<!doctype html>
<html lang="{{ site.lang }}"{% if site.dir %} dir="{{ site.dir }}"{% endif %}>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ page.title }} | {{ site.title }}</title>
    <link rel="stylesheet" href="{{ asset('css/main.css') }}">
    {{ paligo.head }}
</head>
<body>
    <main>
        <h1>{{ page.title }}</h1>
        {{ page.content }}
    </main>
    {{ paligo.foot }}
</body>
</html>
What every page needs
  • {{ paligo.head }} just before </head>. It adds the stylesheet for your content, the search setup, analytics, SEO tags and the layout's Custom &lt;head&gt; code.

  • {{ paligo.foot }} just before </body>. It adds the scripts for search, code copy buttons and glossary popovers, and the layout's Custom end-of-body code.

  • lang="{{ site.lang }}" on <html>. Search with several languages uses it to choose the right index.

  • dir="{{ site.dir }}" on <html> when site.dir is set, so right-to-left languages display correctly.

If a template leaves out paligo.head or paligo.foot, Paligo adds it before </head> or </body> itself and writes a warning in the publishing log. Place them yourself so that you control where they go.

Share a page frame between templates

Write the parts every page shares once in base.html.twig, and fill in the parts that differ with blocks:

templates/base.html.twig:

<!doctype html>
<html lang="{{ site.lang }}"{% if site.dir %} dir="{{ site.dir }}"{% endif %}>
<head>
    <meta charset="utf-8">
    <title>{% block title %}{{ page.title }} | {{ site.title }}{% endblock %}</title>
    <link rel="stylesheet" href="{{ asset('css/main.css') }}">
    {{ paligo.head }}
</head>
<body>
    {% include 'partials/header.html.twig' %}
    {% block content %}{% endblock %}
    {{ paligo.foot }}
</body>
</html>

templates/page.html.twig:

{% extends 'base.html.twig' %}
 
{% block content %}
    <main>
        <h1>{{ page.title }}</h1>
        {{ page.content }}
    </main>
{% endblock %}

Template names are relative to the templates/ folder.

HTML is escaped

Everything a template prints is HTML-escaped, so text from your content or the layout can't break the page. The values that already are HTML print as HTML without anything extra: page.content, paligo.head, paligo.foot and the output of the paligo_…() components.

Limits

Each page must finish rendering within 10 seconds. A template that includes itself, or a macro that calls itself without stopping, is stopped after 128 levels.

Theme options reference

Theme options are settings your theme offers in the Layout Editor, so that the people who publish with it can adjust it without changing the theme. You declare them in the options list of theme.json, and templates read the value chosen for the layout as theme.options.<key>.

{
    "key": "accent",
    "type": "color",
    "label": "Accent color",
    "description": "Links, buttons and the portal banner.",
    "default": "#3a46e8"
}

Field

Required

Description

key

Yes

The name templates use. An option without a key is ignored.

type

No

The kind of value, which decides the field in the Layout Editor. See the types below. A missing or unknown type gives a text field.

label

No

The option's name in the Layout Editor. Without one, the key is shown.

description

No

Help text shown under the field, up to 500 characters.

default

No

The value used until someone changes it. Give every option a default, so that the theme works before anyone sets it.

values

For select

The choices.

Types

Type

Also written as

In the Layout Editor

Value in templates

color

A color picker

A string

text

string, url

A text field

A string

select

choice, enum

A list of choices

A string

toggle

bool, boolean

Enable or Disable

true or false

number

int, integer, float

A text field, checked for a number when the layout is saved

A number

image

Upload image

The image's file name

color: The value must be a hex color with 3, 4, 6 or 8 digits, such as #3a46e8, or a CSS color name, such as rebeccapurple or transparent. Values such as rgb(…) and hsl(…) are not accepted. Use a default of "" to let the option be left empty, and test for it in the template.

<style>:root { --accent: {{ theme.options.accent }}; }</style>

text: Up to 2,000 characters.

select: List the choices in values, either as objects with a value and a label, or as plain strings that are both:

{
    "key": "hero-contrast",
    "type": "select",
    "label": "Portal banner text and logo",
    "default": "auto",
    "values": [
        { "value": "auto",  "label": "Automatic (from the background)" },
        { "value": "light", "label": "Light" },
        { "value": "dark",  "label": "Dark" }
    ]
}
"values": ["auto", "light", "dark"]

A select without any values is shown as a text field.

toggle:

{% if theme.options.sidebar %}
    <nav class="sidebar">…</nav>
{% endif %}

number: Any finite number, such as 68 or 1.5.

image: The person setting the option uploads an image, and Paligo publishes it with the theme's assets. Use asset() to link to it:

{% if theme.options['hero-image'] %}
    <div class="hero" style="background-image: url('{{ asset(theme.options['hero-image']) }}')"></div>
{% endif %}
Reading options in templates

Use a dot for a simple key, and brackets for a key with a hyphen in it:

{{ theme.options.accent }}
{{ theme.options['hero-image'] }}

The Layout Editor checks values when the layout is saved. If a stored value doesn't suit the option's type, such as after you change an option from text to toggle, it is ignored when publishing and the default is used.

Theme assets

Everything in the theme's assets/ folder is published with the site: stylesheets, scripts, images and fonts. To link to an asset, use the asset() function with the file's path inside assets/:

<link rel="stylesheet" href="{{ asset('css/main.css') }}">
<script src="{{ asset('js/theme.js') }}" defer></script>
<img src="{{ asset('img/logo.svg') }}" alt="">

Always use asset() rather than writing the path yourself. Pages are published in a folder per language, and asset() gives the path that works from every page. A path containing .. is rejected.

Inside a stylesheet, link to other assets with paths relative to the stylesheet, as usual:

@font-face {
    font-family: "Inter";
    src: url("../fonts/inter.woff2") format("woff2");
}

Fonts must be in WOFF2 format. Images can be SVG, PNG, JPG, WebP or ICO.

The stylesheet for your content itself, which styles the tables, admonitions, code blocks and other elements in page.content, comes from Paligo and is added by paligo.head. Your theme's stylesheets are loaded alongside it, so you can change any of its styles.

For the class names in your content, such as admonitions, tables and code blocks, see Start from Paligo Base.

Translate a theme

Text that your theme writes itself, such as "Skip to content" or "Open menu", can be translated, so that it matches the language of each page. The text that Paligo writes, such as the content and the site title, is translated as usual. To translate your theme's own text:

  1. Add an i18n folder to the theme.

  2. Add a JSON file for the theme's source language, named after its language code, such as i18n/en.json. List each text with a key:

    {
        "skipToContent": "Skip to content",
        "openMenu": "Open menu"
    }
  3. Add a file with the same keys for each other language, such as i18n/de.json:

    {
        "skipToContent": "Zum Inhalt springen",
        "openMenu": "Menü öffnen"
    }
  4. In theme.json, set the source language:

    "i18n": { "sourceLanguage": "en" }
  5. In your templates, use theme.strings.<key> wherever the text appears:

    <a class="skip-link" href="#main">{{ theme.strings.skipToContent }}</a>

Each page uses the file that matches its language. A key that is missing from that file is taken from the source language's file.

Note

The file name must match the page's language code exactly, as site.lang shows it. If site.lang is de-DE, the file must be de-DE.json: Paligo doesn't fall back from de-DE to de. Keep each file a flat list of keys and texts, because nested objects are ignored.

site.strings holds a few texts that Paligo translates for you, such as the search box placeholder. See Template variables.

Template variables

Every template has four variables: site for the published site, page for the page being produced, theme for your theme's options and texts, and paligo for markup that Paligo adds. A value that isn't set prints as nothing.

site

Variable

Type

Contents

site.title

text

The layout's Site title, or the publication's title if that is empty.

site.subtitle

text or null

The layout's Site subtitle.

site.logo

URL or null

The layout's Logo.

site.logoDark

URL or null

The layout's Logo for dark backgrounds. Null means use site.logo everywhere.

site.logoLink

URL or null

The layout's Logo link URL. Null means link the logo to the site's front page.

site.logoLinkNewTab

true or false

The layout's Open logo link in a new tab. Only matters when site.logoLink is set.

site.favicon

URL or null

The layout's Favicon.

site.copyright

text or null

The footer copyright line, from the layout's Footer copyright, or from Company name and Copyright year.

site.base_url

text

The layout's Site base URL, or empty.

site.lang

text

The language code of the page, such as en or de-DE.

site.dir

rtl, ltr or null

The text direction of the publication's language. Null when it isn't set, which means left-to-right.

site.languages

list

The site's languages. See below.

site.nav

list

The navigation tree. See below.

site.topNav

object

The links for a top navigation bar. See below.

site.portal

object or null

The portal page's content. Null if the site has no portal page. See below.

site.search

object

The search engine. site.search.backend is none, pagefind (Nexus Search), algolia or coveo. See Search in a theme.

site.strings

object

Texts that Paligo translates into the page's language: searchPlaceholder, navPrev and navNext.

site.codeColors

text

The layout's Code colours: pygments for Syntax style, or theme for Theme colours. With theme, your stylesheet colors highlighted code.

site.languages: one entry for each language, with:

  • code: the language code, such as de.

  • label: the language's name in that language, such as Deutsch.

  • default: true for the site's default language.

site.nav: the pages of the publication as a tree, for a navigation menu. Each entry has:

  • title: the page title.

  • url: the link to the page. Missing if the entry has no page of its own.

  • children: the entries under it, in the same form.

  • current: true for the page being produced.

  • ancestor: true for every entry above the current page.

{% macro tree(items) %}
    <ul>
    {% for item in items %}
        <li{% if item.current %} aria-current="page"{% endif %}>
            {% if item.url %}<a href="{{ item.url }}">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}
            {% if item.children %}{{ _self.tree(item.children) }}{% endif %}
        </li>
    {% endfor %}
    </ul>
{% endmacro %}
 
<nav class="sidebar">{{ _self.tree(site.nav) }}</nav>

site.topNav: two lists, either of which can be empty:

  • items: topics whose section has the role top-nav-link. Each has a title, a url and children, a list of title and url for a dropdown.

  • links: the layout's Top navigation links. Each has a title and a url.

Show a top bar only when at least one of the lists has entries.

site.portal: the content for the portal page:

  • heading: the layout's portal heading, or a translated default.

  • featured: the featured topics, each with a title, slug and url.

  • sections: the topics to browse, each with a title, slug and url.

  • labels: translated texts for the portal: featuredContent, showMore and showLess.

  • startPage: the slug of the topic to start reading from, if there is one.

page

Variable

Type

Contents

page.title

text

The page title.

page.content

HTML

The page's content. Print it with {{ page.content }}.

page.type

text

topic, portal or search.

page.home

true or false

true on the publication's first page when the site has no portal page.

page.slug

text

The page's file name without .html.

page.url

text

The page's file name, such as install.html.

page.toc

list

The headings on the page, for an on-page table of contents. Each has a title, an anchor and children.

page.breadcrumbs

list

The pages above this one, starting from the top. Each has a title and a slug.

page.prevPage, page.nextPage

object or null

The previous and next pages, each with a title, slug and url.

page.meta.description

text

A plain-text summary of the page, up to 160 characters.

page.meta.canonical

URL

The page's canonical URL, when the layout has Add canonical URL turned on.

page.taxonomies

list

The page's taxonomy values. Each has a group, a value and, for nested taxonomies, a hierarchy: a list of levels from the top down.

page.search.weight

text

boost or reduce when the topic's search priority has been changed.

On the portal and search pages, page.content is empty and the lists are empty.

{% if page.prevPage or page.nextPage %}
<nav class="pager">
    {% if page.prevPage %}<a rel="prev" href="{{ page.prevPage.url }}">{{ site.strings.navPrev }}: {{ page.prevPage.title }}</a>{% endif %}
    {% if page.nextPage %}<a rel="next" href="{{ page.nextPage.url }}">{{ site.strings.navNext }}: {{ page.nextPage.title }}</a>{% endif %}
</nav>
{% endif %}
theme

Variable

Contents

theme.options

The value of each theme option for the layout. See Theme options reference.

theme.strings

The theme's own texts in the page's language. See Translate a theme.

paligo

Variable

Contents

paligo.head

Markup that must be in every page's <head>. See Theme templates.

paligo.foot

Markup that must be at the end of every page's <body>. See Theme templates.

Paligo components and helpers

Paligo gives templates ready-made components for common parts of a page, and a few helper functions and filters. A component produces complete, accessible HTML that you style with your own CSS. You can also build the same parts yourself from the Template variables.

Components

Function

Produces

Produces nothing when

paligo_search_box()

A search box. See Search in a theme.

The layout has no search engine.

paligo_breadcrumbs()

A breadcrumb trail.

The page is at the top level.

paligo_toc()

A list of the headings on the page.

The page has no headings.

paligo_pager()

Links to the previous and next pages.

There are no previous or next pages.

paligo_language_switcher()

Links to the site in its other languages.

The site has one language.

<header>{{ paligo_search_box() }}</header>
<aside>{{ paligo_toc() }}</aside>
<main>
    {{ paligo_breadcrumbs() }}
    {{ page.content }}
    {{ paligo_pager() }}
</main>

The class names to style:

Component

Markup

paligo_breadcrumbs()

<nav class="paligo-breadcrumbs"> containing an <ol>. The current page is the last <li>, with aria-current="page".

paligo_toc()

<nav class="paligo-toc"> containing nested <ul> lists of links.

paligo_pager()

<nav class="paligo-pager"> containing <a class="paligo-pager-prev"> and <a class="paligo-pager-next">.

paligo_language_switcher()

<nav class="paligo-language-switcher"> containing a <ul>. The current language's link has aria-current="true".

Note

The texts in paligo_pager() and the accessible names of these components are in English. For a site in other languages, build the pager from page.prevPage and page.nextPage with site.strings.navPrev and site.strings.navNext, as shown in Template variables.

Write your own language menu

paligo_language_switcher() produces a plain list. To show the languages in your own way, such as a dropdown in the header, build the menu from site.languages. This example shows the current language's name, and opens to a list of every language:

templates/partials/language-menu.html.twig:

{% if site.languages|length > 1 %}
  {% set current = site.languages|filter(l => l.code == site.lang)|first %}
  <details class="language-menu">
    <summary aria-label="{{ theme.strings.changeLanguage|default('Change language') }}">
      {{ current.label|default(site.lang|upper) }}
    </summary>
    <ul>
      {% for language in site.languages %}
        <li>
          {% if language.code == site.lang %}
            <a href="#" aria-current="true" lang="{{ language.code }}" hreflang="{{ language.code }}">{{ language.label }}</a>
          {% else %}
            <a href="../{{ page.translations[language.code]|default(language.code ~ '/') }}" lang="{{ language.code }}" hreflang="{{ language.code }}">{{ language.label }}</a>
          {% endif %}
        </li>
      {% endfor %}
    </ul>
  </details>
{% endif %}

Include it where the menu goes:

{% include 'partials/language-menu.html.twig' %}

How the example works:

  • The menu is shown only when the site has more than one language.

  • site.languages has each language's code, and its label, the language's name in that language, such as Deutsch. The current page's language is site.lang.

  • The current language is marked with aria-current. The lang attribute lets screen readers say each name in its own language.

  • Each other language links to the same page in that language when Paligo knows it, from page.translations, and otherwise to that language's front page. This is what paligo_language_switcher() does too.

Use <details> and <summary> as in the example, and the menu opens and closes without a script.

Helper functions

asset(path) gives the link to a file in the theme's assets/ folder. See Theme assets.

paligo_contrast_color(background, dark, light) returns whichever of dark and light is easier to read on background, using the WCAG contrast ratio. dark defaults to #161617 and light to #ffffff. Use it to pick a text color that works with a color chosen in a theme option:

<div class="hero" style="background: {{ theme.options.accent }}; color: {{ paligo_contrast_color(theme.options.accent) }}">

All three colors must be hex colors, such as #3a46e8. If one isn't, the function returns an empty value.

Filters

excerpt(length) turns HTML into plain text and shortens it to at most length characters, at the end of a word, adding "…" when it shortens it. The default length is 160.

<meta property="og:description" content="{{ page.content|excerpt(200) }}">
Search in a theme

The search engine is chosen in the layout's Search settings: Nexus Search, Algolia or Coveo. Your theme provides the search box, and Paligo provides everything else: the index, the scripts that run the search, and a search results page. The theme works with every search engine without changes.

Add the search box

Place paligo_search_box() where the search box goes:

{{ paligo_search_box() }}

By default, results drop down under the box as the reader types, with a link to see all results. For the search results page, use paligo_search_box('page'), which lists the results on the page, keeps the search in the page address, and shows taxonomy filters when the layout has them turned on.

{# templates/search.html.twig #}
{% extends 'base.html.twig' %}
{% block content %}
    <main>
        <h1>{{ page.title }}</h1>
        {{ paligo_search_box('page') }}
    </main>
{% endblock %}

When the layout has no search engine, paligo_search_box() produces nothing and no search page is published.

Style the search box

The search box and its results use these classes:

Class

Element

site-search

The whole search box. data-search-mode is dropdown or page.

site-search__form

The form.

site-search__input

The text field.

site-search__clear

The button that clears the text field.

site-search__panel

The area that holds the results.

site-search__status

The line that says how many results there are.

site-search__results

The list of results.

site-search__result, site-search__result-link, site-search__result-title, site-search__result-excerpt

One result, its link, title and excerpt. The search words in the excerpt are in <mark>.

site-search__sub, site-search__sub-link

Matching headings within a result.

site-search__all

The link to all results, in a dropdown.

site-search__more

The button that loads more results, on the search page.

site-search__facets

The taxonomy filters, on the search page.

The highlighted result has the class is-active.

Write your own search box

You can write the search box's HTML yourself, for example to add your own icon or button, or to put it in a different structure. Paligo's search script finds every element with the class site-search on the page, connects it to the layout's search engine and fills in the results. Your markup only has to use the class names the script looks for.

This example writes the search box once, as a macro, and uses it in two places: as a dropdown in the header of every page, and as a full search on the search results page.

templates/partials/search-box.html.twig:

{% macro box(mode, id, placeholder, strings) %}
<div class="site-search" data-search-mode="{{ mode }}">
  <form class="site-search__form" role="search" method="get" action="search.html">
    <input class="site-search__input" type="search" name="q" autocomplete="off"
        placeholder="{{ placeholder }}" aria-label="{{ placeholder }}"
        {%- if mode == 'dropdown' %} role="combobox" aria-expanded="false" aria-controls="{{ id }}-panel" aria-autocomplete="list"{% endif %}>
    <button type="button" class="site-search__clear" hidden aria-label="{{ strings.clearSearch|default('Clear search') }}">&times;</button>
  </form>
  <div class="site-search__panel" id="{{ id }}-panel"{% if mode == 'dropdown' %} hidden{% endif %}>
    <p class="site-search__status" role="status" aria-live="polite"></p>
    <ul class="site-search__results"></ul>
    {% if mode == 'dropdown' %}
      <a class="site-search__all" hidden></a>
    {% else %}
      <button type="button" class="site-search__more" hidden>{{ strings.searchMore|default('Load more results') }}</button>
    {% endif %}
  </div>
  {% if mode == 'page' %}
    <div class="site-search__facets" hidden></div>
  {% endif %}
</div>
{% endmacro %}

The macro takes the mode (dropdown or page), an ID that is unique on the page, the placeholder text and the theme's texts. The ID connects the text field to its results, so that screen readers can announce them.

templates/base.html.twig, with the header search in a block of its own:

{% import 'partials/search-box.html.twig' as search %}
<!doctype html>
<html lang="{{ site.lang }}"{% if site.dir %} dir="{{ site.dir }}"{% endif %}>
<head>
    <meta charset="utf-8">
    <title>{% block title %}{{ page.title }} | {{ site.title }}{% endblock %}</title>
    {{ paligo.head }}
</head>
<body>
    <header>
        {% block header_search %}
            {% if site.search.backend|default('none') != 'none' %}
                {{ search.box('dropdown', 'header-search', site.strings.searchPlaceholder|default('Search'), theme.strings) }}
            {% endif %}
        {% endblock %}
        {% include 'partials/language-menu.html.twig' %}
    </header>
    {% block content %}{% endblock %}
    {{ paligo.foot }}
</body>
</html>

templates/search.html.twig, which empties the header search, because the page has its own:

{% extends 'base.html.twig' %}
{% import 'partials/search-box.html.twig' as search %}
{% block header_search %}{% endblock %}
 
{% block content %}
    <main>
        <h1>{{ page.title }}</h1>
        {{ search.box('page', 'search-page', site.strings.searchPlaceholder|default('Search'), theme.strings) }}
    </main>
{% endblock %}

The search results page uses the address search.html, which is the action of the form. If the reader can't run scripts, the form still opens the search results page.

Paligo checks a theme's search box when the theme is uploaded:

  • These parts are required, and a missing one is an error: a <form class="site-search__form"> with an action, a site-search__input field named q, a site-search__panel and a site-search__results.

  • These parts are recommended, and a missing one is a warning: data-search-mode set to dropdown or page, a site-search__status and a site-search__clear, a site-search__all in a dropdown, and a site-search__more and site-search__facets on the search page.

Translate the search texts

The search texts are in English unless your theme's translation files provide these keys. See Translate a theme.

Key

Text

clearSearch

The name of the clear button.

searchMore

The button that loads more results.

searchAll

The link to all results. [COUNT] is replaced by the number of results.

searchPage

The name of the search results page.

searchNone, searchOne, searchMany

The result count. [COUNT] is the number and [SEARCH_TERM] the search.

searchFilters

The heading of the filters.

searchUnavailable

Shown when search can't be loaded.

searchError

Shown when a search fails.

Keep page furniture out of the index

Nexus Search indexes the whole page. To leave a part of the page out of the index, such as a navigation menu or a footer, add data-pagefind-ignore to it:

<nav class="sidebar" data-pagefind-ignore>…</nav>
What theme templates can use

Templates run in a restricted version of Twig. They can use the features in this topic and nothing else. Using anything else is an error when the theme is checked, even in a part of a template that never runs.

Tags: if, for, set, block, extends, use, include, embed, apply, macro, import, from

Filters: escape, e, upper, lower, capitalize, trim, date, length, slice, join, split, default, merge, sort, first, last, map, filter, keys, json_encode, number_format, replace, striptags, url_encode, excerpt, content_sections

Functions: asset, include, min, max, parent, block, paligo_search_box, paligo_search_results, paligo_breadcrumbs, paligo_toc, paligo_pager, paligo_language_switcher, paligo_contrast_color, related_pages

Tests: all of Twig's tests except constant, such as defined, empty, null, iterable, even, odd, same as and divisible by

Not available, among others:

  • The raw filter. The HTML that Paligo provides already prints as HTML.

  • range() and the .. operator. Loop over a list instead.

  • attribute(). Use brackets, such as item[key].

  • template_from_string(), constant(), random(), cycle(), dump() and source().

  • The with, autoescape, do, spaceless and sandbox tags.

  • Calling methods or reading properties of objects. Every value in a template is a text, number, list or map.

For how Twig works, see the Twig documentation.

Theme validation messages

Paligo checks a theme twice when you upload it. The first check is done while the file uploads: if it fails, the upload is refused and nothing changes. The second check renders every template with sample content, and its result is shown in the Layout Editor as Theme validation passed or Theme validation failed.

When uploading

Message

What to do

The theme ZIP is too large (…); the limit is …

Make the theme smaller than 20 MB, such as by compressing images.

The theme ZIP has too many files (…); the limit is …

A theme can have at most 5,000 files and folders.

The theme is too large uncompressed (…); the limit is …

The files can take up at most 100 MB unzipped.

The uploaded file is not a valid ZIP archive.

Compress the theme again as a ZIP file.

The theme's files are not at the top of the ZIP (they are inside "…").

Compress the contents of the theme folder, not the folder. See Theme package structure.

Required file is missing from the theme: …

The theme must have theme.json and templates/page.html.twig.

File type not allowed in a theme (rejected): …

Remove the file. See the allowed file types in Theme package structure.

Symlinks are not allowed in a theme (rejected): …

Replace the symbolic link with the file itself.

Unsafe path in ZIP (rejected): …

A file's path starts with / or a drive letter or contains ... Compress the theme again from its folder.

Duplicate entry in ZIP (rejected): …

The ZIP file contains the same path twice. Compress the theme again.

theme.json is not valid JSON.

Correct the syntax of theme.json, such as a missing comma or quote.

theme.json is missing a "name". (or "version", or "contract" version range)

Add the field. See The theme manifest.

These are warnings, and the upload still succeeds:

Message

Means

The theme's files are inside a folder in the ZIP ("…"); it was ignored.

The ZIP file had a folder around the theme. Paligo used its contents.

theme.json "version" ("…") is not a semver version.

Use a version number such as 1.0.0.

theme.json "author" is ignored (not a string).

Write the author as text.

When checking the templates

Each message names the template and the line, where there is one.

Message

What to do

Theme "…" requires contract "…", but this renderer implements contract 1.0.

Set contract to "^1.0" in theme.json.

Theme is missing the required template "page.html.twig"

Add templates/page.html.twig.

Function "…" is not allowed. (or Filter, or Tag)

The template uses something themes can't use. See What theme templates can use.

Unknown "…" function. (or filter, or tag)

The name is misspelled, or it isn't part of Twig.

Unexpected token, Unclosed "…", and other syntax errors

Correct the Twig syntax on the line given.

Template "…" exceeded the per-template render time limit …

The template took too long. This is almost always a loop that never ends, or a template that includes itself.

Theme template nesting exceeded the maximum depth of 128.

A template includes or embeds itself, or a macro calls itself without stopping.

The search box's markup messages, such as a missing site-search__input

See Search in a theme.

The check uses sample content, so a template can still fail with real content, for example on a value that the sample doesn't have. Preview the theme on your own content with Paligo Theme Studio before you publish with it.