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 stand here in full. Take them and point the render step at your own documents. The rest of this page is what a landing page and a manual page need. Search, both modes, the bar and the footer arrive with the theme.

The commands in Publishing it, in CI are the ones this site renders with: PHP, Composer, Node, no make and no container. 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 workflow builds the renderer 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 makes a theme called soul exist. Empty, the bar carries the project title and the footer carries the site's own sections. Configuration has each setting and what happens without it.

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 one that can deploy.
#
# Nothing here is specific to 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.
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@v7

      - 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 has a name instead of an inheritance.
      - uses: actions/setup-node@v7
        with:
          node-version: '24'

      # The renderer, built where it is in use and thrown away with the runner.
      # A documentation project is documents. The theme in the repository
      # itself puts a `composer.json` and a lock file next to them that nothing
      # else 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

      # `--fail-on-error` turns a reference with no target 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 on the
      # branch-based build runs Jekyll, which drops every path with a leading
      # underscore, `_search.json` and `_images/` included.
      - run: touch site/.nojekyll

      # The upload leaves a dotfile out unless told otherwise, and the marker
      # above is one.
      - uses: actions/upload-pages-artifact@v5
        with:
          path: site
          include-hidden-files: true

  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@v6
      - id: deploy
        uses: actions/deploy-pages@v5

The job that uses the renderer builds it, and it goes away with the runner: a few Composer lines, no manifest in the repository. Publishing it, in CI reads the rest of this file, and 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 the parts of each shape.

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. The rail, the breadcrumb and the footer columns come from it. A landing page that lists its sections in prose and writes no tree is a site whose every other page has no navigation.

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

Run it#

bash
composer require typo3/soul-guides-theme   # 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 turns them into a site. Installation says what that directory of its own is for. 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.