Skip to content
Merged
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
107 changes: 107 additions & 0 deletions .claude/skills/add-doc-page/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: add-doc-page
description: Add a new documentation page to this repo and register it so it
renders on amezmo.com. Use when creating any new .md page, including API
endpoint pages. Covers the section index entry, reciprocal backlinks, the
nav registration in the dashboard app, and the lint checks.
---

# Add a Documentation Page

Adding a page is a two-repo change. The Markdown lives here, but the page
only renders on amezmo.com after it is registered in the dashboard app at
`~/Source/dashboard.amezmo.com`. A page that exists only in this repo is
unreachable: the menu, the prev/next nav, the sitemap, and the JSON-LD dates
all come from the dashboard's hand-maintained tree, not from file discovery.

## 1. Write the Page

Follow the conventions in [CLAUDE.md](../../../CLAUDE.md): a single `#` H1,
no front matter, relative `.md`-suffixed internal links, GitHub alert
callouts. API endpoint pages follow the fixed layout in the "API reference
pages" section of CLAUDE.md.

An optional lead paragraph directly under the H1 can carry `{.lead}` on its
own line above the paragraph (see `cron/schedules.md`). The site styles it
as the page intro.

## 2. Link It from Its Section Index

Pages are not auto-discovered. Add a `###` entry for the page under the
section index's `## In This Section` run, with a one-or-two sentence excerpt
written from the page's content. The heading text is plain, never a link;
the link goes in the excerpt prose. Keep the entries in reading order: this
order should match the menu order you register in step 4.

For top-level sections, check whether the root `index.md` or
`how-to-guides/index.md` should also point at the page.

## 3. Make Content Links Reciprocal

Every content page you link from the new page should link back with a
contextual link. Index pages are exempt. Record deliberate one-ways in
`.github/backlink-ignore.txt` as `SRC -> DST` with a comment.

## 4. Register the Page in the Dashboard App

Edit `app/Http/Docs/Services/DocumentationMenuService.php` in
`~/Source/dashboard.amezmo.com`. Find the section's
`DocumentationCategory` and add a child entry in the same position the page
has in the section index (menu order is also prev/next reading order).

The constructor is:

```php
// app/Http/Docs/Models/DocumentationCategory.php
public function __construct(
string $name, // menu label; also the slug source
?string $description, // meta description for the page
?string $parent, // parent category name, e.g. 'Cron'
?array $children, // [] for a leaf page
bool $lineBreak, // false for leaf pages
$slug, // only when the filename differs from the name
?string $title // <title> override when it differs from name
)
```

The slug defaults to `Str::slug(strtolower($name))`, so `'Schedules'`
resolves to `schedules`. Pass an explicit slug when the filename does not
match the name (`'Create a deployment'` renders `post.md`, so it passes
`'post'`). The slug plus the parent chain must equal the file path in this
repo without `.md`: the `Cron` category's child with slug `schedules` renders
`cron/schedules.md`.

Write the description with care: it becomes the page's meta description,
and the search engine snippet.

## 5. Page Dates

`app/Http/Docs/Generated/DocsPageDates.php` maps each page to its git
commit dates for the Article JSON-LD. It is generated by
`php artisan sitemap:generate` through the regenerate-sitemap workflow. Do
not edit it by hand. `DocsPageDatesTest` fails the dashboard CI when a menu
page has no dates, so the docs commit needs to land and the sitemap needs
regenerating after the menu entry is added.

## 6. Validate

Run from this repo's root:

```bash
$ python3 .github/scripts/check_links.py
$ python3 .github/scripts/check_backlinks.py
$ python3 .github/scripts/check_image_paths.py
$ codespell --config .codespellrc
```

## Checklist

- [ ] Page written per CLAUDE.md conventions
- [ ] `###` excerpt entry in the section's `index.md`
- [ ] Root `index.md` / `how-to-guides/index.md` updated if relevant
- [ ] Backlinks reciprocal, or the one-way recorded in
`.github/backlink-ignore.txt`
- [ ] `DocumentationCategory` entry added in `DocumentationMenuService.php`,
in the same order as the section index
- [ ] Sitemap regenerated after both changes land (page dates)
- [ ] Lint scripts pass
13 changes: 12 additions & 1 deletion .github/backlink-ignore.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@
/docs/api/changelog -> /docs/api/deployments/post
/docs/api/changelog -> /docs/api/environments/get-environment
/docs/api/changelog -> /docs/api/environments/update-environment
/docs/api/changelog -> /docs/api/cron/create-cron-entry
/docs/api/changelog -> /docs/api/cron/get-cron-entry
/docs/api/changelog -> /docs/api/cron/list-cron-entries
/docs/api/changelog -> /docs/api/cron/update-cron-entry
/docs/api/changelog -> /docs/api/errors

