Jack for Craft CMS

Templating

Everything on the front end shows the published text, and only the published text. The draft on the control panel's edit screen never reaches a visitor until someone publishes it. A document that has never been published, or is disabled, renders nothing. See Usage.

The Twig API is available in Lite and Pro.

Document URLs

With Give documents their own URLs on (the default), every document has a page at its slug: /impressum, /datenschutz and so on. With Document template blank, Jack serves a plain, readable, printable page of its own, with a contents list and the Last updated line. That's a working answer, not a placeholder. A legal text has to be reachable, and that shouldn't wait on someone writing Twig.

Turn Give documents their own URLs off if your site publishes its legal texts through its own entries instead. Two copies at two URLs is one too many, and one of them goes stale.

Your own template

Set Document template to a template path. It's rendered with a document variable:

{# templates/_legal/document.twig #}
{% extends '_layout' %}

{% block content %}
    <h1>{{ document.title }}</h1>
    {{ craft.jack.render(document.handle, { toc: true }) }}
{% endblock %}

document.render gives you the same output with the default options.

Rendering a document

{{ craft.jack.render('privacy') }}
{{ craft.jack.render('privacy', { toc: true, headingLevel: 3 }) }}

Every lookup takes a document's handle or its type, so a footer can ask for 'privacy' without knowing what anyone called the document. A handle is tried first. Failing that, you get the first document of that type. The first document of each type gets the type as its handle: imprint, privacy, cookies, terms, withdrawal, disclaimer, accessibility, social.

Options

OptionDefaultWhat it does
tocfalsePuts a contents list above the document
lastUpdatedShow a “last updated” line settingPrints Stand: (German) or Last updated: (English) with the publish date
headingLevel2The level of each section heading. Use 3 when the document sits under an existing <h2>, so the outline stays valid
classjack-documentThe class on the wrapping <div>

The markup

Plain on purpose: headings, paragraphs, lists and the occasional table. Legal texts are read, printed and quoted, and every class Jack invents is one your theme has to override.

ElementClass or attribute
Wrapper<div class="jack-document" data-jack-document="privacy">
Each section<section id="…" class="jack-section">, with a stable anchor such as jack-imprint-liability-content
Contents list<nav class="jack-toc"> holding an <ol>
Last updated line<p class="jack-updated">

Methods

MethodReturns
craft.jack.render(handleOrType, options, siteId)The published document as HTML. Empty if there's no published text or the document is disabled
craft.jack.document(handleOrType, siteId)The document element, or null
craft.jack.documents(siteId)Every document
craft.jack.url(handleOrType, siteId)The document's URL, or null
craft.jack.toc(handleOrType, siteId)The published document's sections, as { anchor, title } pairs
craft.jack.lastUpdated(handleOrType, siteId)The publish date as a DateTime, or null if never published
craft.jack.profile(siteId)The profile's facts, as a flat array with dotted keys
craft.jack.services(category)Every service in the inventory that's in use, optionally for one category
craft.jack.consentRequired()Services whose legal basis is consent
craft.jack.cookies(language)Every cookie the inventory declares
craft.jack.libraryVersion()The date the bundled clause library was last reviewed

siteId is optional everywhere and defaults to the current site.

toc() and lastUpdated() read the published copy, so they can't disagree with the document they sit beside.

Links in the footer

<a href="{{ craft.jack.url('imprint') }}">Impressum</a>
<a href="{{ craft.jack.url('privacy') }}">Datenschutz</a>

A contents sidebar

<nav>
    <ol>
        {% for item in craft.jack.toc('privacy') %}
            <li><a href="#{{ item.anchor }}">{{ item.title }}</a></li>
        {% endfor %}
    </ol>
</nav>

{{ craft.jack.render('privacy') }}

{% set updated = craft.jack.lastUpdated('privacy') %}
{% if updated %}
    <p>Last updated {{ updated|date('long') }}</p>
{% endif %}

If you print your own date, pass lastUpdated: false to render() so it doesn't appear twice.

The company address, once

The facts are readable from Twig, so a footer address and the imprint can't disagree:

{% set facts = craft.jack.profile() %}
{{ facts['company.name'] }}<br>
{{ facts['company.street'] }}<br>
{{ facts['company.postalCode'] }} {{ facts['company.city'] }}

Keys are flat and dotted. Derived facts like company.address and controller.display are there too, already assembled. On a multi-site install, a site's overrides are applied.

Driving a consent banner

Jack isn't a consent banner. But if you have one, drive it from the inventory, and the banner and the privacy policy can't disagree about which services exist:

{% for service in craft.jack.consentRequired() %}
    <label>
        <input type="checkbox" name="consent[]" value="{{ service.serviceKey }}">
        {{ service.label() }} ({{ service.category() }})
    </label>
{% endfor %}

A cookie table for a cookie banner or your own cookie page:

<table>
    {% for cookie in craft.jack.cookies() %}
        <tr>
            <td><code>{{ cookie.name }}</code></td>
            <td>{{ cookie.service }}</td>
            <td>{{ cookie.duration }}</td>
            <td>{{ cookie.essential ? 'Essential' : 'Optional' }}</td>
        </tr>
    {% endfor %}
</table>

Each cookie has service, category, essential, name and duration. Labels and durations come in the current site's language unless you pass one.

Unlike documents, the inventory is live: services(), consentRequired() and cookies() reflect the inventory as it is now, not as of the last publish.

Reference tags

Inside a rich-text field, a reference tag drops a published document in place:

{jack:privacy:render}

The middle part is the document's handle. Like render(), it shows the published text only, and nothing for a disabled or unpublished document.

The Legal document field

Jack adds a Legal document field type, for relating entries to documents: a checkout page that links its terms, say.

{% set terms = entry.legalDocs.one() %}
{% if terms %}
    <a href="{{ terms.url }}">{{ terms.title }}</a>
{% endif %}