Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/verify-content.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
name: Verify course content

on:
pull_request:
paths:
- 'content/**'
- 'lib/content.ts'
- 'scripts/verify-content.mjs'
- '.github/workflows/verify-content.yml'
schedule:
# Weekly, Monday morning. The things this catches - a vendor moving a URL,
# an image tag disappearing, nextcloud-docker-dev changing under the setup
# module - drift over months, not hours. Running it more often would add
# nothing except load on other people's servers.
- cron: '17 6 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
offline:
name: Offline checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: node scripts/verify-content.mjs

online:
name: Online checks
# Never on pull requests: a dead third-party link must not block somebody's
# typo fix. This reports, it does not gate.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22

- name: Run online checks
id: verify
run: |
set +e
node scripts/verify-content.mjs --online > report.txt 2>&1
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
cat report.txt

- name: Report failures
if: steps.verify.outputs.exit_code != '0'
env:
GH_TOKEN: ${{ github.token }}
TITLE: 'Course content verification is failing'
run: |
BODY=$(printf 'The weekly content check found problems. Full output:\n\n```\n%s\n```\n\nRun locally with `npm run verify:content -- --online`.\n\n[Workflow run](%s/%s/actions/runs/%s)\n' \
"$(cat report.txt)" "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")
# One issue, kept up to date, rather than a new one every Monday.
EXISTING=$(gh issue list --state open --search "in:title \"$TITLE\"" --json number --jq '.[0].number')
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --body "$BODY"
else
gh issue create --title "$TITLE" --body "$BODY" --label documentation
fi
2 changes: 1 addition & 1 deletion content/php/beginner/1.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ If you're coming from tutorials written for NC32 or NC33, the development enviro

- [nextcloud-docker-dev repository](https://github.com/nextcloud/nextcloud-docker-dev) — source of truth for the Docker environment
- [Development environment — Nextcloud developer docs](https://docs.nextcloud.com/server/latest/developer_manual/getting_started/devenv.html)
- [occ command reference — Nextcloud admin docs](https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html)
- [occ command reference — Nextcloud admin docs](https://docs.nextcloud.com/server/{{nextcloudVersion}}/admin_manual/occ_command.html)

<!-- Cross-module links: re-enable once the target modules exist (see academy #16).
*Next: [Module 2 — App Anatomy](./php-beginner-m2-app-anatomy.md)*
Expand Down
2 changes: 1 addition & 1 deletion content/php/beginner/2.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ The `FileCopyrightText` line records who holds copyright and when. The `License-

For PHP, NC34 supports 8.2 (deprecated), 8.3, 8.4, and 8.5 (recommended). We set `min-version="8.3"` to avoid the deprecated 8.2 — there's no practical reason to support it for a new app. For future reference, the supported PHP versions for any Nextcloud release are listed on the [Nextcloud system requirements page](https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html).

For the Nextcloud version range, setting both `min-version="34"` and `max-version="34"` is intentional for now — it prevents your app from being enabled on instances older than NC34 where APIs may differ, and from being silently enabled on an NC35 instance where you haven't tested it yet. You'll update this as you verify compatibility.
For the Nextcloud version range, setting both `min-version="{{nextcloudVersion}}"` and `max-version="{{nextcloudVersion}}"` is intentional for now — it prevents your app from being enabled on instances older than NC34 where APIs may differ, and from being silently enabled on an NC35 instance where you haven't tested it yet. You'll update this as you verify compatibility.

**`<navigation>`** is what puts your app in the top navigation bar. The `<id>` must match your app ID. The `<icon>` points to a file in your `img/` directory — Nextcloud uses `img/app.svg` and `img/app-dark.svg` for light and dark mode. The `<route>` value `pinboard.page.index` is a route name — we'll define what this means in a moment. This is the simpler of two approaches — the other is registering programmatically via `INavigationManager` in your `Application.php`, which some complex apps (like Talk) use when they need more control. For Pinboard, the `info.xml` approach is correct.

Expand Down
2 changes: 1 addition & 1 deletion content/php/beginner/4.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ No `url`, no `title`, no `createdAt`. The request succeeds, the response looks l
Implementing `JsonSerializable` fixes this by telling `json_encode()` exactly what to output. Two details in the code above:

- **`implements \JsonSerializable`** — the interface declares a single method, `jsonSerialize()`, which returns whatever should be encoded in place of the object. Returning an array of your fields gives you full control over the shape of your API response, including omitting fields you don't want to expose.
- **`#[\Override]`** — added because `jsonSerialize()` is defined by the interface you just implemented. The attribute tells PHP to verify that: if you typo the name as `jsonSerialise()`, PHP raises a compile-time error instead of silently defining an unrelated method that never gets called. It requires PHP 8.3, which is Nextcloud 34's minimum anyway. This matches how server apps do it — see `Absence` in the DAV app or `VersionEntity` in Files Versions.
- **`#[\Override]`** — added because `jsonSerialize()` is defined by the interface you just implemented. The attribute tells PHP to verify that: if you typo the name as `jsonSerialise()`, PHP raises a compile-time error instead of silently defining an unrelated method that never gets called. It requires PHP 8.3, which is NC34's minimum anyway. This matches how server apps do it — see `Absence` in the DAV app or `VersionEntity` in Files Versions.

This is the standard pattern in Nextcloud apps: an explicit `jsonSerialize()` per entity, listing the fields the API returns. Note that it also decouples your API from your database schema — renaming a column doesn't have to change your JSON.

Expand Down
2 changes: 1 addition & 1 deletion content/php/beginner/8.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Each user's pins are private to them. Built as part of the Nextcloud Developer C
<repository>https://github.com/YOUR_USERNAME/pinboard-php</repository>
<dependencies>
<php min-version="8.3"/>
<nextcloud min-version="34" max-version="34"/>
<nextcloud min-version="{{nextcloudVersion}}" max-version="{{nextcloudVersion}}"/>
</dependencies>
<navigations>
<navigation>
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"verify:content": "node scripts/verify-content.mjs"
},
"dependencies": {
"next": "16.2.12",
Expand Down
Loading
Loading