Skip to content
TYPO3Soul Design System

Publishing it, in CI

A directory of documents becomes the site this manual is by running the commands below: one builds the renderer, one writes the documents, one turns what was written into a site. The workflow after them is the same run with a checkout in front and a deploy behind — the commands this site is rendered with, and the last is a file out of the package rather than a script you are asked to write.

bash
composer require typo3/soul-guides-theme:dev-main
vendor/bin/guides docs --output=site -c docs --fail-on-error
node vendor/typo3/soul-guides-theme/resources/dist/soul-finish.js site

The first runs in a directory of its own — a documentation repository holds documents, not a PHP manifest. Installation says what that directory is and why it is thrown away.

What the renderer does not do#

The second command writes documents and stops there. It knows nothing about what a page in this theme still needs, and each of these is something a project finds out about the hard way:

  • the drop-in — the stylesheet, the script and the faces — standing at the site root, copied whole;
  • every element on the page drawn ahead of the browser, or a reader with no script gets an empty box where a card belongs;
  • _search.json, because the field in the bar fetches an index that nothing in the render writes.

--fail-on-error is the renderer's own half of the safety net: a reference it could not resolve becomes a red build rather than a line in a log. It says nothing about the references a theme or a copy step introduced, which is the third command's job.

The finishing step#

soul-finish.js ships inside the drop-in, which ships inside the theme, so the require above is what put it there. It needs nothing installed of its own — one bundled file for the Node that is on every CI image already.

bash
node vendor/typo3/soul-guides-theme/resources/dist/soul-finish.js <output-dir> [options]
the output directory required #
type
string

What the renderer just wrote. Everything happens in place.

--drop-in #
type
string
default
the directory the script is in

Where the stylesheets are. The default is the answer for anybody who copied the drop-in as a directory, because the script is in it.

--no-drop-in #
type
flag

The output already has them — a second project rendered under a root that was finished once already.

--styles #
type
string
default
"styles"

What the directory is called at the site root. The theme's <head> links this name, so changing it means overriding a template.

type
string
default
"_search.json"

The index the bar fetches. --no-search writes none, and the field then opens, finds nothing and says so — a poor answer to give somebody on every page of a site.

It exits non-zero on the one failure that is otherwise silent: a reference that does not resolve inside the output. What gets published is that directory alone, so a link that worked during the build because the build happened in a checkout resolves to nothing on the server, and arrives as a page with no stylesheet rather than as an error anybody reads.

This is the same code make guides runs in this repository — scripts/lib/site.ts, bundled. A documented step that drifts from the one we run is worse than no documented step, so there is one implementation and both callers share 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 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

What is worth reading in it rather than copying:

One checkout, and a renderer built beside it. The repository holds documents and this file — no manifest, no lock file. The drop-in and the finishing step are not Composer packages and cannot be, a stylesheet being no PHP dependency, so the theme carries them: one require into a directory under runner.temp brings the command, the templates and the stylesheets at once, and the runner throws all of it away again.

Ask for a tag rather than dev-main as soon as there is one. A site rebuilt against a moving branch is a site whose look can change on a commit nobody in your repository made — and the next build after that one is the one that has to be explained.

The Node version is named. Inherited, it is whatever the runner image happens to ship this month.

``.nojekyll``. Pages serves an uploaded artifact as it stands, but a repository ever switched back to the branch-based build runs Jekyll over it, and Jekyll drops every path beginning with an underscore — _search.json and _images/ among them.

Two jobs, not one. Deploying is the only step that writes anything outside the run, so it is the only one holding pages: write, and it waits for the render. Deployments queue rather than cancel: a half-replaced site is worse than a site one commit behind.

Once, in the settings#

GitHub Pages has to be told to take its content from Actions — Settings → Pages → Source → GitHub Actions. Left on a branch, the workflow runs green, uploads an artifact and publishes nothing, which looks exactly like a build that worked.

Without GitHub#

Nothing above is specific to Actions. The commands at the top are the build; what a different runner needs is PHP with Composer, Node, and somewhere to put a directory of static files. There is no server-side anything in the output:

bash
php -S localhost:8000 -t site

That is also how to look at it while writing — the page served there is the page that gets published, mode switch and search included.

Several projects, one root#

A render is one guides.xml, one CLI call and one --output, and nothing stops a build from doing that several times. Finish each output separately: styles/ is resolved from a site's own root, so each root needs its own drop-in, and each gets its own index.

Give every project a root of its own rather than a directory inside another one. A page one level below somebody else's root does not resolve its assets the way a published page does, and what is published is then the whole of what was rendered there — with nothing to remember to take back out. This site is the manual, and beside it the theme's control surface, which is built on every run and published on none.

Where a project does keep something inside the published root, a name beginning with an underscore is left out of the search index by the finishing step.

A project to copy is the project the workflow above builds, file by file. Installation is the same ground for somebody rendering locally for the first time.