Skip to content
TYPO3Soul Design System

A project to copy

A documentation site is a directory of documents, one settings file and one workflow. Both files are printed here in full — take them, point the render step at your own documents, and what a landing page and a manual page need is the rest of this page. Search, both modes, the bar and the footer arrive with the theme and are asked for nowhere.

The commands in Publishing it, in CI are the ones this site is rendered with — PHP, Composer, Node, no make and no container — and the step after the render is the same file in both cases, out of the package.

What a project holds#

text
.github/workflows/publish.yml    the renderer, render, finish, publish
docs/
  guides.xml                     the project, the bar, the footer
  index.rst                      the landing page
  guide/                         the manual pages

No manifest among them: the renderer is built by the workflow, in a directory the runner discards. Both files are below.

The configuration#

docs/guides.xml
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!--
    The smallest file that renders with this theme, plus the bar and the
    footer, which are configuration because a page free to write its own is a
    site that disagrees with itself by the third page.

    The <extension> element is load-bearing: it is what makes a theme called
    "soul" exist, and without it the render stops on `Theme "soul" is not
    registered`. It also registers the syntax highlighter and the Markdown
    parser the theme is published with, so neither is named here.
-->
<guides xmlns="https://www.phpdoc.org/guides"
        input-format="rst"
        links_are_relative="true"
        theme="soul"
        default_code_language="text">
    <project title="Example Project" version="1.0" copyright="© 2026 Example Project"/>
    <extension class="TYPO3\Soul\GuidesTheme\DependencyInjection\SoulExtension">
        <brand>Example</brand>
        <product>Documentation</product>
        <!-- The handful of places this site has. Not the toctree: that is the
             rail's job. An href here is a document, written the way a :doc:
             reference is, and resolved per page. -->
        <navigation>
            <link href="/guide/index" label="Guide"/>
        </navigation>
        <!-- The columns are the toctree itself. What is configured is only
             what the tree cannot know. -->
        <footer>
            <note>An example, not a product.</note>
        </footer>
    </extension>
</guides>

Everything inside the theme's <extension> is optional; the element itself is not, because it is what makes a theme called soul exist. Written empty, the bar carries the project title and the footer carries the site's own sections. Configuration has each setting and what happens when it is left out.

The workflow#

.github/workflows/publish.yml
yaml
# Render the documentation and publish it to GitHub Pages.
#
# Two jobs, because they need different permissions: the first turns documents
# into a site and uploads it, the second is the only thing allowed to deploy.
#
# Nothing here is specific to any one project. Point the render step at your own
# documentation directory and this file is the whole build — the repository it
# runs on holds documents and this file, and no PHP manifest at all.
name: Documentation

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

# A second push to a branch replaces the first. Deployments queue instead —
# a half-replaced site is worse than a site one commit behind.
concurrency:
  group: docs-${{ github.ref }}
  cancel-in-progress: true

jobs:
  render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          coverage: none

      # The finishing step is one bundled file with no dependencies, but it is
      # Node, and the version is named rather than inherited.
      - uses: actions/setup-node@v4
        with:
          node-version: '24'

      # The renderer, built where it is used and thrown away with the runner.
      # A documentation project is documents; requiring the theme into the
      # repository itself would put a `composer.json` and a lock file next to
      # them that nothing else ever reads. The theme brings the renderer with
      # it, so this one `require` is the whole dependency.
      - name: The renderer
        env:
          RENDERER: ${{ runner.temp }}/renderer
        run: |
          mkdir -p "$RENDERER"
          cd "$RENDERER"
          composer init --no-interaction --name=example/documentation
          composer require --no-interaction --no-progress typo3/soul-guides-theme:dev-main

      # `--fail-on-error` turns a reference the renderer could not resolve into
      # a red build. Without it a broken link is a line in a log nobody reads.
      - name: Render the documents
        run: ${{ runner.temp }}/renderer/vendor/bin/guides docs --output=site -c docs --fail-on-error

      # Everything between a render and a site: the drop-in copied to the site
      # root, every element drawn so the pages read with no script, the search
      # index written, and a refusal to publish a reference that leaves the
      # output. The file sits inside the theme package and copies what is
      # beside it, so the `require` above brought it.
      - name: Finish the site
        run: node ${{ runner.temp }}/renderer/vendor/typo3/soul-guides-theme/resources/dist/soul-finish.js site

      # Pages serves this artifact as it stands, but a repository switched to
      # the branch-based build runs Jekyll, which drops every path starting
      # with an underscore — including `_search.json` and `_images/`.
      - run: touch site/.nojekyll

      - uses: actions/upload-pages-artifact@v3
        with:
          path: site

  publish:
    needs: render
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deploy.outputs.page_url }}
    concurrency:
      group: pages
      cancel-in-progress: false
    steps:
      - uses: actions/configure-pages@v5
      - id: deploy
        uses: actions/deploy-pages@v4

The renderer is built in the job that uses it and goes away with the runner — a handful of Composer lines, no manifest in the repository. Publishing it, in CI reads the rest of this file, including what it refuses to publish.

The two page shapes#

The landing page writes :layout: marketing at the top and is a run of full-bleed bands with no rail. Every other page writes no such field and is the manual shape, a column beside a rail. Both carry the same bar and the same footer, because a reader must never have to work out which site they are on; Directives has what each shape is made of.

docs/index.rst
text
:navigation-title: Home
:layout: marketing

===============
Example Project
===============

.. toctree::
   :titlesonly:
   :hidden:

   guide/index

.. hero:: /_images/workbench.png

   What this project is, in the sentence somebody arriving needs.

.. button-bar::

   .. button:: :doc:`guide/index`
      :icon: actions-download

   .. button:: The source
      :href: https://github.com/example/project
      :variant: secondary
      :rel: external

.. band:: What it holds
   :quiet:

.. grid::

   .. card:: Write a page
      :href: /guide/index
      :tag: Guide

      What a manual page is made of.

The hidden toctree is not a formality. It is what the rail, the breadcrumb and the footer columns are built from, and a landing page that lists its sections in prose but writes no tree is a site whose every other page has nothing to navigate with.

The rest of the shape follows from that opening: the hero makes the claim, the row of presses is the way on, and everything after a band belongs to it until the next one starts. Directives has each of them with its options and a rendered example.

Running it#

bash
composer require typo3/soul-guides-theme:dev-main   # in a directory of its own
vendor/bin/guides docs --output=site -c docs --fail-on-error
node vendor/typo3/soul-guides-theme/resources/dist/soul-finish.js site
php -S localhost:8000 -t site

The second command writes documents; the third is what turns them into a site. Installation says what that directory of its own is for, and Publishing it, in CI says what the workflow's jobs are for and what they refuse to publish.

Core markup, and what it becomes is the theme's answer to what the renderer already emits — admonitions, code, tabs, tables and a reference entry — with the markup that produces each one beside it.