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
39 changes: 39 additions & 0 deletions .claude/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Claude Code configuration

The machinery only. Guidance for agents lives in `AGENTS.md`, which links
every rule and skill here.

## How guidance is split

| Mechanism | Loaded | Holds |
| ------------------- | ------------------------------- | --------------------------------------------------- |
| `settings.json` | Enforced by the client | Rules that must not depend on Claude following them |
| `AGENTS.md` | Every session | Facts that apply to every change |
| `rules/*.md` | When a matching file is read | Guidance for one area of the tree |
| `skills/*/SKILL.md` | When invoked or judged relevant | Procedures run occasionally |

`AGENTS.md` links each rule and skill, because other agents do not load them
on their own. A `CLAUDE.md` or `CLAUDE.local.md` in the project or a parent
directory makes Claude Code skip `AGENTS.md`, unless its project instructions
setting loads both.

## Hooks

**`check-before-commit.sh`** runs the check-only `npm run prettier` and
`npm run textlint` CI runs, through `ddev npm`. Every failure maps to exit 2,
and a stopped project blocks too rather than letting an unchecked commit
through. It takes about 12 seconds. It checks the working tree of
`$CLAUDE_PROJECT_DIR`, not the staged files, so a commit made in another
worktree is checked against the main checkout instead.

**`format-edited-file.sh`** passes only the edited file to `ddev prettier`, and
to `ddev textlint` under `src/content/`, the scope CI lints. A path outside
the project is skipped, `.prettierignore` still applies, and `.astro` files
are skipped because no Prettier plugin parses them. An error the tools cannot
fix exits 2, so Claude sees it; a payload without a file path or a stopped
project exits 1, which only the user sees.

## Permissions

`Bash(git push *)` also matches a bare `git push`. It does not match a push
written another way, such as `git -C . push`; `AGENTS.md` covers those.
23 changes: 23 additions & 0 deletions .claude/hooks/check-before-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
# PreToolUse gate on `git commit`: the check-only prettier and textlint runs
# CI does. Only exit 2 blocks the commit. See .claude/README.md.

set -uo pipefail

cd "$CLAUDE_PROJECT_DIR" || exit 2

if ! ddev exec true >/dev/null 2>&1; then
echo "check-before-commit: the DDEV project is not running, so prettier and textlint did not run. Run 'ddev start', then commit again." >&2
exit 2
fi

status=0
if ! out=$(ddev npm run prettier 2>&1); then
printf '%s\n\nRun "ddev prettier" to fix it, then commit again.\n\n' "$out" >&2
status=2
fi
if ! out=$(ddev npm run textlint 2>&1); then
printf '%s\n\nRun "ddev textlint" to fix it, then commit again.\n' "$out" >&2
status=2
fi
exit $status
38 changes: 38 additions & 0 deletions .claude/hooks/format-edited-file.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
# PostToolUse(Edit|Write): `ddev prettier` on the edited file, and
# `ddev textlint` too under src/content/, the scope CI lints. See
# .claude/README.md.

set -uo pipefail

payload=$(cat)

if command -v jq >/dev/null 2>&1; then
file=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')
else
file=$(printf '%s' "$payload" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4)
fi

if [ -z "$file" ]; then
echo "format-edited-file: no file path in the payload, so nothing was formatted" >&2
exit 1
fi

case "$file" in
"$CLAUDE_PROJECT_DIR"/*) ;;
*) exit 0 ;;
esac

cd "$CLAUDE_PROJECT_DIR" || exit 1
if ! ddev exec true >/dev/null 2>&1; then
echo "format-edited-file: the DDEV project is not running, so $file was not formatted" >&2
exit 1
fi

# Exit 2 shows the output to Claude, so it can fix what the tools could not.
rel="${file#"$CLAUDE_PROJECT_DIR"/}"
ddev prettier "$rel" >&2 || exit 2
case "$rel" in
src/content/*) ddev textlint "$rel" >&2 || exit 2 ;;
esac
exit 0
23 changes: 23 additions & 0 deletions .claude/rules/comments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
paths:
- "**/*.{js,mjs,cjs,ts,tsx,jsx,astro,css,sh}"
- ".github/**"
- ".ddev/**"
---

# Comment style

A comment earns its place only by saying something the code does not: why,
not what. **Budget: three lines inside a function or block, eight for a file
header or a doc comment.** Past that, the reasoning belongs in the commit
message, not the file.

- Do not restate the code or the function name, or explain standard
language or tool behavior.
- Do not repeat one explanation at more than one call site, or re-describe
what a linked issue or commit message already covers.
- Describe what is true now, not what changed. "Moved from X" reads as a diff
note and goes stale.

