Build a Nextcloud app on the Conduction stack — Part 8: Document and showcase
Stand up the documentation site for the PetStore app you've built across Parts 1–7. Hosted from your Codeberg or GitHub repo via the central documentation workflow, illustrated with token-built screen mocks, and walked through with Playwright-captured screenshots. Every push to development rebuilds and republishes.
A 45-minute tutorial that gives your PetStore app a documentation site. Hosted from your own Codeberg or GitHub repo via the central documentation workflow, illustrated with abstract screen mocks that compose from design tokens, and brought to life with real screenshots that Playwright captures from your running app. By the end, every push to development rebuilds and republishes.
Part 8 assumes you've finished Part 7: The nc-vue component library. The screen mocks and capture flow build on the PetStore app from Parts 1–7.
Three of the steps below run Conduction skills (/journeydoc-init, /journeydoc-instrument, /journeydoc-add-story). Skills are short, declarative Claude Code workflows. If you've never run one, take Part 1 of the Claude Skills series first (10 minutes).
In the examples we use petstore as the app slug. Replace with your own slug throughout. Commands assume you have a Claude Code session open in the repo root.
Step 1: Bootstrap the docs scaffold
In your Claude Code session, run:
/journeydoc-init petstore
The skill walks through five short prompts:
- Confirms the app slug (
petstore). - Confirms the docs domain (default
petstore.conduction.nl). - Asks for your user-track stories, one per line. For PetStore: Open the app for the first time, Add a pet, Place an order, Search the catalogue.
- Asks for admin-track stories, or skip if PetStore's admin UI is light.
- Audits the repo for the four canonical brown-field migrations (screenshots dir, relative image refs, stale Dutch locale, old docs domain) and applies fixes where needed.
When it finishes you have:
docs/with a Docusaurus config that importscreateConfigfrom@conduction/docusaurus-presetdocs/tutorials/user/01-first-launch.md,02-add-pet.md,03-place-order.md,04-search-catalogue.mdas skeleton pages, one per storytests/e2e/docs-screenshots.spec.tswith empty test stubs that match the markdown filenamesplaywright.config.tsextended with adocs-capturePlaywright projectdocs/static/CNAMEset topetstore.conduction.nl- A pull request opened against
developmentwith the diff
Verify:
ls docs/tutorials/user/
# 01-first-launch.md 02-add-pet.md 03-place-order.md 04-search-catalogue.md
cat docs/static/CNAME
# petstore.conduction.nl
Merge the PR before continuing. Part 8 builds on the scaffold.
Step 2: Wire the central documentation workflow
The build and deploy are shared across the Conduction fleet via a reusable workflow in Conduction/.github (Codeberg) or ConductionNL/.github (GitHub). Your per-repo wrapper declares three things: the Cloudflare project name, the docs source folder, and the secrets to forward.
- Codeberg (Forgejo Actions)
- GitHub Actions
Create .forgejo/workflows/documentation.yml:
name: Publish docs
on:
push:
branches: [documentation, main, development]
pull_request:
branches: [documentation, main]
workflow_dispatch:
schedule:
- cron: "0 4 * * *"
jobs:
docs:
uses: https://codeberg.org/Conduction/.github/.forgejo/workflows/documentation.yml@main
with:
cf-project-name: petstore-docs
source-folder: docs
secrets:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
The central workflow runs on codeberg-small runners, builds Docusaurus, then deploys via wrangler deploy to Cloudflare Workers Static Assets.
Create .github/workflows/documentation.yml:
name: Publish docs
on:
push:
branches: [documentation, main, development]
pull_request:
branches: [documentation, main]
workflow_dispatch:
schedule:
- cron: "0 4 * * *"
jobs:
docs:
uses: ConductionNL/.github/.github/workflows/documentation.yml@main
with:
cf-project-name: petstore-docs
source-folder: docs
secrets:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
The GitHub variant uses the same Cloudflare Workers Static Assets deploy step on the ubuntu-latest runner.
Commit and push the wrapper:
git add .forgejo/workflows/documentation.yml
git commit -m "ci(docs): wire central docs workflow"
git push origin development
Verify by opening the Actions tab on your forge. The workflow appears in the list, queued or already running.
Step 3: Set the Cloudflare secrets + trigger the first deploy
Two org-level secrets unlock the deploy:
CF_API_TOKEN: create at dash.cloudflare.com/profile/api-tokens. Grant Account: Workers Scripts: Edit and Zone: DNS: Edit. Set a 1-day TTL while you test, then rotate.CF_ACCOUNT_ID: copy from the Cloudflare dashboard sidebar.
- Codeberg
- GitHub
Set at the org-secrets screen:
https://codeberg.org/org/Conduction/-/secrets/actions
Add three secrets:
CF_API_TOKEN = <your token>
CF_ACCOUNT_ID = <your account id>
CODEBERG_TOKEN = <a forge PAT, used to authenticate npm prebuild requests to the Codeberg API>
Trigger the workflow manually so you don't have to wait for the next push:
curl -sS -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" \
-d '{"ref":"development"}' \
"https://codeberg.org/api/v1/repos/Conduction/petstore/actions/workflows/documentation.yml/dispatches"
Set at the org-secrets screen:
https://github.com/organizations/ConductionNL/settings/secrets/actions
Add two secrets:
CF_API_TOKEN = <your token>
CF_ACCOUNT_ID = <your account id>
Trigger the workflow manually:
gh workflow run documentation.yml -R ConductionNL/petstore --ref development
Verify by watching the run finish. The build step prints the upload count, the deploy step prints a URL:
Uploaded petstore-docs (12.4 sec)
Deployed petstore-docs triggers (1.9 sec)
https://petstore-docs.conduction-a2d.workers.dev
Open that URL. You see your Docusaurus site live. Binding the custom domain petstore.conduction.nl to the Worker is a one-time API call described in the central workflow's README.
Step 4: Embed an <AppMock> for context
@conduction/docusaurus-preset ships token-built abstract previews of every fleet app. They paint with design tokens (no images, no real text) so they stay sharp at any zoom and never go stale when the real UI moves. Use them whenever a docs page needs to show an app without taking a screenshot.
Open docs/intro.md (created by /journeydoc-init) and add the import plus a mock:
import { AppMock } from '@conduction/docusaurus-preset/components';
# PetStore on the Conduction stack
PetStore is a typed-data app built on OpenRegister. The dashboard
composes from the same widget chassis you see across the fleet, here
illustrated with the LaunchPad reference:
<AppMock app="launchpad" caption />
Verify by running the docs locally:
cd docs
npm run start
Open http://localhost:3000/docs/intro. The LaunchPad mock renders with its caption, scaling cleanly at any zoom level.
The app prop expects one of the predefined fleet variants (launchpad, openregister, openconnector, procest, decidesk, docudesk, larpingapp, nldesign, openwoo, pipelinq, softwarecatalog, zaakafhandelapp, opencatalogi). To ship an abstract mock of your PetStore dashboard, add PetStoreMock.jsx under @conduction/docusaurus-preset/src/components/AppMock/variants/ and register it in the VARIANTS map. Follow the design-system repo's SKILL.md for the "app glyphs" and "status palette" conventions: one orange accent per variant, no images, design tokens only.
Step 5: Compose a deeper scene with <WidgetMock> and <SidebarMock>
<AppMock> shows the whole app frame. <WidgetMock> shows one widget, typically a dashboard tile. <SidebarMock> shows a Nextcloud sidebar opened over an app (e.g. the OpenRegister "object detail" sidebar over a list).
Add a new page docs/architecture/dashboards.md:
import {
AppMock,
WidgetMock,
SidebarMock,
} from '@conduction/docusaurus-preset/components';
# Dashboard widgets
PetStore's home dashboard ships three KPI widgets and an activity feed:
<div className="hex-grid">
<WidgetMock kind="kpi-tile" caption />
<WidgetMock kind="activity-feed" caption />
<WidgetMock kind="status-rollup" caption />
</div>
Each row in the activity feed opens a detail sidebar in place, using
the same chrome the rest of the fleet ships:
<AppMock app="launchpad" sidebar={<SidebarMock kind="register-detail" />} />
Verify by reloading http://localhost:3000/docs/architecture/dashboards. Three widget tiles render in a hex grid, and the AppMock below renders with the sidebar overlay.
The result is a docs page that shows the app pattern without a single screenshot. When the real UI changes, the mock does not need updating: it speaks in shapes, not pixels.
Step 6: Instrument your Vue components for capture
Before the capture spec can find UI elements reliably, the relevant Vue components need stable data-testid attributes. The /journeydoc-instrument skill audits one component at a time and proposes the additions.
Run it on the form for adding a pet:
/journeydoc-instrument src/components/PetForm.vue
The skill:
- Reads the component.
- Identifies the elements the capture spec will need (form fields, submit button, success and error notices).
- Proposes
data-testidvalues following the<component>.<element>convention. - Asks you to confirm before writing the edits.
- Runs Vitest after the edit to make sure no existing test breaks.
Verify:
grep -n 'data-testid' src/components/PetForm.vue
# 23: <input data-testid="pet-form.name" v-model="form.name" />
# 24: <input data-testid="pet-form.category" v-model="form.category" />
# 31: <button data-testid="pet-form.submit" @click="save">Save</button>
The shared chrome (lists, modals, headers) is already instrumented in @conduction/nextcloud-vue. Run /journeydoc-instrument once per app-specific component, typically 3–5 files for a small app.
Step 7: Add a tutorial story with /journeydoc-add-story
/journeydoc-init left the markdown skeletons empty. /journeydoc-add-story fleshes them out one at a time, dropping a markdown page plus a capture-spec test block, with the sidebar position auto-incremented.
/journeydoc-add-story petstore user "Add a pet"
The skill asks three short questions:
| Question | Example answer |
|---|---|
| Goal, in one sentence, what does the user accomplish? | Register a new pet under their customer profile. |
| Prerequisites, what must already be true? | A pet schema exists. The user is signed in. |
| Steps, paste a numbered list of UI clicks. | 1. Open the PetStore app. 2. Click "Add pet". 3. Fill name + category. 4. Click Save. 5. See the new row in the list. |
It then writes two files:
docs/tutorials/user/02-add-pet.md: the markdown page with five numbered steps, each with an image reference and aTODOHTML comment for verification and common issues- A new
test('U02 add-pet')block at the end oftests/e2e/docs-screenshots.spec.ts, with one TODO per numbered step
Verify:
ls docs/tutorials/user/
# 01-first-launch.md 02-add-pet.md 03-place-order.md 04-search-catalogue.md
grep -A1 "U02 add-pet" tests/e2e/docs-screenshots.spec.ts
# test('U02 add-pet: REPLACE WITH ACTUAL FLOW', async ({ page }) => {
# // docs/tutorials/user/02-add-pet.md
Step 8: Run the capture spec to populate the screenshots
Open tests/e2e/docs-screenshots.spec.ts and fill the U02 block. Each numbered TODO comment maps 1:1 to a step in the markdown. Use the data-testid attributes from Step 6 and the three helpers the template ships (dismissOverlays, go, shoot):
test('U02 add-pet: Add a pet to the store', async ({ page }) => {
await dismissOverlays(page);
await go(page, '/apps/petstore');
await shoot(page, 'user', '02-step-1.png');
await page.getByTestId('pet-list.add').click();
await shoot(page, 'user', '02-step-2.png');
await page.getByTestId('pet-form.name').fill('Mochi');
await page.getByTestId('pet-form.category').fill('Cats');
await shoot(page, 'user', '02-step-3.png');
await page.getByTestId('pet-form.submit').click();
await page.getByTestId('pet-list.row').first().waitFor();
await shoot(page, 'user', '02-step-4.png');
});
Run only the user-track tests so you don't trigger the full regression suite:
npm run test:e2e:docs -- --grep "U02"
Playwright launches a headless Chromium, runs the test, and writes the PNGs to docs/static/screenshots/tutorials/user/:
ls docs/static/screenshots/tutorials/user/02-*
# 02-step-1.png 02-step-2.png 02-step-3.png 02-step-4.png
Verify by opening http://localhost:3000/docs/tutorials/user/add-pet. Each numbered step now renders its captured screenshot. The full capture run for all four stories takes about 90 seconds on a laptop.
What you ship at the end of Part 8
petstore.conduction.nlserving your Docusaurus site after the custom-domain bind- Intro and architecture pages illustrated with
<AppMock>and<WidgetMock>, scaling cleanly without screenshots - Tutorials under
/docs/tutorials/user/with real Playwright-captured screenshots - Every push to
development,main, ordocumentationredeploys the site automatically
Where to go from here
The journeydoc capture is the highest-leverage doc surface. Run /journeydoc-add-story once a sprint and your tutorials grow alongside the code. When the UI shifts, re-run npm run test:e2e:docs and the screenshots refresh in one pass.
You have now finished the eight-part PetStore series: scaffolded, modelled, integrated, mastered the nc-vue library, and shipped a documentation site that grows with the code. The natural next move is publishing the app to the Nextcloud app store: signing the release, the appstoreapp:create-app flow, and shipping under your own brand. Until that part lands, the Nextcloud app store docs describe the upload protocol.