From 40f3466f2b6ef9a8c525d2b6e88dc4ea26e84f95 Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk Date: Mon, 5 Oct 2026 14:46:15 +0300 Subject: [PATCH 1/2] chore(claude): split AGENTS.md into rules, skills, and hooks, fix stale docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short Summary (TL;DR) Most of `AGENTS.md` moves into rules and skills that load only when needed, hooks now run the prettier and textlint checks that CI runs, and the docs that had drifted from the code are corrected. ## The Issue `AGENTS.md` had grown to 304 lines, all loaded in every session, although most of it applies only to blog content, images, or commits. Nothing enforced its rules, and `.gitignore` ignored all of `.claude`, so no shared Claude Code configuration could be committed. `ddev textlint` passed on errors it could not fix, because `textlint --fix` exits 0 when they remain, so CI failed where the local check had passed. README.md, README_SPONSOR.md, MARKDOWN_FORMATTING.md, and FORK_PREVIEW_SETUP.md had drifted from the code. ## How This PR Solves The Issue Follows ddev/ddev#8805 and its follow-ups. `AGENTS.md` keeps what applies to every change and links the rest: - `.claude/rules/content.md` (blog posts, their voice, schema, links, images) and `.claude/rules/comments.md` load only for the files they cover. - Skills: `bump-deps` documents the dependency bump from #601, #637, #685, #734, and #747, including `allowScripts`. `add-sponsor` replaces README_SPONSOR.md, and only needs the sponsor's website. `ddev-commit` holds the commit and PR rules, and the PR template gains the Short Summary (TL;DR) section from the ddev/ddev template. `blog-images` holds the screenshot steps, now with a shorter FEATURE_IMAGE_GUIDE.md as `feature-banner.md`. - `.claude/settings.json` denies `git push`, allows read-only `gh` commands and the `ddev` checks, and adds two hooks. Before `git commit`, the check-only prettier and textlint run as in CI. After an edit, `ddev prettier` runs on that file, and `ddev textlint` too under `src/content/`. - `ddev prettier` and `ddev textlint` take optional file arguments, and `ddev textlint` checks again after fixing. The npm scripts call `prettier` and `textlint` by name, since `npm run` already puts `node_modules/.bin` on `PATH`. - `.gitignore` ignores only the personal files the Claude Code docs list. The docs corrections include blog categories, the dev server port, page paths, image conversion to WebP, external link behavior, feature image sizes, the unused `squareLogo`, the theme toggle, and the fork preview workflows and URLs. Prettier has no Astro plugin, so `AGENTS.md` now says nothing formats `.astro` files. README.md is shorter, with setup steps that `AGENTS.md` and the skills no longer repeat, and opens with the logo, badges, and links, as the ddev/ddev README does. Two posts set `modifiedData` instead of `modifiedDate`, which the content schema dropped without an error, so their modified dates now show. ## Manual Testing Instructions ```bash ddev start ddev prettier src/lib/api.ts # formats one file ddev textlint # fixes, then reports anything left ``` Start a new Claude Code session on this branch, then confirm that `/bump-deps`, `/add-sponsor`, `/ddev-commit`, and `/blog-images` are listed, that editing a file formats it, that `git commit` runs the checks first, and that `git push` is denied. ## Automated Testing Overview None. The hook scripts were run directly against a formatted file, an unformatted one, an unfixable textlint error, a `.prettierignore` path, a path outside the project, an empty payload, and a stopped project. In a live session, an edit was formatted, an unfixable textlint error was reported back, and `git commit --dry-run` was blocked while that error remained. ## Release/Deployment Notes No effect on the site build. Agents other than Claude Code, including Copilot code review, keep reading `AGENTS.md`, which links each rule and skill file. 🤖 Developed with assistance from [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 5.5 --- .claude/README.md | 39 ++ .claude/hooks/check-before-commit.sh | 23 ++ .claude/hooks/format-edited-file.sh | 38 ++ .claude/rules/comments.md | 23 ++ .claude/rules/content.md | 98 +++++ .claude/settings.json | 57 +++ .claude/skills/add-sponsor/SKILL.md | 115 ++++++ .claude/skills/blog-images/SKILL.md | 64 +++ .claude/skills/blog-images/feature-banner.md | 128 ++++++ .claude/skills/bump-deps/SKILL.md | 133 ++++++ .claude/skills/ddev-commit/SKILL.md | 135 ++++++ .ddev/commands/web/prettier | 12 +- .ddev/commands/web/textlint | 13 +- .github/FORK_PREVIEW_SETUP.md | 81 +--- .github/PULL_REQUEST_TEMPLATE.md | 8 + .gitignore | 6 +- AGENTS.md | 412 ++++++------------- FEATURE_IMAGE_GUIDE.md | 203 --------- MARKDOWN_FORMATTING.md | 17 +- README.md | 227 ++++------ README_SPONSOR.md | 48 --- package.json | 8 +- src/content/blog/ddev-jan-2026-newsletter.md | 2 +- src/content/blog/ddev-snapshots.md | 2 +- 24 files changed, 1106 insertions(+), 786 deletions(-) create mode 100644 .claude/README.md create mode 100755 .claude/hooks/check-before-commit.sh create mode 100755 .claude/hooks/format-edited-file.sh create mode 100644 .claude/rules/comments.md create mode 100644 .claude/rules/content.md create mode 100644 .claude/settings.json create mode 100644 .claude/skills/add-sponsor/SKILL.md create mode 100644 .claude/skills/blog-images/SKILL.md create mode 100644 .claude/skills/blog-images/feature-banner.md create mode 100644 .claude/skills/bump-deps/SKILL.md create mode 100644 .claude/skills/ddev-commit/SKILL.md delete mode 100644 FEATURE_IMAGE_GUIDE.md delete mode 100644 README_SPONSOR.md diff --git a/.claude/README.md b/.claude/README.md new file mode 100644 index 000000000..a6f7193d1 --- /dev/null +++ b/.claude/README.md @@ -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. diff --git a/.claude/hooks/check-before-commit.sh b/.claude/hooks/check-before-commit.sh new file mode 100755 index 000000000..0a2066e8c --- /dev/null +++ b/.claude/hooks/check-before-commit.sh @@ -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 diff --git a/.claude/hooks/format-edited-file.sh b/.claude/hooks/format-edited-file.sh new file mode 100755 index 000000000..5d2495730 --- /dev/null +++ b/.claude/hooks/format-edited-file.sh @@ -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 diff --git a/.claude/rules/comments.md b/.claude/rules/comments.md new file mode 100644 index 000000000..e66ce638c --- /dev/null +++ b/.claude/rules/comments.md @@ -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. diff --git a/.claude/rules/content.md b/.claude/rules/content.md new file mode 100644 index 000000000..3cefaf664 --- /dev/null +++ b/.claude/rules/content.md @@ -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/.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. diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..88d679d86 --- /dev/null +++ b/.claude/settings.json @@ -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" + } + ] + } + ] + } +} diff --git a/.claude/skills/add-sponsor/SKILL.md b/.claude/skills/add-sponsor/SKILL.md new file mode 100644 index 000000000..2b7e912d3 --- /dev/null +++ b/.claude/skills/add-sponsor/SKILL.md @@ -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 to + sponsors", "update a sponsor logo", or a sponsor website URL given with a + request to feature it. +argument-hint: +--- + +# 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 `, not WebFetch, which summarizes + instead of returning markup. Look for, in this order: + - an `` or `` with `logo` in its `src`, `class`, `id`, or `alt`, + usually in the header, pointing at an `.svg` + - an inline `` in the header logo link + - a press, brand, or media kit page (`/press`, `/brand`, `/media`) + - `
`. + +**Write each paragraph of a commit body or `--body-file` as one continuous +line**, with empty lines only between paragraphs, headings, and list items. +Code blocks and tables are unaffected. + +## Passing the body + +Write the message to a file, then `git commit -F `. Never use +`git commit -m "$(cat <<'EOF' ... EOF)"`, which mangles the message. The same +holds for `gh`: + +```bash +gh pr create --title "" --body-file ~/tmp/pr_body.md +gh pr edit <number> --title "<title>" --body-file ~/tmp/pr_body.md +``` + +## Before committing + +1. Run the checks in `AGENTS.md`, including `ddev npm run build` when code, + config, or dependencies changed. +2. Read `git diff --cached` for comments that break + `.claude/rules/comments.md`, and cut them. +3. When amending, re-check every claim already in the body against the + current diff. + +Then commit and stop: pushing is forbidden, per `AGENTS.md`. Report the branch +and which commits are unpushed. diff --git a/.ddev/commands/web/prettier b/.ddev/commands/web/prettier index 4dd06a688..8117d0bc7 100755 --- a/.ddev/commands/web/prettier +++ b/.ddev/commands/web/prettier @@ -1,10 +1,14 @@ #!/usr/bin/env bash #ddev-silent-no-warn -## Description: Run prettier inside the web container -## Usage: prettier -## Example: "ddev prettier" +## Description: Run prettier inside the web container, on the whole tree or the given files +## Usage: prettier [file ...] +## Example: "ddev prettier" or "ddev prettier src/lib/api.ts" (paths relative to the project root) ## ExecRaw: true ## MutagenSync: true -npm run prettier:fix +if [ $# -eq 0 ]; then + npm run prettier:fix +else + "$DDEV_APPROOT"/node_modules/.bin/prettier --write --ignore-unknown "$@" +fi diff --git a/.ddev/commands/web/textlint b/.ddev/commands/web/textlint index b92867231..d6f801a16 100755 --- a/.ddev/commands/web/textlint +++ b/.ddev/commands/web/textlint @@ -1,10 +1,15 @@ #!/usr/bin/env bash #ddev-silent-no-warn -## Description: Run textlint inside the web container -## Usage: textlint -## Example: "ddev textlint" +## Description: Run textlint inside the web container, on src/content/** or the given files +## Usage: textlint [file ...] +## Example: "ddev textlint" or "ddev textlint src/content/blog/my-post.md" (paths relative to the project root) ## ExecRaw: true ## MutagenSync: true -npm run textlint:fix +# --fix exits 0 even when errors it cannot fix remain, so check again after it. +if [ $# -eq 0 ]; then + npm run textlint:fix && npm run textlint +else + "$DDEV_APPROOT"/node_modules/.bin/textlint --fix "$@" && "$DDEV_APPROOT"/node_modules/.bin/textlint "$@" +fi diff --git a/.github/FORK_PREVIEW_SETUP.md b/.github/FORK_PREVIEW_SETUP.md index e7d406d66..321c68403 100644 --- a/.github/FORK_PREVIEW_SETUP.md +++ b/.github/FORK_PREVIEW_SETUP.md @@ -4,10 +4,10 @@ This guide explains how to configure the automated preview generation for forked ## Overview -The workflow in `.github/workflows/cloudflare-preview-forks.yml` implements a secure two-stage process: +Two workflows implement a two-stage process: -1. **Build Stage**: Safely builds the site from fork code without exposing secrets -2. **Deploy Stage**: Uses Cloudflare API to deploy the built site with secure credentials +1. **Build stage**, `.github/workflows/cloudflare-preview-forks-build.yml`: builds the site from fork code on `pull_request`, with no access to secrets +2. **Deploy stage**, `.github/workflows/cloudflare-preview-forks-deploy.yml`: runs on `workflow_run` after a successful build and deploys the artifact with the Cloudflare credentials, without checking out fork code ## Required Setup @@ -45,20 +45,12 @@ Add these in GitHub repository settings → Secrets and variables → Actions: - `CF_ACCOUNT_ID`: Your Cloudflare Account ID (found in dashboard sidebar) - `CF_PAGES_PROJECT`: Set to `ddev-com-fork-previews` (dedicated fork preview project) -### 4. Repository Variables (Optional) +### 4. Enable Workflow -For custom build configurations, set these in GitHub repository settings → Secrets and variables → Actions → Variables: +The workflows run automatically for PRs from forks only: -- `PAGES_BUILD_CMD`: Custom build command (e.g., `npm ci && npm run build`) -- `PAGES_OUTPUT_DIR`: Build output directory (e.g., `dist`, `public`, `build`) -- `PAGES_WORKING_DIR`: Project subdirectory if not root (e.g., `site`, `docs`) - -### 5. Enable Workflow - -The workflow is triggered automatically for: - -- Forked repository PRs only -- Events: `opened`, `synchronize`, `reopened`, `ready_for_review`, `closed` +- Build: `opened`, `synchronize`, `reopened`, `ready_for_review` +- Deploy: after each successful build, and on `closed` (through `pull_request_target`) to add a closing note ## Security Features @@ -73,29 +65,28 @@ The workflow is triggered automatically for: - Validates blog post frontmatter structure - Detects potentially unsafe content patterns - Warns about oversized images (>2MB) -- Runs textlint and prettier if available +- Runs textlint and prettier, and a failure in either stops the preview ### Access Controls - Only processes PRs from forked repositories -- Uses `pull_request_target` with explicit fork checkout +- Builds on `pull_request`, so fork code never runs with secrets; `pull_request_target` is used only for the closing note, which checks out no code - Separates untrusted code execution from credential access ## Workflow Behavior ### Build Process -1. Detects build system (npm/yarn/pnpm/hugo/custom) -2. Runs content validation and security checks -3. Installs dependencies and runs linting -4. Builds the site -5. Packages output as artifact +1. Runs content validation and security checks +2. Installs dependencies with `npm ci`, then runs textlint and prettier +3. Builds the site with `npm run build` +4. Packages `dist/` and the PR number as an artifact ### Deployment Process 1. Downloads build artifact from Stage 1 2. Deploys to Cloudflare Pages using wrangler-action -3. Creates stable branch URL for consistent preview access +3. Deploys under the `pr-<number>` alias, see [Preview URLs](#preview-urls) 4. Comments preview URL on the PR 5. Updates comment on subsequent pushes @@ -116,7 +107,7 @@ The workflow is triggered automatically for: ### Missing Secrets - Workflow will fail with clear error messages -- Verify all three secrets are set correctly +- Verify the `TESTS_SERVICE_ACCOUNT_TOKEN` secret, the `CF_API_TOKEN` item in the 1Password `test-secrets` vault, and the `CF_ACCOUNT_ID` and `CF_PAGES_PROJECT` variables - Check Cloudflare API token permissions ### Content Validation Errors @@ -124,8 +115,8 @@ The workflow is triggered automatically for: - Review security check output - Fix frontmatter issues in blog posts - Address linting warnings locally with: - - `ddev npm run textlint:fix` - - `ddev npm run prettier:fix` + - `ddev textlint` + - `ddev prettier` ### Preview URL Issues @@ -143,39 +134,11 @@ To test the workflow: 4. Watch GitHub Actions for build/deploy progress 5. Check for preview URL comment on the PR -## Maintenance - -### Regular Tasks - -- Monitor Cloudflare Pages usage and costs -- Review security warnings in build logs -- Update dependencies in fork validation steps -- Clean up old preview deployments if needed - -### Updates +## Preview URLs -- Keep `cloudflare/wrangler-action` version current -- Monitor Cloudflare API changes -- Update content validation rules as needed +The deploy step passes `--branch=pr-<number>` to `wrangler pages deploy`, so each PR keeps one URL across pushes, `https://pr-<number>.ddev-com-fork-previews.pages.dev`. The PR comment links that alias, or the commit-specific deployment URL when there is no alias. -## Stable URL Implementation - -### Solution Implemented - -The workflow now uses `cloudflare/wrangler-action` which provides stable branch URLs through the `pages-deployment-alias-url` output. This ensures consistent preview URLs for each PR: - -- **Branch URL**: Stable per PR (e.g., `https://pr-123.project-name.pages.dev`) -- **Deployment URL**: Commit-specific for debugging if needed - -### Benefits - -1. **Stable bookmarking**: Preview URLs remain constant across pushes to the same PR -2. **Better collaboration**: Team members can bookmark and share stable URLs -3. **Future-proof**: Uses the recommended, actively maintained Cloudflare action -4. **Enhanced debugging**: Both stable and commit-specific URLs available - -### Migration Notes +## Maintenance -- Migrated from deprecated `cloudflare/pages-action@v1` to `cloudflare/wrangler-action` -- Updated output variable handling (`url` → `pages-deployment-alias-url`) -- Maintained backward compatibility with existing project configuration +- Keep the `cloudflare/wrangler-action` version current. +- Review the security warnings in the build logs, and update the validation rules when the content structure changes. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0cfb9b6c3..a4a6ec3ab 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,16 @@ <!-- PR titles have very precise rules, please read https://docs.ddev.com/en/stable/developers/building-contributing/#pull-request-title-guidelines + + If this is a nontrivial contribution, please create an issue for discussion first, or discuss it first in Discord, https://ddev.com/s/discord. This will save you time and help direct the feature. + + DDEV is community-funded — if you or your team rely on DDEV, see https://ddev.com/sponsor. --> +## Short Summary (TL;DR) + +<!-- Required. One or two sentences a reviewer can read at a glance: what this changes, and why. Write it last, but put it here first. If it needs a paragraph, the detail belongs in the sections below. --> + ## The Issue - Fixes #REPLACE_ME_WITH_RELATED_ISSUE_NUMBER diff --git a/.gitignore b/.gitignore index c33aff8a0..3e91f8d39 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,8 @@ pnpm-debug.log* .astro -.claude +# .claude/ is shared team configuration and is committed; only personal +# overrides stay private. https://code.claude.com/docs/en/claude-directory +**/.claude/settings.local.json +.claude/agent-memory-local/ +CLAUDE.local.md diff --git a/AGENTS.md b/AGENTS.md index 9b2c1c319..d5bc03e1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,304 +1,124 @@ # AGENTS.md -This file provides guidance to AI agents when working with code in this repository. - -## Tool Preferences - -- Prefer `jq` over Python for JSON processing in shell commands - -## Communication Style - -- Use direct, concise language without unnecessary adjectives or adverbs -- Avoid flowery or marketing-style language ("tremendous", "dramatically", "revolutionary", etc.) -- Don't use vague superlatives ("comprehensive", "complete", "full", "entire", "thorough", "detailed") -- Don't include flattery or excessive praise ("excellent!", "perfect!", "great job!") -- State facts and findings directly without embellishment -- Skip introductory phrases like "I'm excited to", "I'd be happy to", "Let me dive into" -- Avoid concluding with summary statements unless specifically requested -- When presenting options or analysis, lead with the core information, not commentary about it - -## Project Overview - -This is the source code for ddev.com, a static website built with Astro and hosted on Cloudflare Pages. The site features a blog, documentation, sponsor information, and project resources for DDEV, a local development environment tool. - -## Development Commands - -Use DDEV for all development tasks: - -- `ddev start` - Start project with all dependencies -- `ddev npm run dev` - Start development server with hot reloading -- `ddev npm run build` - Build production site to `./dist/` -- `ddev npm run preview` - Preview built site locally -- `ddev prettier` - Auto-fix formatting (custom command, runs `npm run prettier:fix` in the web container) -- `ddev textlint` - Auto-fix content writing issues (custom command, runs `npm run textlint:fix` in the web container) - -Prefer the `ddev prettier` and `ddev textlint` custom commands (defined in `.ddev/commands/web/`). The underlying npm scripts are also available if you need the check-only variants: `ddev npm run prettier`, `ddev npm run prettier:fix`, `ddev npm run textlint`, `ddev npm run textlint:fix`. - -### Site Access - -- Dev server: `https://<projectname>.ddev.site:4321` -- Built site: `https://<projectname>.ddev.site` - -### Testing and Quality - -Before committing changes, always run: - -1. `ddev prettier` - Fix formatting -2. `ddev textlint` - Fix content quality issues -3. `ddev start` - Ensure environment is working -4. Spellcheck and check links in any new content -5. Check image sizes for any new/changed images: `find public \( -name "*.jpg" -o -name "*.png" -o -name "*.jpeg" \) -exec sh -c 'size=$(stat -f%z "$1" 2>/dev/null || stat -c%s "$1"); if [ "$size" -gt 2097152 ]; then echo "Large image: $1 ($((size/1024))KB)"; fi' _ {} \;` - CI warns on any image over 2MB under `public/`; resize/recompress (or convert photographic PNGs to JPEG) before committing instead of fixing it in a follow-up PR - -### Commits - -When making commits after major changes, use AI-assisted commit messages that include: - -- Clear description of changes made -- A body following `.github/PULL_REQUEST_TEMPLATE.md`, HTML comments removed and empty sections dropped, so it can be reused as the pull request description -- A short body: a few lines per section, no restating the diff, no padding -- End with: `🤖 Developed with assistance from [Claude Code](https://claude.ai/code)` -- A trailer naming the model, such as `Co-authored-by: Claude Opus 5 <noreply@anthropic.com>`, model name only - -Manual Testing Instructions link the pull request's Cloudflare preview, not a local dev server. Ask where the branch will be pushed, since the URL differs, and use a `REPLACE_ME`-style placeholder for anything not known yet: - -- `ddev/ddev.com`: `https://<branch-with-dashes>.ddev-com-front-end.pages.dev/`, cut to 28 characters -- A fork: `https://pr-<number>.ddev-com-fork-previews.pages.dev/` - -Only commit when explicitly requested by the user. - -**Never run `git push` (or any command that pushes to a remote), under any circumstances, even if explicitly asked.** The user always pushes their own branches/commits themselves. - -### Avoiding Hard Line Breaks in Issue/PR/Comment Bodies - -GitHub renders issue, PR, and comment bodies (`gh issue create`, `gh pr create`, `gh pr comment`, `gh issue comment`, etc.) with GFM's hard-line-break behavior: a single `\n` inside a paragraph becomes an actual `<br>`. This is different from how GitHub renders committed Markdown files (this file, docs, READMEs), which follow standard CommonMark, where a lone `\n` is just whitespace and the paragraph reflows to the container width. - -Hand-wrapping prose to a fixed column width — normal, good practice for a text file — produces a ragged, too-short-lined paragraph when posted as an issue/PR/comment body, because each wrapped line becomes its own forced line instead of reflowing. - -When writing a `--body-file` for any of these commands, write each paragraph as one continuous line with no embedded newlines. Only use actual blank lines to separate paragraphs, headings, and list items. This does not apply to code blocks, tables, or files meant to be read as source. - -Because a commit body here is reused verbatim as the pull request description, write commit bodies the same way: one continuous line per paragraph, rather than wrapping to a fixed column width as git convention would otherwise suggest. The same applies to any report a workflow generates and posts through `gh`. - -## Working with Claude Code - -### Branch Naming - -Use descriptive branch names that include: - -- Date in YYYYMMDD format -- Your GitHub username -- Brief description of the work - -Format: `YYYYMMDD_<username>_<short_description>` - -Examples: - -- `20250919_rfay_update_quickstart` -- `20250919_username_fix_blog_styling` -- `20250919_contributor_add_sponsor` - -### Whitespace and Formatting - -- **Never add trailing whitespace** - Blank lines must be completely empty (no spaces or tabs) -- Match existing indentation style exactly (spaces vs tabs, indentation depth) -- Preserve the file's existing line ending style -- Run linting tools to catch whitespace issues before committing - -## Architecture - -### Technology Stack - -- **[Astro](https://astro.build)** - Static site generator -- **[Tailwind CSS](https://tailwindcss.com)** - Utility-first CSS framework -- **[Tailwind Typography](https://tailwindcss.com/docs/typography-plugin)** - Typography plugin -- **[Heroicons](https://heroicons.com)** - Icon library -- **[Textlint](https://textlint.github.io)** - Content linting -- **[Giscus](https://giscus.app)** - GitHub-based commenting system - -### Project Structure - +Guidance for AI agents working on ddev.com, the static Astro site for +[DDEV](https://github.com/ddev/ddev), hosted on Cloudflare Pages. Setup without +DDEV is in `README.md`. + +## Where the rest of the guidance lives + +Read the file that matches what you are touching. Claude Code loads these on +its own. + +| Working on | Read | +| ------------------------------------------------- | ------------------------------------- | +| Blog posts (voice, frontmatter), authors, pages | `.claude/rules/content.md` | +| Markdown features (callouts, code blocks, images) | `MARKDOWN_FORMATTING.md` | +| Screenshots or feature images for a post | `.claude/skills/blog-images/SKILL.md` | +| Adding or updating a featured sponsor | `.claude/skills/add-sponsor/SKILL.md` | +| Bumping npm dependencies | `.claude/skills/bump-deps/SKILL.md` | +| A commit or PR | `.claude/skills/ddev-commit/SKILL.md` | +| A comment, in any code file | `.claude/rules/comments.md` | + +Fetch files from GitHub through `raw.githubusercontent.com`; a +`github.com/.../blob/...` page wraps the file in markup that costs tokens and +can be summarized instead of read. DDEV's +[organization-wide patterns](https://raw.githubusercontent.com/ddev/.github/main/AGENTS.md) +mostly restate this file, which **wins where they differ**, for example on +never pushing. + +## Claude Code automation + +<!-- +Maintainer note: anything the harness can enforce belongs in +.claude/settings.json, and guidance for one area belongs in a rule or skill. +See .claude/README.md. +--> + +`.claude/settings.json` enforces some rules in this file, so Claude Code should +treat them as facts about the environment rather than steps to repeat: + +- Before every `git commit`, the check-only `ddev npm run prettier` and + `ddev npm run textlint` run, and a failure or a stopped project blocks the + commit. +- Editing a file runs `ddev prettier` on it, and `ddev textlint` too under + `src/content/`. Errors they cannot fix are shown to Claude. +- `git push` is denied. + +## Commands + +Run everything through DDEV: + +```bash +ddev start # Installs dependencies, starts the Astro dev server +ddev npm run build # Production build to ./dist/ +ddev prettier [file] # Fix formatting, of the whole tree or the given files +ddev textlint [file] # Fix content wording in src/content/**, then report what is left +ddev logs # Dev server output, from the astro-dev-daemon ``` -├── cache/ # GitHub API response cache for local development -├── public/ # Static assets copied to dist/ -│ ├── logos/ # Sponsor and technology logos (prefer SVG) -│ └── _redirects # Cloudflare Pages redirects -├── src/ -│ ├── components/ # Reusable Astro components -│ ├── content/ # Content collections (blog, authors) -│ ├── layouts/ # Page layout wrapper -│ ├── lib/ # Utilities (GitHub API, search, read time) -│ ├── pages/ # Direct route mapping (.astro files) -│ └── styles/ # Global PostCSS styles -├── .env.example # Environment variables template -├── astro.config.mjs # Astro configuration -├── package.json # Dependencies and scripts -└── tailwind.config.cjs # Tailwind configuration -``` - -### Content Management - -The site uses Astro's Content Collections with strict schema validation: - -- **Blog posts**: `src/content/blog/*.md` - Markdown with frontmatter, validated against categories and author schemas -- **Authors**: `src/content/authors/*.md` - Author profiles with name, firstName, and optional avatarUrl -- **Static pages**: `src/pages/*.astro` - Direct route mapping - -### Content Schema - -Blog posts require: -- Valid author (must exist in authors collection) -- Categories from predefined list: Announcements, Community, DevOps, Performance, Guides, Newsletters, TechNotes, Training, Videos -- pubDate as Date object -- Optional featureImage with alt text +The dev server with hot reload is at `https://<projectname>.ddev.site:4321`, +and the last build at `https://<projectname>.ddev.site`. File arguments to +`ddev prettier` and `ddev textlint` are relative to the project root. CI runs +the check-only `npm run prettier` and `npm run textlint` on every PR. -For special markdown formatting features (callout boxes, code blocks, etc.), see [MARKDOWN_FORMATTING.md](MARKDOWN_FORMATTING.md). +## Before committing -### Content Linking +1. `ddev prettier` and `ddev textlint` +2. `ddev npm run build` when code, config, or dependencies changed; it also + fails on broken internal links +3. For new content, spell check it and check images as described in + `.claude/rules/content.md` -- **Internal blog links**: Use markdown filename references (e.g., `[link text](filename.md)`) for links between blog posts. Astro automatically resolves these to proper URLs. -- **Other internal links**: Use root-relative paths (e.g., `[Contact](/contact)`) for links to other site pages -- **External links**: Use full URLs for links outside the site - -### GitHub Integration - -The site fetches dynamic data from GitHub API: - -- Requires `GITHUB_TOKEN` environment variable -- Uses local `cache/` directory to reduce API calls during development -- Token needs: `repo`, `read:org`, `read:user`, `read:project` scopes - -### Sponsor Management - -Featured sponsors are managed in `src/featured-sponsors.json` with specific schema for logos, URLs, and types. This data generates sponsor displays and SVG badges used in the main DDEV repository. - -## Development Setup - -### DDEV Setup (Recommended) - -1. Run `ddev start` to start and set up the project's dependencies -2. Open `https://<projectname>.ddev.site:4321` in your browser -3. To rebuild static site: `ddev npm run build` -4. Static site available at: https://<projectname>.ddev.site - -### Setup Without DDEV - -1. Run `nvm use` to use appropriate Node.js version -2. Run `npm install` to install dependencies -3. Run `npm run dev` to start development server -4. Visit `http://localhost:4321/` - -### GitHub Token Setup - -For dynamic GitHub data (not required for blog posts): - -1. Run `cp .env.example .env` -2. Create [classic GitHub access token](https://github.com/settings/tokens) with scopes: `repo`, `read:org`, `read:user`, `read:project` -3. Add token to `.env` as `GITHUB_TOKEN=your_token_here` - -## Content Creation - -### Blog Posts - -Template for new blog posts in `src/content/blog/`: - -```markdown ---- -title: "Post Title" -pubDate: 2023-01-01 -summary: Brief description -author: Author Name -featureImage: - src: /img/blog/kebab-case.jpg - alt: Descriptive alt text - caption: Optional caption - credit: Optional credit -categories: - - Category Name ---- - -Post content here... -``` - -**Categories**: Announcements, Community, DevOps, Performance, Guides, Newsletters, TechNotes, Training, Videos - -**Images**: Production-ready, <2MB, reasonable dimensions, optimized - -**Logo/text banner images**: For a `featureImage` that combines project logos and/or text on a solid background, see [FEATURE_IMAGE_GUIDE.md](FEATURE_IMAGE_GUIDE.md). - -### Terminal Screenshots - -For screenshots of DDEV command output (`ddev list`, `ddev st`, the `ddev` dashboard), render them with [VHS](https://github.com/charmbracelet/vhs) (`brew install vhs`, which also needs `ttyd` and `ffmpeg`). Work in `~/tmp`, then copy the final image into `public/img/blog/YYYY/MM/`. - -- Write a `.tape` file that hides the `cd` and `clear`, shows the command, sleeps a few seconds, and ends with `Screenshot`. Run it as `bash -c "cd ~/tmp/shots && timeout 100 vhs name.tape"` with stdin from `/dev/null`. -- The `Screenshot` directive can silently produce nothing. The GIF is still written, so take its last frame: `magick name.gif -coalesce -delete 0--2 +repage name.png`. -- Use a terminal about 820px wide (`Set Width 820`, `Set FontSize 16`) for `ddev list`. `ddev st` needs about 900px wide and 1000px tall. At 640px DDEV's tables overflow the right edge. Too short a height scrolls the top of the output off. -- Crop with `magick in.png -crop WxH+0+0 +repage out.png`. To leave room for callout arrows, add space above with `-background "<bg color>" -gravity north -splice 0x70`. -- The OSC 8 hyperlinks DDEV prints appear underlined in the VHS terminal, which is how to show "clickable" output. DDEV only emits them when stdout is a terminal, so text captured from a pipe or file has no links (set `FORCE_HYPERLINK=1` to force them). -- Do not use `freeze` (charmbracelet) for DDEV tables: it draws the box characters badly. A hover or click state needs a real terminal such as iTerm2 and must be captured by hand. -- Keep the screenshots under 2MB, use a descriptive alt text, and view the result before adding it. If reads under `public/` are denied, copy the image to `~/tmp` and view it there. - -### SVG Feature Images - -An illustration can be a hand-written SVG referenced directly from `featureImage.src`. Generate it from a script kept in `~/tmp` (not the repository), preview it with `rsvg-convert -w 1672 -o out.png in.svg`, and install only the `.svg`. Match the 1672x940 size of the other feature images and leave the left side clear for the title. - -### Authors - -Add new authors to `src/content/authors/` with schema: - -- name (must match blog post frontmatter) -- firstName -- avatarUrl (optional) - -### Pages - -Add `.astro` files to `src/pages/` where filename becomes URL slug. - -## Quality Control - -### Textlint - -- Configuration in `.textlintrc` -- Runs against `src/content/**` -- Enforces consistent language and terminology -- Run `ddev textlint` before committing - -### Prettier - -- Configuration in `.prettierrc` -- Auto-formats code -- Run `ddev prettier` before committing -- VS Code: Auto-format on save enabled - -### Recommended VS Code Extensions - -- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) -- [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) -- [Astro](https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode) - -## Build & Deployment - -- GitHub Actions tests on every push to main -- Cloudflare Pages automatically builds and deploys from main branch -- Preview builds created for all PR branches -- Redirects managed via `public/_redirects` file - -### Secrets - -Production requires `GITHUB_TOKEN` environment variable in Cloudflare Pages settings. - -## Important Notes - -- Always preserve existing code style and component patterns -- Blog images should be production-ready: optimized, < 2MB, reasonable dimensions -- All content goes through textlint validation for consistency -- The site is configured for DDEV development with special CORS and host settings -- Use kebab-case for blog post filenames -- Prefer SVG logos in `public/logos/` -- Internal blog links use markdown filename references -- External links use full URLs - -## Resources +## Architecture -- [Astro Documentation](https://docs.astro.build) -- [DDEV Documentation](https://docs.ddev.com/) -- [Contributing to ddev.com Training](https://ddev.com/blog/ddev-website-for-contributors/) +The layout under `src/` is standard Astro (`components/`, `content/`, +`layouts/`, `lib/`, `pages/`, `styles/`). The parts that are not obvious: + +- Content collections are validated by `src/content.config.ts`, so a bad + author or category fails the build. +- `src/lib/api.ts` fetches GitHub data with `GITHUB_TOKEN` from `.env` (see + `.env.example` and `README.md`) and caches responses in `cache/` during + development. Content work needs no token; without one, sponsorship data + falls back to sample data. +- `src/featured-sponsors.json` also generates the SVG sponsor badges + (`src/pages/resources/featured-sponsors*.svg.js`) used in the main DDEV + repository's README. +- Redirects and short links are in `public/_redirects`. + +## Writing style + +Applies to conversation, commit messages, PR text, content, and comments: + +- Direct, concise language. State findings plainly, including what failed or + was not verified. +- **Never use any of these, in any form:** `comprehensive`, `complete`, + `full`, `entire`, `thorough`, `detailed`, `seamless`, `genuine`, + `genuinely`, `honest`, `honestly`, `truly`, `really` (as an intensifier), + `perfect`, `perfectly`, `robust`, `powerful`, `effortless`, + `production-ready`, `tremendous`, `dramatically`, `revolutionary`, `delve`, + `elevate`, `unleash`. They assert importance instead of showing it; delete + the word, and if that changes the meaning, the claim needed evidence. +- No flattery (`You're absolutely right`, `Great question`), no introductory + phrases ("I'd be happy to"), and no closing summary unless asked. Lead with + the substance. +- Prefer `jq` over Python for JSON in shell commands. + +## Files + +- **Never add trailing whitespace.** Empty lines must contain no spaces or tabs. +- Match the file's indentation and line endings, and the surrounding + component patterns. Prettier has no Astro plugin here, so nothing formats + `.astro` files; match their style by hand. +- Use kebab-case for blog post filenames, and SVG for logos in + `public/logos/`. +- Put temporary files and scripts in `~/tmp`, not the repository. + +## Git workflow + +**Commit only when asked, and never run `git push`** or anything else that +pushes to a remote, even when asked. The maintainer pushes. + +Branch names are `YYYYMMDD_<username>_<short_description>`, for example +`20250919_rfay_update_quickstart`. Commit titles follow Conventional Commits, +and the commit body is reused as the PR description; see +`.claude/skills/ddev-commit/SKILL.md` before writing either. diff --git a/FEATURE_IMAGE_GUIDE.md b/FEATURE_IMAGE_GUIDE.md deleted file mode 100644 index 8df980033..000000000 --- a/FEATURE_IMAGE_GUIDE.md +++ /dev/null @@ -1,203 +0,0 @@ -# Building a Logo + Text Feature Image - -This guide covers building a `featureImage` banner for a blog post by compositing official -project logos (and optional caption text) on a solid background, using command-line tools -instead of a design app. It's the right approach for posts about integrations between two -tools/projects (e.g. "Using DDEV with X"), where recognizable brand marks communicate the -topic faster than a screenshot. - -This produces a raster PNG via ImageMagick and `rsvg-convert`. It does not require Photoshop, -Figma, or any GUI tool — everything below runs from the shell. - -## Prerequisites - -```bash -brew install librsvg # provides `rsvg-convert`; ImageMagick's own SVG delegate falls back to a - # much lower-quality renderer when this isn't installed -which rsvg-convert # confirm it's on PATH before continuing -which magick # ImageMagick 7 (the `magick` binary); `convert` also works on older installs -``` - -## Step 1: Get official SVG logos, not recreations - -Always prefer the project's own brand assets over hand-drawn approximations — colors, exact -shapes, and stroke weights matter for recognizability, and an approximation will look "off" to -anyone familiar with the brand. - -- DDEV's official logos live in the `ddev/ddev` repository: - `https://raw.githubusercontent.com/ddev/ddev/main/docs/content/developers/logos/SVG/Logo_w_text.svg` - (also `Logo.svg` for the icon mark alone, and dark-background variants). -- Other projects usually publish a brand/press page (search "`<project>` brand assets SVG"). Check the - page's rendered HTML for the logo's actual asset URL — view source or `curl` the page and `grep` - for `.svg`: - - ```bash - curl -fsSL "https://example.com/brand-page" | grep -o '[^"'"'"' ]*Logo[^"'"'"' ]*\.svg' - ``` - -Download the SVG(s) into a scratch directory (not the repository) — these are intermediate build -inputs, not site assets: - -```bash -curl -fsSL "<svg-url>" -o /path/to/scratch/typo3-logo.svg -``` - -## Step 2: Render each SVG to PNG with `rsvg-convert` directly - -**Do not** rasterize via `magick some.svg -resize WIDTHx out.png`. For SVGs with a small -viewBox, ImageMagick's resize-density heuristics can rasterize at a lower internal resolution -than requested and then upscale the result, producing visibly blurry/jagged edges — this is a -real bug we hit, confirmed by diffing the two render paths (`magick compare -metric AE`) and -finding ~24% of pixels differed between them. - -Instead, invoke `rsvg-convert` directly and let it rasterize the vector at the exact final -pixel size: - -```bash -rsvg-convert -w 1800 -o typo3-raw.png typo3-logo.svg -rsvg-convert -w 1800 -o ddev-raw.png ddev-logo.svg -``` - -Render at (at least) 2x the pixel size you expect to display the final image at. The site may -display the image wider than its native resolution on high-DPI screens, and upscaling a -low-resolution raster always looks softer than downscaling a high-resolution one. - -## Step 3: Trim each render to its true visual bounding box - -SVG viewBoxes often have inconsistent padding between different logos (or even within the -same logo when you add elements to it later). If you center images by their nominal canvas -size instead of their actual ink, unrelated logos end up visually misaligned even though -they're "centered" — this happened when a wordmark's baseline didn't match a mark-only icon's -optical center. - -```bash -magick typo3-raw.png -trim +repage typo3-trim.png -magick ddev-raw.png -trim +repage ddev-trim.png -``` - -Always trim before you reason about width/height/centering. Check the result: - -```bash -identify -format "%wx%h\n" typo3-trim.png -``` - -## Step 4: Build any caption text as its own trimmed image - -If you're adding a caption under a logo (e.g. a command name like "share"), render the text -separately, trim it, then stack it onto the logo with a transparent spacer for the gap — this -keeps the gap size deterministic and keeps both elements centered relative to each other -regardless of their individual widths: - -```bash -FONT="/System/Library/Fonts/Supplemental/Arial Bold.ttf" - -magick -background none -fill "#1e2127" -font "$FONT" -pointsize 190 label:"share" \ - -trim +repage text-caption.png - -magick -size 1800x30 xc:none spacer.png # width matches the logo; height is the gap - -magick -gravity center ddev-trim.png spacer.png text-caption.png \ - -background none -append -trim +repage ddev-block.png -``` - -`-gravity center` before `-append` is required — without it, `-append` left-aligns images of -different widths instead of centering them. - -## Step 5: Pick a canvas size that won't get cropped - -Check how the site actually displays `featureImage` before choosing dimensions — a banner that -looks right full-size can get clipped in a smaller UI element. On ddev.com specifically: - -- `FeatureImage.astro` (the full blog post hero) renders at `w-full h-auto` — any aspect ratio - is safe there. -- `BlogPostCard.astro` (the blog listing thumbnail) renders with `aspect-3/2 object-cover` — - this **crops** any image whose aspect ratio is wider than 3:2. The safe content width is - `canvas_height * 1.5`; anything outside that horizontally gets sliced off in card view. - -The simplest fix is to make the canvas exactly 3:2 (e.g. `1500x1000`, `2400x1600`) so there's -never anything to crop, on either surface. - -```bash -magick -size 2400x1600 xc:"#f7f8fa" banner-bg.png -``` - -## Step 6: Choose a layout that matches the logos' natural shape - -- **Wide/short wordmarks** (most "logo + text" lockups, aspect ratio ~3:1–4:1): stack them - top-to-bottom. Side by side, two wordmarks plus a margin/gap rarely fit a reasonable canvas - width without shrinking both logos down to a sliver; stacked, each one can span most of the - canvas width and end up dramatically larger. -- **Square-ish icon marks** (aspect ratio near 1:1): side by side with a small connecting - element (arrow, "+", "×") works well, since neither logo dominates the layout. - -Composite with `-gravity North` (or `South`/`West`/`East`) and explicit pixel offsets computed -from the trimmed dimensions from Step 3–4: - -```bash -magick banner-bg.png \ - typo3-trim.png -gravity North -geometry +0+210 -composite \ - ddev-block.png -gravity North -geometry +0+801 -composite \ - final-banner.png -``` - -Do the arithmetic explicitly rather than eyeballing offsets: sum up margin, logo height, gap, -and block height, confirm the total is less than the canvas height, then split the leftover -space into top/bottom margins. - -## Step 7: Verify placement and color numerically — don't just eyeball it - -If you (the agent) can't visually preview the rendered PNG, use ImageMagick to sample pixels -and bounding boxes instead of guessing: - -```bash -# Confirm a specific brand color lands where a logo should be -magick final-banner.png -format "%[pixel:p{420,450}]" info: - -# Find the bounding box of non-background content in a region (catches misplaced/oversized -# elements, or elements that silently didn't render) -magick final-banner.png -crop 2400x150+0+665 +repage -fuzz 3% -transparent "#f7f8fa" \ - -format "%@" info: - -# Diff two renders to catch regressions (e.g. before/after switching rasterizers) -magick compare -metric AE before.png after.png null: -``` - -This turns "does it look right" into a checkable assertion, and it's how the rsvg-convert bug -in Step 2 was actually caught and confirmed fixed. - -## Step 8: Save, wire up, and lint - -```bash -mkdir -p public/img/blog/YYYY/MM -cp final-banner.png public/img/blog/YYYY/MM/descriptive-name.png -``` - -```yaml -featureImage: - src: /img/blog/YYYY/MM/descriptive-name.png - alt: Descriptive alt text naming what's actually in the image -``` - -Then run the site's normal content checks: - -```bash -ddev npm run prettier:fix -ddev npm run textlint -``` - -Clean up intermediate SVGs/PNGs from the scratch directory — only the final composited PNG -belongs in the repository. - -## Common pitfalls (all hit while building this workflow) - -- **Missing `rsvg-convert`** makes ImageMagick fall back to its own SVG renderer, which doesn't - handle `clip-path` correctly — logos using it can come out visibly broken (parts of shapes - will be wrong/missing). Install `librsvg` first. -- **Rasterizing via `magick file.svg -resize`** instead of `rsvg-convert -w` can produce - blurry/jagged output for small-viewBox SVGs — always render directly at the target size. -- **Centering by nominal canvas size instead of trimmed bounds** misaligns logos with different - amounts of internal padding — always `-trim` before positioning. -- **Canvas aspect ratio wider than the site's card-thumbnail crop ratio** gets its edges cut - off in listing views, even though it looks fine on the full post page — match the crop ratio. -- **Final resolution too close to intended display size** looks soft once the browser scales - it up on a high-DPI screen — render at 2x+ the expected display size. diff --git a/MARKDOWN_FORMATTING.md b/MARKDOWN_FORMATTING.md index aa20064bb..3cb67626a 100644 --- a/MARKDOWN_FORMATTING.md +++ b/MARKDOWN_FORMATTING.md @@ -175,7 +175,7 @@ Strike through text using double tildes: ### Tables -Create tables using pipes and hyphens (already documented below in the standard markdown section). +Create tables with pipes and hyphens, as on GitHub. Each table is wrapped in a `div.table-wrapper`, so a wide table scrolls horizontally instead of overflowing the page. ### Task Lists @@ -282,12 +282,12 @@ Use full URLs: **Automatic Security Enhancement:** -All external links (links to domains other than ddev.com) automatically receive: +Every absolute `http` or `https` link, including one to `https://ddev.com`, automatically receives: - `target="_blank"` - Opens in a new tab - `rel="noopener noreferrer"` - Security attributes to prevent potential exploits -You don't need to add these attributes manually. Internal links (to ddev.com pages) are not affected. +You don’t need to add these attributes manually. Root-relative links such as `/blog/` are not affected, so use them for pages on this site. ## Images @@ -302,15 +302,19 @@ Images are automatically wrapped in semantic HTML `<figure>` elements with capti This automatically generates: ```html -<figure> +<figure class="rehype-figure"> <img - src="/img/blog/2022/03/macos-m1-vs.-drupal-drush-install-seconds.png" alt="Descriptive alt text" + loading="lazy" + decoding="async" + src="/_astro/my-image.<hash>.webp" /> <figcaption>Descriptive alt text</figcaption> </figure> ``` +PNG, JPEG, and GIF images under `/img/` are converted to WebP at build time. SVG images are not converted. + The alt text serves dual purposes: 1. Accessibility for screen readers @@ -397,12 +401,11 @@ The site uses Tailwind Typography for consistent, beautiful text rendering: Check out these resources for markdown formatting examples: -- **Demo Post** - A complete demonstration of all available formatting features (hidden from blog listings) +- **Demo Post** - A demonstration of the available formatting features, dated 2022 so it does not appear among recent posts - [Rendered version](https://ddev.com/blog/markdown-features-demo) - See how the features look on the site - [Source code](https://github.com/ddev/ddev.com/blob/main/src/content/blog/markdown-features-demo.md) - View the raw markdown - Any post in `src/content/blog/` that uses the `:::` directive syntax - Posts using blockquote-style callouts with `> **Label**:` format -- [Mermaid diagram documentation](https://mermaid.js.org/intro/) ## Questions? diff --git a/README.md b/README.md index 567bf1b59..26e886586 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,84 @@ -# ddev.com Astro code +<div align="center"> -Source code for [ddev.com](https://ddev.com)’s static front end, built with [Astro](https://astro.build) to keep things organized, maintainable, and fast. +<a href="https://ddev.com"> + <picture> + <source media="(prefers-color-scheme: dark)" srcset="public/logos/dark-ddev.svg"> + <img alt="DDEV" src="public/logos/ddev.svg" width="320"> + </picture> +</a> -## Overview +### Source code for ddev.com -### Main Ingredients +The static site for [DDEV](https://github.com/ddev/ddev), built with [Astro](https://astro.build) and [Tailwind CSS](https://tailwindcss.com), and hosted on Cloudflare Pages. -- [Astro](https://astro.build) -- [Tailwind CSS](https://tailwindcss.com) -- [Tailwind Typography](https://tailwindcss.com/docs/typography-plugin) plugin -- [Heroicons](https://heroicons.com) -- [Textlint](https://textlint.github.io) -- [Giscus](https://giscus.app) +[![Website](https://img.shields.io/badge/website-ddev.com-blue)](https://ddev.com) +[![Test](https://img.shields.io/github/actions/workflow/status/ddev/ddev.com/test.yml?branch=main&label=test)](https://github.com/ddev/ddev.com/actions/workflows/test.yml) +[![Discord](https://img.shields.io/discord/664580571770388500?logo=discord&logoColor=%23fff&label=Discord&link=https%3A%2F%2Fddev.com%2Fs%2Fdiscord)](https://ddev.com/s/discord) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) -### Project Structure +[**Contributor Training**](https://ddev.com/blog/ddev-website-for-contributors/) · [**Markdown Formatting**](MARKDOWN_FORMATTING.md) · [**Agent Guidance**](AGENTS.md) · [**Sponsor DDEV**](https://ddev.com/sponsor) -The file structure follows a typical Astro [project layout](https://docs.astro.build/en/core-concepts/project-structure/). +</div> -Most pages are built with [Astro components](https://docs.astro.build/en/core-concepts/astro-components/), while blog posts and authors are sourced from local Markdown that’s validated with tidy schemas we get using [content collections](https://docs.astro.build/en/guides/content-collections/). - -- **`cache/`** – custom, project-specific folder for caching GitHub responses in local developent to reduce API calls. -- **`public/`** – images and [redirects](https://developers.cloudflare.com/pages/platform/redirects) that will be copied verbatim into the generated `dist/` directory. -- **`src/`** – components, layouts, styles, and supporting TypeScript/JavaScript. - - **`components/`** – individual `.astro` components used in pages. (You can also use [components for UI frameworks](https://docs.astro.build/en/core-concepts/framework-components/) like Vue, React, and Svelte!) - - **`content/`** – configuration and Markdown for the blog’s [content collections](https://docs.astro.build/en/guides/content-collections/). - - **`layouts/`** – contains the single component we use for every page. - - **`lib/`** – helper code for fetching data from GitHub, building the search index, injecting read time into frontmatter, and handling common formatting. - - **`pages/`** – `.astro` pages whose filenames directly translate into routes for the site. - - **`styles/`** – global PostCSS that’s not already handled by the [Tailwind plugin](https://docs.astro.build/en/guides/integrations-guide/tailwind/). -- **`.env.example`** – file you’ll want to rename `.env` and populate for a new environment. -- **`.nvmrc`** – Node.js version to support `nvm use`. -- **`.prettierrc`** – rules for [Prettier](https://prettier.io) code formatting. -- **`astro.config.mjs`** – Astro configuration. -- **`package.json`** – standard file that details the project’s packages and versions. -- **`README.md`** – you are here! 👋 -- **`tailwind.config.cjs`** – [configuration for Tailwind](https://tailwindcss.com/docs/configuration) and the Tailwind Typography plugin we’re using. -- **`tsconfig.json`** – [TypeScript configuration](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). - -## Development - -### Commands - -All commands are run from the root of the project, from a terminal: +--- -| Command | Action | -| :--------------------- | :------------------------------------------------- | -| `npm install` | Installs dependencies | -| `npm run dev` | Starts local dev server at `localhost:3000` | -| `npm run build` | Build your production site to `./dist/` | -| `npm run preview` | Preview your build locally, before deploying | -| `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` | -| `npm run astro --help` | Get help using the Astro CLI | -| `npm run prettier` | Run prettier in the project root | -| `npm run prettier:fix` | Apply fixable updates to resolve prettier errors | -| `npm run textlint` | Run textlint on content collections | -| `npm run textlint:fix` | Apply fixable updates to resolve textlint errors | +## Project Structure -### Local Development Setup +The layout follows Astro’s [project structure](https://docs.astro.build/en/basics/project-structure/). The parts specific to this site: -#### DDEV setup +- **`cache/`** – GitHub API responses cached during local development, to reduce API calls. +- **`public/`** – images, logos, and [redirects](https://developers.cloudflare.com/pages/configuration/redirects/), copied as they are into `dist/`. +- **`src/content/`** – Markdown for the blog posts and authors, validated by the [content collections](https://docs.astro.build/en/guides/content-collections/) schemas in `src/content.config.ts`. +- **`src/pages/`** – `.astro` and `.mdx` pages whose filenames become routes. +- **`src/layouts/`** – `Layout.astro`, used by every page, and `MarkdownLayout.astro`, used by the `.mdx` pages. +- **`src/lib/`** – GitHub API fetching, the search index, and the remark and rehype plugins for blog Markdown. +- **`src/featured-sponsors.json`** – the featured sponsors, see [Sponsor Management](#sponsor-management). -DDEV already has all the dependencies included. +## Development -1. Run `ddev start` to start and set up the project’s dependencies. -2. Open `https://<projectname>.ddev.site:4321` in your browser. +### DDEV Setup -To rebuild a static copy of the site, run `ddev npm run build`. The contents of the `dist/` folder are what gets [deployed to Cloudflare Pages](#build--deployment) and can be found at `https://<projectname>.ddev.site`. The dev server runs on a `web_extra_daemons`, it includes Vite HMR (hot module reloading) among other features, and it can be found at `https://<projectname>.ddev.site:4321`. +DDEV includes all the dependencies. -Troubleshooting steps: Check `ddev logs`. +1. Run `ddev start`. It installs dependencies and starts the dev server. +2. Open `https://<projectname>.ddev.site:4321`. The dev server reloads as you edit. -#### Setup without DDEV +| Command | Action | +| :--------------------- | :-------------------------------------------------------------------------------- | +| `ddev npm run build` | Build the production site to `dist/`, served at `https://<projectname>.ddev.site` | +| `ddev prettier [file]` | Fix formatting of the whole tree, or of the given files | +| `ddev textlint [file]` | Fix wording in `src/content/**`, or in the given files, then report what is left | +| `ddev logs` | Dev server output, for troubleshooting | -Check out the project in your favorite Node.js environment, ideally running [`nvm`](https://github.com/nvm-sh/nvm). We’ll install dependencies, add a GitHub API key, and run a local dev server with a hot-reloading browser URL. +Run `ddev prettier` and `ddev textlint` before committing. CI runs the same checks. -1. Run `nvm use` to make sure you’re running an appropriate Node.js version. -2. Run `npm install` to set up the project’s dependencies. -3. Run `npm run dev` to start Astro’s dev server. If it fails then run `npm cache clean --force && npm install && npm run dev`. -4. Visit the URL displayed in your terminal. (Probably `http://localhost:4321/`.) The site will automatically refresh as you work on it, displaying errors in the relevant terminal or browser console. +### Setup Without DDEV -To generate a static copy of the site, run `npm run build`. The contents of the `dist/` folder are exactly what get [deployed to Cloudflare Pages](#build--deployment). You can preview locally by running `npm run preview` or using a tool like [`serve`](https://www.npmjs.com/package/serve). +1. Run `nvm use` to use the Node.js version in `.nvmrc`. +2. Run `npm install`. +3. Run `npm run dev`, and open `http://localhost:4321/`. If it fails, run `npm cache clean --force && npm install && npm run dev`. -#### Switching from Without DDEV to with DDEV +`npm run build` builds to `dist/`, and `npm run preview` serves the build. `npm run prettier:fix` and `npm run textlint:fix && npm run textlint` replace the DDEV commands above. -Make sure to delete your `node_modules/` directory and run `ddev npm install`. The change in architecture can create odd issues otherwise. +When switching from this setup to DDEV, delete `node_modules/` and run `ddev npm install`, since the two architectures can conflict. -#### GitHub Token +### GitHub Token -This step is not required if you just want to contribute a blog post to ddev.com. +Not needed to contribute a blog post. Contributors, sponsors, releases, and other DDEV data come from the GitHub API; without a token, sponsorship data falls back to sample data. To use the real data: -Contributors, sponsors, releases and more data about DDEV is retrieved dynamically from the GitHub API. To test this, please follow these steps: +1. Run `cp .env.example .env`. Don’t commit `.env`. +2. Create a [classic GitHub access token](https://github.com/settings/tokens) with the scopes `repo`, `read:org`, `read:user`, and `read:project`. +3. Paste the token after `GITHUB_TOKEN=` in `.env`. -1. Run `cp .env.example .env` to create a `.env` file for environment variables. (Don’t check this in!) -2. Create a [classic GitHub access token](https://github.com/settings/tokens) with these scopes: `repo`, `read:org`, `read:user`, and `read:project`. -3. Paste the GitHub token after `.env`’s `GITHUB_TOKEN=`. +### Editor Setup -There is a local `cache/` to reduce API calls. +`.editorconfig` and `.prettierrc` hold the formatting rules. VS Code suggests the extensions in `.vscode/extensions.json` (Prettier, EditorConfig, Astro), and `.vscode/settings.json` formats on save. ## Managing Content -The site’s content lives in either `.astro` components that resemble souped-up HTML, or Markdown files organized into schema-validated [content collections](https://docs.astro.build/en/guides/content-collections/). - -### Blog Posts and Guest Blog Posts - -Hint: There's a full contributor training on [contributing to ddev.com](https://ddev.com/blog/ddev-website-for-contributors/). +### Blog Posts -Blog posts are Markdown files with frontmatter that live in `src/content/blog/`. - -For details on special markdown formatting features like callout boxes, see [MARKDOWN_FORMATTING.md](MARKDOWN_FORMATTING.md). - -To add a new blog post, use this Markdown as a template: +Blog posts are Markdown files in `src/content/blog/`, named with a kebab-case slug, for example `my-new-post.md`. For callouts, code blocks, images, and other features, see [MARKDOWN_FORMATTING.md](MARKDOWN_FORMATTING.md). Use this frontmatter: ```markdown --- @@ -119,7 +89,7 @@ modifiedComment: "This got updated" summary: author: Randy Fay featureImage: - src: /img/blog/kebab-case.jpg + src: /img/blog/2026/01/kebab-case.jpg srcDark: alt: caption: @@ -129,99 +99,40 @@ categories: --- ``` -Name your file with a kebab-case, URL-and-SEO-friendly slug with a `.md` extension, and drop it in the `src/content/blog/` directory. - -Give it a succinct title, and if you include a feature image be sure to write descriptive alt text along with an optional caption and image credit. The `caption:` and `credit:` fields can both use Markdown, but you’ll probably need to wrap the whole value in straight quotes (`"`). - -The Astro build doesn’t do any fancy image sizing or optimization, so be sure any images you add are production-ready: an appropriate format for the image type (JPEG, PNG, or SVG), with size no larger than ~1–2MB and dimensions no greater than 2000px or so. Use an app like [ImageOptim](https://imageoptim.com) to quickly apply lossless compression. +- `author` must match the `name` of an author in `src/content/authors/`. Add one there for a new author. +- Write descriptive `alt` text for the feature image. `caption` and `credit` can use Markdown, wrapped in straight quotes (`"`). +- Choose categories from `allowedCategories` in `src/content.config.ts`. The first one shows on post cards: _Add-ons_, _Announcements_ (releases, organization news), _Community_ (events, third-party developments), _DevOps_ (workflows, infrastructure), _Performance_ (benchmarks, tips), _Guides_ (how-to posts), _Newsletters_, _TechNotes_ (code-level discussions), _Training_ (contributor training), _Videos_. +- Put 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 keep them under 2MB and no wider than about 2000px. [ImageOptim](https://imageoptim.com) applies lossless compression. -Choose whichever categories apply, with special attention to the first because it’ll be displayed on post summary cards: - -- _Add-ons_ (Info about add-ons) -- _Announcements_ (releases, organization news, etc.) -- _Community_ (events, third-party developments, etc.) -- _DevOps_ (workflows, infrastructure, etc.) -- _Performance_ (benchmarking, tips, etc.) -- _Guides_ (how-to style posts) -- _Newsletters_ (monthly newsletters) -- _Podcasts_ (podcasts) -- _Releases_ (new features, bug fixes, etc.) -- _Showcase_ (showcase of DDEV projects) -- _Tutorials_ (tutorials) -- _Videos_ (videos) -- _TechNotes_ (more technical code-level discussions) -- _Training_ (contributor training) -- _Videos_ (posts that include or primarily feature video content) - -> 💡 **If you’re publishing work from a new author**, add an entry for them in `src/content/authors/`! The `"name"` value needs to match the one you’re using in your post frontmatter. - -Blog comments are managed by [giscus integration](https://github.com/ddev/giscus-comments). +Blog comments use [giscus](https://github.com/ddev/giscus-comments). ### Pages -Add a `.astro` file to the `pages/` directory, where its name will become the page slug. Use an existing page to grab and re-use whatever layout and components you can to save yourself time and encourage consistency with the rest of the site. - -If you need to dynamically add multiple pages, see files with brackets like `src/blog/[page].astro`, `src/blog/category/[slug].astro`, and `src/blog/author/[slug].astro` for examples. +Add a `.astro` or `.mdx` file to `src/pages/`, and its name becomes the URL. Reuse the layout and components of an existing page. For generated pages, see `src/pages/blog/[page].astro`, `src/pages/blog/category/[slug].astro`, and `src/pages/blog/author/[id].astro`. ### Textlint -A basic textlint configuration lives in `.textlintrc` and runs against `src/content/**` to try and help keep language consistent and accurate. This doesn’t yet conform to the DDEV docs [spellcheck rules](https://github.com/ddev/ddev/blob/main/.spellcheck.yml) and [massive exclusion list](https://github.com/ddev/ddev/blob/main/.spellcheckwordlist.txt), but ideally the two can someday converge. - -Textlint’s [default terminology](https://github.com/sapegin/textlint-rule-terminology/blob/master/terms.jsonc) catches a lot of accepted best practices on its own, where the only major override is to allow “website” (instead of its suggested “site”) because it’s rampant in blog posts and documentation. Same with the “front end” and “back end” conundrum and two-word “command line”. - -Run `ddev textlint` before committing your changes. - -### Prettier and EditorConfig - -Prettier is used for auto-formatting files, see `.prettierrc`. EditorConfig is used for basic IDE settings, see `.editorconfig`. The EditorConfig configuration is [automatically parsed by Prettier](https://prettier.io/docs/en/configuration.html#editorconfig). - -If you work with Visual Studio Code, please install these three extensions: - -- https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode -- https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig -- https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode - -Automatically "Format on Save" setting is activated via `.vscode/settings.json` for Visual Studio Code. - -Run `ddev prettier` before committing your changes. +`.textlintrc` checks `src/content/**` for terminology and stop words, using textlint’s [default terminology](https://github.com/sapegin/textlint-rule-terminology/blob/master/terms.jsonc) with a few overrides, such as allowing “website”, “front end”, and “command line”. ### Sponsor Management -How to add a new featured sponsor: see [instructions](README_SPONSOR.md). +`src/featured-sponsors.json` lists the featured sponsors shown on the [home page](https://ddev.com/#supporters) and in the [light](https://ddev.com/resources/featured-sponsors.svg) and [dark](https://ddev.com/resources/featured-sponsors-darkmode.svg) badges generated for the [main DDEV README](https://github.com/ddev/ddev#sponsor-ddev). To add one, follow [.claude/skills/add-sponsor/SKILL.md](.claude/skills/add-sponsor/SKILL.md), or ask Claude Code to add the sponsor’s website. -The `src/featured-sponsors.json` file is used for manually curating prominent sponsors. +## Redirects and Short Links -While it’s a bit of a pain and [still relies on coercion](https://github.com/ddev/ddev.com/blob/main/src/components/FeaturedSponsors.astro#L4-L20) in some places, it lets us collect pristine, brand-friendly resources in one place and use them in different contexts. +Add redirects to `public/_redirects`. They can point to pages on the site, the DDEV docs, or external resources. -It’s used to display sponsor details in a few places: +- Most redirects should be `301`, a permanent redirect. +- Prefix short links with `/s`, for example `/s/port-conflict`. -1. The [homepage](https://ddev.com) “Featured Sponsors” list. -2. The [procedurally-generated](https://github.com/ddev/ddev.com/blob/main/src/pages/resources/featured-sponsors.svg.js) featured sponsors [light](https://ddev.com/resources/featured-sponsors.svg) and [dark](https://ddev.com/resources/featured-sponsors-darkmode.svg) SVG images used in the [main project readme](https://github.com/ddev/ddev#wonderful-sponsors). +## Build and Deployment -## Redirects/Short Links/Shortcuts - -Any redirect can be added to ddev.com by editing `public/_redirects`. This can be useful to provide short redirects in a variety of contexts. Redirects can be to local URLs, DDEV docs, or external resources. - -- Most redirects should be listed as `301` for a permanent redirect. -- Short links can be prefixed with `/s` to imply their nature. For example, `/s/port-conflict` - -## Build & Deployment - -For the site to exist at `ddev.com`, it needs to be built and hosted somewhere. Cloudflare Pages responds to commits in order to build and deploy the site. - -On every push to the `main` branch, the following happens: - -- GitHub Actions tests the site using [this workflow](https://github.com/ddev/ddev.com/blob/main/.github/workflows/test.yml). -- [Cloudflare Pages](https://pages.cloudflare.com) runs `npm run build`, and deploys the resulting output from `dist/`. - - Cloudflare Pages is also configured to build previews for branches on this repository. It will automatically add a comment with the build status and eventual URL(s) to any PR. +- GitHub Actions runs [the test workflow](.github/workflows/test.yml) on every push to `main` and every pull request. +- [Cloudflare Pages](https://pages.cloudflare.com) runs `npm run build` on every push to `main` and deploys `dist/`. It also builds a preview for each branch and comments the URL on its PR. Pull requests from forks get previews from GitHub Actions instead, see [FORK_PREVIEW_SETUP.md](.github/FORK_PREVIEW_SETUP.md). ### Secrets -The site [uses Octokit to make REST and GraphQL API requests](https://github.com/ddev/ddev.com/blob/main/src/lib/api.ts) for repository and contribution details from github.com. It needs an API token to authenticate these requests to function and avoid hitting quota limits. - -GitHub supplies its own private `GITHUB_TOKEN` in the GitHub Actions build environment. In any other environment, including local development, you’ll need to populate a `GITHUB_TOKEN` environment variable with a **classic** GitHub personal access token that has `repo`, `read:org`, `read:user`, and `read:project` scopes. - -A valid Personal Access Token (PAT) must also be supplied to [Cloudflare](https://dash.cloudflare.com/2aecb1c6b99f9d2274b12efc45152be2/pages/view/ddev-com-front-end/settings/environment-variables). +The site [uses Octokit](src/lib/api.ts) for GitHub REST and GraphQL requests, which need a token to authenticate and to stay within quota. GitHub Actions supplies its own `GITHUB_TOKEN`. Anywhere else, including local development and [Cloudflare](https://dash.cloudflare.com/2aecb1c6b99f9d2274b12efc45152be2/pages/view/ddev-com-front-end/settings/environment-variables), set `GITHUB_TOKEN` to a classic personal access token with the scopes listed under [GitHub Token](#github-token). ## Resources diff --git a/README_SPONSOR.md b/README_SPONSOR.md deleted file mode 100644 index 06a730c0d..000000000 --- a/README_SPONSOR.md +++ /dev/null @@ -1,48 +0,0 @@ -# ddev.com - -[Go Back](./README.md) - -## How to Add a New Featured Sponsor - -1. Open the [`src/featured-sponsors.json`](./src/featured-sponsors.json) file in the repository. -2. Get the sponsor's logo files and place them in the `public/logos/` directory. - - I usually search for the organization's brand assets on their website by inspecting the page source in the browser and looking for `.svg` or `.png` files. If I cannot find them there, I search Google Images. - - It is preferable to use SVG files for logos, as they scale better and look sharper on different screen sizes and resolutions. If SVGs are not available, high-resolution PNGs can be used as a fallback. - - If a PNG has high enough resolution, it can be converted to SVG using [online tools](https://convertio.co/png-svg/). After conversion, it is a good idea to manually adjust the SVG colors by editing the file directly and using a color picker to extract colors from the original PNG. - - Dark variants for logos: - - If this is an SVG logo, you can upload it to AI and ask it to create a dark mode variant. - - If it is a PNG, you can try adjusting the colors manually using [online tools](https://onlinepngtools.com/change-png-color) to create a dark mode version. Ask AI to help with reverse color selection if needed. - - Square logos: if the logo is already square, reuse it. If a separate square version is available, use that. Otherwise, crop the original logo to a square aspect ratio. If this is an SVG, ask AI to do it. We don't care about dark mode for square logos. - -3. Add a new JSON object to the array with the following structure (we do not care much about the order; the important thing is that the result looks good): - - ```json - { - "name": "Upsun", - "type": "major", - "logo": "/logos/upsun.svg", - "darklogo": "/logos/upsun-darkmode.svg", - "squareLogo": "/logos/upsun-square.svg", - "url": "https://upsun.com", - "github": "upsun" - } - ``` - - - **`name`** – the human-friendly organization name. (Be sure this is formatted exactly as it’s used on the website or GitHub profile!) - - **`type`** – can be `"major"` or `"standard"` depending on contribution level. (Not currently used but can affect styling later.) - - **`logo`** – absolute, webroot-relative path for a logo you’ve added to the `public/logos/` directory. Make sure this is a clean, optimized vector SVG file unless it’s a person’s headshot. (Again, follow the organization’s brand guide wherever possible!) - - **`darklogo`** – absolute, webroot-relative path for a logo you’ve added to the `public/logos/` directory. This should be a version of the logo that looks good on dark backgrounds. If the original logo is already suitable for dark mode, reuse it. - - **`squareLogo`** – a square variant of the organization’s logo, not in use right now. No need to add this if `logo` is already square. - - **`url`** – organization’s website URL. - - **`github`** – optional GitHub username when relevant, which can be used to make sure the sponsor doesn’t appear twice in a list—as seen in the [Sponsors.astro](https://github.com/ddev/ddev.com/blob/main/src/components/Sponsors.astro#L53) component. - -4. Try it locally and check both light and dark mode (there is no manual switch; change your OS or browser appearance settings): - - `ddev launch :4321` - - `ddev launch :4321/sponsor/` - -5. Create a pull request with your changes and add screenshots to make review easier. diff --git a/package.json b/package.json index d72b2da09..8aaf14014 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,10 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "prettier": "./node_modules/.bin/prettier --check .", - "prettier:fix": "./node_modules/.bin/prettier --write .", - "textlint": "./node_modules/.bin/textlint 'src/content/**'", - "textlint:fix": "./node_modules/.bin/textlint 'src/content/**' --fix" + "prettier": "prettier --check .", + "prettier:fix": "prettier --write .", + "textlint": "textlint 'src/content/**'", + "textlint:fix": "textlint 'src/content/**' --fix" }, "dependencies": { "@astrojs/markdown-remark": "^7.3.1", diff --git a/src/content/blog/ddev-jan-2026-newsletter.md b/src/content/blog/ddev-jan-2026-newsletter.md index 49a57926d..e9c940bfb 100644 --- a/src/content/blog/ddev-jan-2026-newsletter.md +++ b/src/content/blog/ddev-jan-2026-newsletter.md @@ -1,7 +1,7 @@ --- title: "DDEV January 2026: Year in Review, Looking Ahead, and Community Momentum" pubDate: 2026-01-20 -modifiedData: 2026-04-30 +modifiedDate: 2026-04-30 modifiedComment: Fixed reference to Klemens Arro summary: "Reflecting on 2025's growth, mapping 2026's roadmap, celebrating a 10% sponsorship surge, and discovering global community contributions from macOS tools to international tutorials" author: Randy Fay diff --git a/src/content/blog/ddev-snapshots.md b/src/content/blog/ddev-snapshots.md index 8edd51a63..cc6bc9900 100644 --- a/src/content/blog/ddev-snapshots.md +++ b/src/content/blog/ddev-snapshots.md @@ -1,7 +1,7 @@ --- title: "DDEV Snapshots: Checkpoints, Restores, and Seeded Databases" pubDate: 2026-09-21 -modifiedData: 2026-09-23 +modifiedDate: 2026-09-23 modifiedComment: "Added snapshot/dbimage contributor training from 2026-09-23" summary: How DDEV database snapshots work, how to use them as checkpoints during migrations, and how to seed new projects or containers from a snapshot instead of a full import. author: Randy Fay From ade5ed8beb39a7ff72fc866edc3bb8b66f2802c5 Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk <stasadev@gmail.com> Date: Wed, 7 Oct 2026 10:36:26 +0300 Subject: [PATCH 2/2] chore(claude): allow only `ddev npm run build` without a prompt ## Short Summary (TL;DR) `Bash(ddev npm run *)` also auto-approved `astro`, `dev`, `start`, `preview`, and any script added to `package.json` later, so it is narrowed to `ddev npm run build`. The `bump-deps` skill now runs `ddev prettier` instead of `ddev npm run prettier`, which would prompt. The hooks are not affected, since they bypass permission rules. Suggested by @pengfei-threemoonslab, after running Agents Shipgate on #751. --- .claude/settings.json | 2 +- .claude/skills/bump-deps/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 88d679d86..97967e4e0 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "allow": [ - "Bash(ddev npm run *)", + "Bash(ddev npm run build)", "Bash(ddev npm outdated)", "Bash(ddev npm audit)", "Bash(ddev npm ls *)", diff --git a/.claude/skills/bump-deps/SKILL.md b/.claude/skills/bump-deps/SKILL.md index 1f3d340c2..c3d4935ec 100644 --- a/.claude/skills/bump-deps/SKILL.md +++ b/.claude/skills/bump-deps/SKILL.md @@ -88,7 +88,7 @@ ddev npm run build 2>&1 | tee ~/tmp/build.log grep -niE 'warn|error|deprecat' ~/tmp/build.log # GITHUB_TOKEN/AMPLITUDE notices are expected locally ddev restart && ddev logs # astro-dev-daemon must reach RUNNING ddev npm audit -ddev npm run prettier +ddev prettier ``` - The build must finish with astro-link-validator reporting no broken links.