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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 105 additions & 7 deletions .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
name: Link check
on:
push:
branches: [main]
pull_request:
paths:
- "src/content/blog/**"
- ".linkspector.yml"
- ".github/workflows/link-check.yml"
# The full sweep re-checks every external link in every post, including ones
# nobody touched. Third-party rot is a chore, not a reason to break main, so
# it runs weekly and files an issue instead of failing a required check.
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
checks: write
contents: read
pull-requests: read

jobs:
linkspector:
# Blocking, and scoped to links the author actually added or changed. A typo'd
# URL in a new post fails here, where the author can still fix it.
pr:
name: Check blog links
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
permissions:
checks: write
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v7
- name: Run linkspector
Expand All @@ -30,6 +38,96 @@ jobs:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-check
fail_on_error: true
# Check links only in the added lines for PRs, but check everything otherwise
filter_mode: ${{ github.event_name == 'pull_request' && 'added' || 'nofilter' }}
filter_mode: added
config_file: ".linkspector.yml"

# Non-blocking. Reports into a single reusable issue so link rot accumulates in
# one place instead of turning main red.
sweep:
name: Sweep all blog links
if: github.event_name != 'pull_request'
runs-on: ubuntu-24.04
permissions:
contents: read
issues: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_TITLE: "Link check: broken external links in blog content"
NODE_VERSION: 24
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}

- name: Run linkspector
id: check
run: |
# A non-zero exit means broken links were found, which is not a job
# failure here. Unparseable output means the tool itself broke, which is.
npx --yes @umbrelladocs/linkspector@0.5.6 check \
-c .linkspector.yml -j > result.json || true
if ! jq -e 'has("diagnostics")' result.json >/dev/null 2>&1; then
echo "::error::linkspector produced no usable JSON"
head -c 2000 result.json || true
exit 1
fi
echo "broken=$(jq '.diagnostics | length' result.json)" >> "$GITHUB_OUTPUT"

- name: Build report
if: steps.check.outputs.broken != '0'
run: |
{
# Each paragraph is one long line on purpose: GitHub renders issue
# bodies with GFM hard line breaks, so a wrapped paragraph would come
# out ragged. See "Avoiding Hard Line Breaks" in AGENTS.md.
echo "${{ steps.check.outputs.broken }} link(s) in \`src/content/blog\` did not resolve."
echo
echo "A Cloudflare bot challenge returns 403 whether or not the page exists, so confirm by hand before editing a post. If a link is genuinely dead, fix the post; only add an entry to \`.linkspector.yml\` when the page is fine and the checker is being blocked."
echo
jq -r '
.diagnostics
| map(((.message | capture("Cannot reach (?<url>.+?) Status: (?<st>.*)$")?)
// {url: .message, st: "?"}) as $m
| {url: $m.url, st: $m.st, path: .location.path,
line: .location.range.start.line})
| group_by(.url | sub("^https?://"; "") | sub("/.*$"; ""))
| map("### \(.[0].url | sub("^https?://"; "") | sub("/.*$"; "")) (\(length))\n"
+ (map("- [\(.path):\(.line)](\(env.BLOB)/\(.path)#L\(.line)) — `\(.st)`\n \(.url)")
| join("\n")))
| join("\n\n")
' result.json
echo
echo "---"
echo "[Sweep run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
} > report.md
env:
BLOB: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}

- name: Open or update the tracking issue
if: steps.check.outputs.broken != '0'
run: |
existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "\"$ISSUE_TITLE\" in:title" --json number,title \
| jq -r --arg t "$ISSUE_TITLE" \
'[.[] | select(.title == $t)] | first | .number // empty')
if [ -n "$existing" ]; then
gh issue edit "$existing" --repo "$GITHUB_REPOSITORY" --body-file report.md
echo "Updated issue #$existing"
else
gh issue create --repo "$GITHUB_REPOSITORY" \
--title "$ISSUE_TITLE" --body-file report.md
fi

- name: Close the tracking issue when everything resolves
if: steps.check.outputs.broken == '0'
run: |
existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "\"$ISSUE_TITLE\" in:title" --json number,title \
| jq -r --arg t "$ISSUE_TITLE" \
'[.[] | select(.title == $t)] | first | .number // empty')
if [ -n "$existing" ]; then
gh issue close "$existing" --repo "$GITHUB_REPOSITORY" \
--comment "All blog links resolved in the latest sweep."
fi
18 changes: 17 additions & 1 deletion .linkspector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ignorePatterns:
# Known to block or reject linkspector's headless-browser / CI requests,
# even though the pages are reachable from a normal browser.
- pattern: "^https://(www\\.)?linkedin\\.com"
- pattern: "^https://(www\\.)?tag1consulting\\.com"
- pattern: "^https://(www\\.)?tag1(consulting)?\\.com"
- pattern: "^https://(www\\.)?packtpub\\.com"
- pattern: "^https://buildkite\\.com/ddev/"
- pattern: "^https://(www\\.)?npmjs\\.com"
Expand All @@ -32,6 +32,22 @@ ignorePatterns:
- pattern: "^https://firesphere\\.dev/articles/ddev-elasticsearch-and-silverstripe$"
# Blocks or is unreachable from CI runners, even though the site is up.
- pattern: "^https://(www\\.)?rueegger\\.me"
# Serves a Cloudflare bot challenge ("cf-mitigated: challenge", HTTP 403) to
# anything that isn't an interactive browser, which is what linkspector's
# headless Chrome looks like. Cloudflare's "Agent" bot category, offered to
# site owners starting 2026-07-01, explicitly covers browser-driving agents:
# https://blog.cloudflare.com/content-independence-day-ai-options/
# The linked pages are all live; verified out-of-band via api.stackexchange.com.
- pattern: "^https://stackoverflow\\.com"
# Same challenge. Kept even though no post currently links here: meeting
# announcements use timeanddate.com for timezone-aware times, so new links
# will appear. A stale link to a past meeting should be removed from the post
# rather than left for this pattern to hide.
- pattern: "^https://(www\\.)?timeanddate\\.com"
# Same challenge, but only intermittently: failed in the 2026-07-30 and
# 2026-07-31 runs, passed on 2026-08-03. Used only for image-credit
# attribution links.
- pattern: "^https://(www\\.)?pixabay\\.com"
aliveStatusCodes:
- 200
- 206
Expand Down
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ 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
Expand Down
2 changes: 1 addition & 1 deletion src/content/blog/2024-plans.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ categories:
- Community
---

