diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml index e0187751..b54d97c0 100644 --- a/.github/workflows/link-check.yml +++ b/.github/workflows/link-check.yml @@ -1,12 +1,15 @@ 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: @@ -14,14 +17,19 @@ concurrency: 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 @@ -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 (?.+?) Status: (?.*)$")?) + // {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 diff --git a/.linkspector.yml b/.linkspector.yml index 6b4ca019..ff9e6f38 100644 --- a/.linkspector.yml +++ b/.linkspector.yml @@ -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" @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 2be51403..96c23474 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `
`. 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 diff --git a/src/content/blog/2024-plans.md b/src/content/blog/2024-plans.md index e2451149..d9afc2e8 100644 --- a/src/content/blog/2024-plans.md +++ b/src/content/blog/2024-plans.md @@ -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 diff --git a/src/content/blog/ddev-august-2025-newsletter.md b/src/content/blog/ddev-august-2025-newsletter.md index e0d7b26a..4e1eac89 100644 --- a/src/content/blog/ddev-august-2025-newsletter.md +++ b/src/content/blog/ddev-august-2025-newsletter.md @@ -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! diff --git a/src/content/blog/ddev-february-2026-newsletter.md b/src/content/blog/ddev-february-2026-newsletter.md index 9c921641..841276be 100644 --- a/src/content/blog/ddev-february-2026-newsletter.md +++ b/src/content/blog/ddev-february-2026-newsletter.md @@ -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