Reread every comment you wrote before finishing, and cut what breaks these
rules.
98 changes: 98 additions & 0 deletions .claude/rules/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
paths:
- "src/content/**"
- "src/pages/**/*.mdx"
- "public/img/**"
---

# Blog posts, authors, and content pages

Callouts, code blocks, images, and other Markdown features are in
`MARKDOWN_FORMATTING.md`. Blog posts are Markdown, not MDX.

## Voice

A post is a developer telling other developers what works, with the commands
to show it. `navigating-ddev-projects-filesystem.md` is a good model. The
words banned in `AGENTS.md` apply here too.

- Open with the problem, the news, or the question behind the post, not with
"In today's...", "Whether you're a ... or a ...", or a preview of the post.
- Be specific: versions, commands, real output, numbers. Run every command
before putting it in a post, and cut a claim that has no command, link, or
number behind it.
- Write prose. Use a list for steps or options, not for bold-labeled
fragments such as "**Speed:** ...".
- Say what does not work, and the limits and workarounds.
- Address the reader as "you" and the DDEV maintainers as "we".
- Avoid the patterns that read as generated: "not just X, but Y", "It's not
X, it's Y", three adjectives in a row, a rhetorical question to open a
section, "Let's dive in", "In conclusion", "Happy coding!", and emoji in
headings. Use an em dash only where a comma or a period will not do.
- End when the content ends. A docs link or a request for feedback is enough;
do not recap.
- When editing someone else's post, keep their voice. Fix facts and wording,
and do not rewrite it into yours.

Before finishing, reread the draft and cut every sentence that could appear
unchanged on another product's blog.

## Blog posts

`src/content/blog/<kebab-case-slug>.md`, validated by `src/content.config.ts`:

```markdown
---
title: "Post Title"
pubDate: 2026-01-01
modifiedDate: 2026-01-03 # optional, with modifiedComment
summary: Brief description
author: Author Name
featureImage:
src: /img/blog/2026/01/kebab-case.jpg
srcDark: # optional
alt: Descriptive alt text
caption: Optional caption, Markdown allowed in straight quotes
credit: Optional credit
categories:
- Guides
---
```

- `author` must match the `name` of a file in `src/content/authors/`. A new
author needs one, with `name`, `firstName`, and an optional `avatarUrl`.
- `featureImage` also takes `shadow: true`, and `hide: true`, which keeps the
image off the post page but still uses it for cards and social previews.
- `categories` must be from `allowedCategories` in `src/content.config.ts`:
Add-ons, Announcements, Community, DevOps, Performance, Guides, Newsletters,
TechNotes, Training, Videos. The first one shows on summary cards.

## Links

- Another blog post: its filename, `[text](other-post.md)`; Astro resolves it.
- Other site pages: root-relative, `[Contact](/contact)`.
- Outside the site: absolute URL.

The build fails on a broken internal link (astro-link-validator), and CI
checks external links in changed posts (`.linkspector.yml`).

## Images

Put new images in `public/img/blog/YYYY/MM/`. The build converts PNG, JPEG,
and GIF images to WebP, but the source files are committed as they are, so
add them ready to publish: JPEG for photos, PNG or SVG
otherwise, under 2MB, no wider than about 2000px. The fork preview build warns
on anything over 2MB; check before committing:

```bash
find public \( -name "*.jpg" -o -name "*.png" -o -name "*.jpeg" \) -size +2048k
```

Alt text doubles as the visible caption. For terminal screenshots, hand-written
SVG illustrations, or logo-and-text feature images, use the `blog-images`
skill.

## Checks

Run `ddev textlint` (terminology and stop words, per `.textlintrc`, for
`src/content/**` only), and spell check new prose yourself.
57 changes: 57 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": [
"Bash(ddev npm run *)",
"Bash(ddev npm outdated)",
"Bash(ddev npm audit)",
"Bash(ddev npm ls *)",
"Bash(ddev npm install-scripts ls)",
"Bash(ddev prettier *)",
"Bash(ddev textlint *)",
"Bash(ddev logs *)",
"Bash(ddev describe *)",
"Bash(jq *)",
"Bash(gh pr list *)",
"Bash(gh pr view *)",
"Bash(gh pr checks *)",
"Bash(gh pr diff *)",
"Bash(gh issue list *)",
"Bash(gh issue view *)",
"Bash(gh run list *)",
"Bash(gh run view *)",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:docs.astro.build)",
"WebFetch(domain:docs.ddev.com)",
"WebFetch(domain:ddev.com)",
"WebFetch(domain:code.claude.com)"
],
"deny": ["Bash(git push *)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git commit *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-before-commit.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-edited-file.sh"
}
]
}
]
}
}
115 changes: 115 additions & 0 deletions .claude/skills/add-sponsor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
name: add-sponsor
description: Add a featured sponsor to ddev.com from just its website, by finding the official logo, making a dark variant when needed, adding the entry to src/featured-sponsors.json, and checking the home page and the README badges in both themes. Use when asked to add, update, or remove a featured sponsor.
when_to_use: >
Triggered by "add a sponsor", "new featured sponsor", "add <company> to
sponsors", "update a sponsor logo", or a sponsor website URL given with a
request to feature it.
argument-hint: <sponsor website URL>
---

