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.
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 sitecomposer 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.
node vendor/typo3/soul-guides-theme/resources/dist/soul-finish.js <output-dir> [options]node vendor/typo3/soul-guides-theme/resources/dist/soul-finish.js <output-dir> [options]
What the renderer just wrote. Everything happens in place.
-
the output directoryrequired # -
- type
- string
What the renderer just wrote. Everything happens in place.
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.
-
--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.
The output already has them — a second project rendered under a root that was finished once already.
-
--no-drop-in# -
- type
- flag
The output already has them — a second project rendered under a root that was finished once already.
What the directory is called at the site root. The theme's <head> links
this name, so changing it means overriding a template.
-
--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.
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.
-
--search# -
- type
- string
- default
- "_search.json"
The index the bar fetches.
--no-searchwrites 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.
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#
# 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
# 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.
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:
php -S localhost:8000 -t sitephp -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.
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.