# API endpoints cross-link the lookup endpoints they depend on (see the API
# convention in CLAUDE.md); those lookups don't link back to every caller.
# convention in CLAUDE.md); those lookups don't link back to every caller. The
# errors reference is cited the same way: by everything, citing nothing back.
/docs/api/endpoints -> /docs/api/errors
/docs/api/cron/create-cron-entry -> /docs/api/errors
/docs/api/cron/update-cron-entry -> /docs/api/errors
/docs/api/environments/update-environment -> /docs/api/environments/list-environments
/docs/api/environments/update-environment -> /docs/api/instances/list-instance-types
/docs/api/instances/create-instance -> /docs/api/instances/list-instance-types
Expand Down Expand Up @@ -40,6 +49,7 @@
/docs/billing/payment-methods -> /docs/billing/advanced-pay
/docs/php/versions -> /docs/how-to-guides/changing-php-versions
/docs/php/composer -> /docs/instances/ssh
/docs/cron/custom-expressions -> /docs/instances/ssh

# Nginx location-blocks references the domain routing/rules and http-auth pages;
# those describe separate features and don't need to point back.
Expand All @@ -65,6 +75,7 @@
/docs/configuration/logging -> /docs/deployments/directories
/docs/npm/package-caching -> /docs/deployments/directories
/docs/workers/reloading -> /docs/deployments/directories
/docs/cron/editing-entries -> /docs/deployments/directories
/docs/deployments/directories -> /docs/deployments/hooks/deploy-success
/docs/configuration/dotenv -> /docs/deployments/releases
/docs/deployments/hooks/before-pull -> /docs/deployments/releases
Expand Down
9 changes: 7 additions & 2 deletions .github/scripts/check_backlinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

Excluded:
- index/landing pages (index.md) -- their nav is one-directional by design
- non-published files (CLAUDE.md, anything under .github/)
- non-published files (CLAUDE.md, README.md, anything under .github/)
- pairs listed in .github/backlink-ignore.txt (known-intentional one-ways)

Exit code is always 0.
Expand All @@ -28,14 +28,19 @@
EXTERNAL = ("http://", "https://", "mailto:", "//")


# Repo meta files, not published doc pages: skipped as both link sources
# and link targets.
META_FILES = {"CLAUDE.md", "README.md"}


def doc_files(root):
files = set()
for dirpath, _dirs, names in os.walk(root):
parts = dirpath.split(os.sep)
if ".git" in parts or ".github" in parts:
continue
for name in names:
if name.endswith(".md") and name != "CLAUDE.md":
if name.endswith(".md") and name not in META_FILES:
files.add(os.path.relpath(os.path.join(dirpath, name), root))
return files

Expand Down
27 changes: 26 additions & 1 deletion .github/scripts/check_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,24 @@
EXTERNAL = ("http://", "https://", "mailto:", "//")


# Repo meta files, not published doc pages: excluded as link sources and
# link targets for the doc-page check.
META_FILES = {"CLAUDE.md", "README.md"}

# Meta files whose own outgoing links are still checked. They may point at
# any existing repo file, not only published .md pages. CLAUDE.md is not
# here: its links are illustrative examples, not real paths.
CHECKED_META_FILES = {"README.md"}


def doc_files(root):
files = set()
for dirpath, _dirs, names in os.walk(root):
parts = dirpath.split(os.sep)
if ".git" in parts or ".github" in parts:
continue
for name in names:
if name.endswith(".md") and name != "CLAUDE.md":
if name.endswith(".md") and name not in META_FILES:
files.add(os.path.relpath(os.path.join(dirpath, name), root))
return files

Expand All @@ -53,6 +63,21 @@ def main():
if not path.endswith(".md") or target not in files:
broken.append((rel, lineno, url))

for rel in sorted(CHECKED_META_FILES):
full = os.path.join(ROOT, rel)
if not os.path.isfile(full):
continue
with open(full, encoding="utf-8") as handle:
for lineno, line in enumerate(handle, 1):
for match in LINK_RE.finditer(line):
url = match.group(1)
if not is_internal(url):
continue
path = url.split("#", 1)[0]
target = os.path.normpath(os.path.join(os.path.dirname(rel), path))
if path and not os.path.isfile(os.path.join(ROOT, target)):
broken.append((rel, lineno, url))

if broken:
print(f"Found {len(broken)} broken internal link(s):")
for rel, lineno, url in broken:
Expand Down
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Amezmo Documentation