# Adding a featured sponsor

Featured sponsors appear in the Featured Sponsors section of the home page
(`/#supporters`) and in two generated badges that the main DDEV README embeds:
`/resources/featured-sponsors.svg` and
`/resources/featured-sponsors-darkmode.svg`. All three read
`src/featured-sponsors.json`.

The only input needed is the sponsor's website. Ask for anything below that
the site does not settle.

## 1. Find the official logo

Use the sponsor's own logo file. Never redraw or approximate one.

1. Fetch the raw page with `curl -sL <url>`, not WebFetch, which summarizes
instead of returning markup. Look for, in this order:
- an `<img>` or `<a>` with `logo` in its `src`, `class`, `id`, or `alt`,
usually in the header, pointing at an `.svg`
- an inline `<svg>` in the header logo link
- a press, brand, or media kit page (`/press`, `/brand`, `/media`)
- `<link rel="icon" type="image/svg+xml">`, which is often only the mark
2. If there is no SVG, use the largest PNG available, and say so in the PR.
The organization's GitHub avatar (`https://github.com/<org>.png?size=400`)
is the last resort, and right for a person, as with `dougvann.jpeg`.

Save it as `public/logos/<slug>.svg`, where `<slug>` is the lowercase name with
dashes.

## 2. Make it self-contained

An SVG cut out of a page often depends on that page:

- It must have a `viewBox`. Copy the numbers from `width`/`height` if needed.
- Replace `currentColor`, CSS classes, and `var(--...)` fills with explicit
colors taken from the site.
- Remove `<script>`, event handlers, and references to external files or
fonts. Text has to be paths already; if it is `<text>`, find another file.
- Trim empty space around the artwork by tightening the `viewBox`, since the
badges size each logo by its bounds.

Then render it on both backgrounds and look at the result:

```bash
rsvg-convert -h 120 -b white public/logos/<slug>.svg -o ~/tmp/<slug>-light.png
rsvg-convert -h 120 -b '#0d1117' public/logos/<slug>.svg -o ~/tmp/<slug>-dark.png
```

## 3. Dark variant

If the logo reads well on the dark render, use the same file for `darklogo`.
Otherwise copy it to `public/logos/<slug>-dark.svg` and change the near-black
fills and strokes, usually the wordmark, to `#ffffff`, keeping the brand colors
as they are. Render the copy on `#0d1117` again to check it.

## 4. Add the entry

Append to `src/featured-sponsors.json`:

```json
{
"name": "Example GmbH",
"type": "standard",
"logo": "/logos/example.svg",
"darklogo": "/logos/example-dark.svg",
"url": "https://example.com/",
"github": "example"
}
```

- `name`: the organization's name exactly as it writes it on its site.
- `logo`, `darklogo`, `url`: required. `darklogo` repeats `logo` when step 3
needed no copy.
- `type`: `"major"` or `"standard"`, by contribution level. Nothing reads it
yet.
- `github`: the GitHub login, when the sponsorship comes through GitHub.
- `isLeading`: `true` only when asked. It puts the sponsor in a separate first
row on the home page and in both badges.
- Leave out `squareLogo`; no page displays it.

## 5. Check the result

With `ddev start` running:

```bash
for v in "" -darkmode; do
curl -sk "https://<projectname>.ddev.site:4321/resources/featured-sponsors$v.svg" -o ~/tmp/badge$v.svg
done
rsvg-convert -w 800 -b white ~/tmp/badge.svg -o ~/tmp/badge.png
rsvg-convert -w 800 -b '#0d1117' ~/tmp/badge-darkmode.svg -o ~/tmp/badge-darkmode.png
```

View both PNGs. The new logo must be visible, in proportion with its
neighbors, and not cut off. A badge route that fails to build usually means
`sharp` could not read the SVG, so go back to step 2. Then check `/#supporters`
on the dev server in both themes, using the theme toggle in the header. If the
logo looks too large or small there next to the others, add a Tailwind height
class for it to `nudges` in `src/components/FeaturedSponsors.astro`, keyed by
`name`.

## 6. Commit

Title `chore(sponsor): add <name>`. Follow the `ddev-commit` skill. Manual
Testing Instructions compare the preview's `/#supporters` and both badge URLs
against https://ddev.com/, and the PR should include the two badge renders.
Loading
Loading