---
title: "A project to copy"
description: "A documentation site is a directory of documents, one settings file and one workflow."
canonical: example.html
navigation-title: "A project to copy"
---

<a id="a-project-to-copy"></a>

# A project to copy

- [What a project holds](#what-a-project-holds)
- [The configuration](#the-configuration)
- [The workflow](#the-workflow)
- [The two page shapes](#the-two-page-shapes)
- [Run it](#run-it)

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](publishing.md) 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.

<a id="what-a-project-holds"></a>

## 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.

<a id="the-configuration"></a>

## 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](configuration.md) has each setting and what happens without
it.

<a id="the-workflow"></a>

## 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](publishing.md) reads
the rest of this file, and what it refuses to publish.

<a id="the-two-page-shapes"></a>

## 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](directives.md) 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](directives.md) has each of them with its
options and a rendered example.

<a id="run-it"></a>

## 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](installation.md) says what that directory of its own is for.
[Publishing it, in CI](publishing.md) says what the workflow's jobs are for and what they refuse
to publish.

> [!NOTE]
> [Core markup, and what it becomes](markup.md) 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.
