Browse the documentation

Guides

A documentation section on your site

Publish a folder of Markdown as a documentation section, from a deploy or from the admin.

Thallo can publish a folder of Markdown as a documentation section: a sidebar of sections, the page, an "On this page" outline, previous and next links, and an "Edit this page" link. The Markdown stays in git as the source of truth; a deploy imports it. thallo.dev's own docs are published this way, and nothing about it is specific to Thallo — a product, an agency handbook or an open-source project can use it as it stands.

In three commands

bash
# 1. Once: make the content type a docs section needs, and let the site list it.
php glueful thallo:docs:setup

# 2. See what an import would do. Nothing is written.
php glueful thallo:import:markdown docs --type=docs --exclude=internal --dry-run

# 3. Import and publish. Run this on every deploy.
php glueful thallo:import:markdown docs --type=docs --exclude=internal --publish \
    --edit-base=https://github.com/you/your-repo/edit/main/docs

Your pages are at /docs/{page} and /docs is their index. The importers capability has to be on (Extensions › Capabilities).

Step 1 makes five sections, in this sidebar order: getting-started, concepts, guides, reference, operations. Your folders are matched against these names, so a folder called how-to is in none of them and its pages arrive with no section at all. Name your folders after the sections you want, choose your own with --sections=start,guides,reference, or change them later under Settings › Content types.

Without a shell: from the admin

Everything above can be done under Settings › Import / Export, for a site whose owner has the admin and not a terminal.

  1. Choose the adapter Markdown folder (.zip). On a site with no docs section the page offers Set up documentation: one click does what thallo:docs:setup does, with the same five sections. To use your own, change them afterwards under Settings › Content types.
  2. Zip your docs folder and choose the file. It does not matter whether the zip holds the folder or only what is inside it: a folder that wraps every page is set aside, so a page has the same path whichever way it was packed, and the same path the command would give it.
  3. Name any folders to leave out, give the address for “Edit this page” links if you want them, and decide whether the pages are published.
  4. Run it as a dry run first. The job's Report says what each file would become, which links lead nowhere, and which pages no longer have a file. Then run it as a commit.

It is the same import, with the same rules: upload the folder again whenever the files change, only what changed is written, and nothing is deleted. Pages are written as the person who is signed in, so on a site with the review workflow they are published only if that person may publish without review; otherwise they are saved as drafts and the report says so.

An upload is unpacked with care: only Markdown files are read out of the archive, a name that points outside the import refuses the whole archive, and an archive that unpacks to more than 50 MB or holds more than 2,000 pages is refused. It needs PHP's zip extension.

Imports are background jobs on the import-export queue. They run while a queue worker that was given that queue is running, or when a cron line drains it (the scheduler and the queue). Until then the job reads "queued".

The content type

thallo:docs:setup makes an ordinary content type; you can open it under Settings › Content types like any other.

Field What it holds
title The page's title.
summary One line, shown under the title and on the index.
section Which sidebar group the page is in. The list of sections, in sidebar order, is this field's options.
order The page's place within its section.
body The Markdown, as written. A plain text field: the theme renders it.
source_path The file the page came from. How a later import finds the page again.
edit_url Where a reader can propose a change.

The type's slug is the URL. --type=handbook gives you /handbook. Choose your own sections with --sections=start,guides,reference. Running the command again changes nothing, and it never rewrites a type that already exists.

How a file becomes a page

Front matter is optional. A folder written for GitHub imports as it stands.

text
---
title: Installing
slug: install
section: getting-started
order: 1
summary: Get it running in a few minutes.
---
From the front matter Otherwise
URL slug The file's name, without an NN- prefix. A README.md or index.md is its folder.
Title title The first # heading, which is then taken out of the body. Failing that, the name.
Section section The top folder, when it is one of the type's sections — exactly, so Guides and 03-guides are not guides. A folder that matches none leaves the page with no section, and the sidebar holds those pages in one unnamed group.
Order order The NN- prefix of the file's name.
Summary summary or description None.

draft: true or publish: false keeps a file out of the import. --exclude=<folder> keeps a folder out, and can be repeated. Hidden files and folders are never read.

Links between files become links between pages. [Upgrading](../upgrading.md#steps) is rewritten to /docs/upgrading#steps. A link to a file that is not in the import is left as written and reported. Code, fenced or indented, is never rewritten.

Run it again and again

The import is built for a deploy script.

  • A file lands on the page it made last time, found by its URL or, failing that, by its source_path. So a page whose slug changes keeps its entry, and its old URL redirects.
  • Only pages that changed are written. An unchanged page is left alone, so deploys do not pile up versions.
  • Nothing is ever deleted. A page whose file is gone is reported as gone; remove it in the admin when you mean to.
  • One file's failure is that file's. The rest still import, and the command exits non-zero so a deploy stops.
  • If your site has the review workflow on, a plain publish is gated. The page is saved as a draft and the report says so. Pass --actor=<user uuid> for a user allowed to bypass review.

What Markdown can do

GitHub-flavoured Markdown: tables, task lists, strikethrough, autolinks, fenced code. Every heading gets a stable id and a copyable link, and the h2/h3 outline becomes "On this page". A code fence is rendered by the theme's own code block, with its language label and copy button; a bash fence draws the $ prompt without it being copied.

Raw HTML in a source file is stripped, and javascript: and data: links are refused. A docs page can never carry markup or script of its own, whoever wrote the file.

Not yet: images that travel with the import (link to an uploaded image or an absolute URL for now), and syntax colouring inside code listings.

Turn on Settings › General › Content search and run php glueful search:reindex once. The sidebar and the index then carry a search box: results as you type, scoped to your docs, walked with the arrow keys, opened with Enter, focused with /. It uses the site's own PostgreSQL database — there is nothing to install — or Meilisearch if you have configured one (packages/thallo-search/README.md). Each import keeps the index in step. A visitor without JavaScript sees no search box rather than one that does nothing.

Making it yours

The pages are rendered by two templates in the default theme, entry/docs.twig and listing/docs.twig, and styled by assets/docs.css. Neither template names the type, so for a type called handbook copy them to entry/handbook.twig and listing/handbook.twig. Three template functions do the work and are yours to use anywhere (THEMING.md §4.2): markdown(text), markdown_toc(text) and entry_tree(type). The search box is the partial _docs_search.twig, shown where search_enabled().