At the DDEV Advisory Group's 2-hour [annual review/planning meeting on March 6, 2024](https://www.timeanddate.com/worldclock/fixedtime.html?msg=DDEV+Advisory+Group&iso=20240306T09&p1=75&ah=2), we'll talk about plans and priorities for 2024. These have been discussed previously in the [DDEV 2024 Priorities discussion](https://github.com/orgs/ddev/discussions/5720) but in getting ready for the meeting it seems like a good time to propose them in a more cohesive fashion. (Everyone is welcome to the meeting. The Zoom link is announced in [Discord](/s/discord) or email [support](mailto:support%40ddev.com) for a calendar invitation or the zoom link.)
At the DDEV Advisory Group's 2-hour annual review/planning meeting on March 6, 2024, we'll talk about plans and priorities for 2024. These have been discussed previously in the [DDEV 2024 Priorities discussion](https://github.com/orgs/ddev/discussions/5720) but in getting ready for the meeting it seems like a good time to propose them in a more cohesive fashion. (Everyone is welcome to the meeting. The Zoom link is announced in [Discord](/s/discord) or email [support](mailto:support%40ddev.com) for a calendar invitation or the zoom link.)

## Community

Expand Down
2 changes: 1 addition & 1 deletion src/content/blog/ddev-august-2025-newsletter.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ This month: v1.24.7 release, Contributor Training starts up again, ddev-hostname
- **Creating a Simple Donation Form in Drupal with Stripe & Webforms**
Setup payments in minutes with DDEV and Webform. → [Read more↗](https://picozzi.com/notebook/2025/jul/creating-simple-donation-form-drupal-stripe-and-webforms)
- **Free Local AI with Ollama & DDEV**
How to Set Up Ollama as a Free, Local AI Provider for Your local Drupal Development. → [Read more↗](https://www.linkedin.com/pulse/how-set-up-ollama-free-local-ai-provider-your-drupal-rakesh-james-gedje/)
How to Set Up Ollama as a Free, Local AI Provider for Your local Drupal Development. → [Read more↗](https://www.thedroptimes.com/50560/set-ollama-free-local-ai-provider-in-your-drupal-ddev-environment)

## DDEV Training Starting Up Again!

Expand Down
2 changes: 1 addition & 1 deletion src/content/blog/ddev-february-2026-newsletter.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Then in March I'll be at [DrupalCon Chicago](https://events.drupal.org/chicago20
- **ddev-mngr** → A Go-based command-line tool with an interactive terminal UI for managing multiple DDEV projects at once — start, stop, check status, and open URLs across projects. With this add-on [Olivier Dobberkau](https://github.com/dkd-dobberkau) inspired a new TUI approach for DDEV core as well! [View on GitHub↗](https://github.com/dkd-dobberkau/ddev-mngr)
- **TYPO3 DDEV Agent Skill** → Netresearch built an Agent Skill (compatible with Claude Code, Cursor, Windsurf, and GitHub Copilot) that automates DDEV environment setup for TYPO3 extension development, including multi-version testing environments for TYPO3 11.5, 12.4, and 13.4 LTS. [View on GitHub↗](https://github.com/netresearch/typo3-ddev-skill)
- **Using Laravel Boost with DDEV** → Russell Jones explains how to integrate Laravel Boost (an official MCP server) with DDEV, giving AI coding agents contextual access to routes, database schema, logs, and configuration. [Read on Dev.to↗](https://dev.to/jonesrussell/using-laravel-boost-with-ddev-1kc6)
- **Laravel VS Code Extension v1.4.2** → Now includes Docker integration support and a fix for Pint functionality within DDEV environments. [Read more↗](https://news.extly.com/more-news/2030-dev-news/24693-docker-support-in-laravel-vs-code-extension-v1-4-2.html)
- **Laravel VS Code Extension v1.4.2** → Now includes Docker integration support and a fix for Pint functionality within DDEV environments. [Read on Laravel News↗](https://laravel-news.com/laravel-vscode-extension-v1-4-2)

## Community Tutorials from Around the World

Expand Down