[![Docs lint](https://github.com/amezmo/docs/actions/workflows/docs-lint.yml/badge.svg)](https://github.com/amezmo/docs/actions/workflows/docs-lint.yml)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/amezmo/docs/pulls)

The source for the [Amezmo](https://www.amezmo.com) docs: everything on
zero-downtime PHP hosting, from your first `git push` to tuning Nginx, OPcache,
and Redis on a production instance. Published at
[amezmo.com/docs](https://www.amezmo.com/docs/).

**This repo is Markdown and nothing else.** No framework, no package manager,
no build step, no dependencies to install. Clone it, open a `.md` file in your
editor's preview, and you are looking at the page. That means the distance
between "this paragraph is wrong" and a merged fix is about four minutes.

## Contribute

You already know something we got wrong. Ship it:

```bash
$ git clone git@github.com:amezmo/docs.git
$ cd docs
$ git checkout -b fix-cron-timezone-note
$ $EDITOR cron/index.md
$ python3 .github/scripts/check_links.py
```

Then open a pull request. Typo fixes, a missing flag, a code sample that no
longer runs, an entire new how-to guide: all of it is welcome, and small PRs
get merged fast.

Good first contributions:

- Fix a code sample you tried and found stale.
- Add the gotcha that cost you an hour, as a `> [!WARNING]` callout.
- Write the how-to guide for your framework in
[how-to-guides/](how-to-guides/index.md).
- Document an endpoint the [REST API](api/index.md) section is missing.

Read the
[contributor rulebook](CLAUDE.md)
before a larger change. It covers page layout, the API reference format, the
changelog format, and the house writing style. It's written for Claude Code, and
it's the same rulebook a human needs.

## Local Checks

CI runs these four on every push and pull request. Run them from the repo root
first:

```bash
$ python3 .github/scripts/check_links.py # internal links resolve
$ python3 .github/scripts/check_image_paths.py # images use absolute URLs
$ codespell --config .codespellrc # spelling
$ python3 .github/scripts/check_backlinks.py # backlinks (warn-only)
```

The first three gate the build. The backlink check only reports.

## Conventions Worth Knowing Up Front

Internal links are relative and keep their `.md` suffix, so they work in the
GitHub file browser and on the rendered site: `releases.md` for a sibling,
`../instances/scaling.md` across sections. Never use a `/docs/`-absolute or
extensionless path.

Links are reciprocal. When one content page links to another, the target links
back. Landing pages are exempt, and deliberate one-way links are recorded in
[`.github/backlink-ignore.txt`](.github/backlink-ignore.txt)
with a comment explaining why.

Pages render through league/commonmark with the attributes and description list
extensions, plus GitHub alert callouts:

```markdown
> [!WARNING]
> Restoring a backup overwrites the target database.
```

New pages are linked from their section's `index.md`. Nothing is
auto-discovered by a nav generator.

## The Map

[index.md](index.md) is the home page. The big sections:

| Section | What's in it |
| --- | --- |
| [Instances](instances/index.md) | Dedicated and full-stack servers, scaling, private networking |
| [Deployments](deployments/index.md) | Atomic zero-downtime releases, hooks, instant rollbacks |
| [Databases](databases/index.md) | Managed MySQL, backups, restores between environments |
| [REST API](api/index.md) | Endpoint reference for automating the dashboard |
| [How-to guides](how-to-guides/index.md) | Laravel, Craft CMS, Drupal, and friends |
| [Domains](domains/index.md), [Cron](cron/index.md), [Workers](workers/index.md) | The rest of the platform surface |

## Contributors

Built by the people who use it.

[![Contributors](https://contrib.rocks/image?repo=amezmo/docs)](https://github.com/amezmo/docs/graphs/contributors)

Your avatar goes here on your first merged PR.

## See Also

- [Amezmo on YouTube](https://www.youtube.com/@AmezmoPHP)
- [PHP hosting guides](https://www.amezmo.com/guides)
- [Amezmo on GitHub](https://www.github.com/amezmo)
- [Slack community](https://www.amezmo.com/goslack)
40 changes: 40 additions & 0 deletions api/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@

The changelog lists forward-compatible changes to the API.

## 2026-09-12

Added
: [Create a cron entry](cron/create-cron-entry.md) creates an entry on an
instance: a permanent name, a bash script, a schedule, and the environment to
run it in. Deleting an entry stays in the dashboard.

## 2026-09-10

Breaking
: `type` on an error response is now the coarse class of the error
(`invalid_request_error`, `authentication_error`, `rate_limit_error`,
`api_error`) rather than the specific reason. The specific reason moved to the
new `code` field, keeping the strings it used before, so a client matching on
`unknown_resource_error` or `validation_error` should read `code` instead of
`type`. See [Errors](errors.md).

Added
: `code` to every error response: the specific, machine-readable reason, where
`type` is the coarse class. See [Errors](errors.md).

Fixed
: Errors that carry their own response body (no payment method, unverified
email, an instance that is paused or still launching, a feature your instance
type does not include) returned a generic `500` with no usable message. They now
return their real status, message, and `code`.

Fixed
: Error messages written for users were replaced with "An unexpected API error
occurred." outside development. The real message is now returned, and only
genuinely unexpected failures fall back to a generic one.

Added
: [Cron endpoints](cron/index.md). [List cron entries](cron/list-cron-entries.md)
lists every entry on an instance, and
[Get a cron entry](cron/get-cron-entry.md) gets one by ID.
: [Update a cron entry](cron/update-cron-entry.md) updates the `command`, the
`expression`, or both, on an existing entry. An entry's `name` stays fixed,
because Amezmo derives its log file name from it.

## 2022-12-12

Added
Expand Down
Loading