From df9deb37f7945f416669e0a24f378f3251cccfba Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk Date: Fri, 23 Oct 2026 19:17:38 +0300 Subject: [PATCH 1/3] fix: improve accessibility, hash logo URLs, refresh terminal animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short Summary (TL;DR) Fixes the accessibility problems axe-core found across the site, links logos through hashed URLs so a replaced logo shows without a hard refresh, and updates the home page terminal to current `ddev start` output. ## How This PR Solves The Issue - Accessibility: [axe-core](https://github.com/dequelabs/axe-core), an open-source engine that checks a rendered page against WCAG rules such as contrast, names for controls, landmarks, and heading order, reported 204 violations on 12 page types in both themes, and now reports none. The fixes add a skip link, a landmark for the sponsor banner, a correct heading order, names for the copy buttons, keyboard-scrollable code blocks, and darker or underlined links where the contrast was too low. - Menu: closes on Escape or a click elsewhere, and opens below its button so the button stays visible. - Terminal: a pause button, as WCAG 2.2.2 (Pause, Stop, Hide) asks of a looping animation, and with reduced motion it starts paused on the `ddev describe` screen. Screen readers get one sentence instead of the box drawing. The output was recorded from the upcoming v1.25.5 with [VHS](https://github.com/charmbracelet/vhs), which records a terminal session from a script, and the `ready in` waits play in real time. - Logos: Cloudflare tells browsers to keep `/logos/` for 4 hours, so after #763 people saw the old logos until a hard refresh. Pages now link the hashed `/_astro/` copies, and `public/logos` stays for the README badges. - Agent guidance: a new `a11y-check` skill tells Claude how to run axe-core and Lighthouse on the host from `~/tmp`, so neither becomes a dependency of the site. ## Manual Testing Instructions Compare https://20261023-stasadev-accessibil.ddev-com-front-end.pages.dev/ with https://ddev.com/: - Home page: the first Tab shows "Skip to content". The terminal pauses and resumes from the button beside the dots, and with reduced motion turned on in the OS it starts paused. - Home page at phone width: the menu opens below its button and closes on Escape or a click outside. - A blog post with a code block: Tab reaches the code block, and arrow keys scroll a long line. - `/download`, `/sponsor`, `/usage-stats` and `/get-started`: links, tabs and buttons are readable in both themes. - Logo image URLs on the home page start with `/_astro/`. ## Automated Testing Overview Run locally, not in CI: axe-core 4.14 on 12 page types in light and dark themes at 1400 and 390 px wide, with no violations, and Lighthouse 12 with accessibility at 100 on the home page. [Lighthouse](https://developer.chrome.com/docs/lighthouse) is Google's page audit, the engine behind PageSpeed Insights, which scores performance, accessibility, best practices, and SEO; its API quota was used up, so it ran locally. πŸ€– Developed with assistance from [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 5.5 --- .claude/skills/a11y-check/SKILL.md | 75 ++++++++ .claude/skills/a11y-check/axe.mjs | 76 +++++++++ AGENTS.md | 1 + astro.config.mjs | 4 +- src/components/AnimatedTerminal.astro | 209 ++++++++++++++++------- src/components/AppLogos.astro | 4 +- src/components/BlogFeatures.astro | 2 +- src/components/BlogPostCard.astro | 13 +- src/components/CtaButton.astro | 3 +- src/components/FeaturedSponsors.astro | 9 +- src/components/Footer.astro | 5 +- src/components/Header.astro | 24 ++- src/components/Hero.astro | 4 +- src/components/PostUpdate.astro | 6 +- src/components/SoftwareLogos.astro | 4 +- src/components/SponsorsBanner.astro | 4 +- src/components/Terminal.astro | 9 +- src/components/UsageStatChart.astro | 4 +- src/components/quickstart/Cloud.astro | 2 +- src/components/quickstart/Examples.astro | 2 +- src/components/quickstart/Linux.astro | 6 +- src/components/quickstart/Mac.astro | 6 +- src/components/quickstart/Windows.astro | 6 +- src/layouts/Layout.astro | 7 +- src/lib/images.ts | 14 ++ src/lib/shiki-focusable-code.mjs | 16 ++ src/pages/download.astro | 18 +- src/pages/sponsor.astro | 2 +- src/styles/global.css | 16 +- 29 files changed, 426 insertions(+), 125 deletions(-) create mode 100644 .claude/skills/a11y-check/SKILL.md create mode 100644 .claude/skills/a11y-check/axe.mjs create mode 100644 src/lib/shiki-focusable-code.mjs diff --git a/.claude/skills/a11y-check/SKILL.md b/.claude/skills/a11y-check/SKILL.md new file mode 100644 index 000000000..d9ef1c771 --- /dev/null +++ b/.claude/skills/a11y-check/SKILL.md @@ -0,0 +1,75 @@ +--- +name: a11y-check +description: Check ddev.com pages for accessibility problems with axe-core and Lighthouse, in both themes and at desktop and phone width, before a change that touches markup, colors, or layout is committed. Use when asked to check accessibility, contrast, or PageSpeed scores, or after changing a layout, component, or global.css. +when_to_use: > + Triggered by "check accessibility", "run axe", "a11y", "contrast", "WCAG", + "PageSpeed", "Lighthouse score", or a change to src/layouts/, + src/components/, or src/styles/. +--- + +# Checking accessibility + +Run the tools on the host, not in the DDEV web container, which has no +browser. Both need Chrome or Chromium; set `CHROME_PATH` to it when it is not +`/usr/bin/chromium`, as on macOS: + +```bash +export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" +``` + +## 1. Build + +Check the production build, not the dev server, whose toolbar adds markup of +its own: + +```bash +ddev npm run build +base=$(ddev describe -j | jq -r .raw.primary_url) +``` + +## 2. axe-core + +Install the packages in `~/tmp`, never in the repository's `package.json`, +and run [`axe.mjs`](axe.mjs) from there: + +```bash +mkdir -p ~/tmp/a11y-tools && cd ~/tmp/a11y-tools +[ -d node_modules ] || { npm init -y >/dev/null && npm install puppeteer-core@24 axe-core@4; } +node /axe.mjs "$base" / /blog/ /download/ > axe.json +jq 'group_by(.id) | map({id: .[0].id, count: length, example: .[0]})' axe.json +``` + +Pass every page type the change can reach. For a change to the layout, +header, footer, or `global.css`, that is all of them: + +```text +/ /about/ /blog/ /blog// /blog/author/ /blog/author// +/blog/category/ /blog/category// /download/ /get-started/ /sponsor/ +/usage-stats/ +``` + +The script checks each path in light and dark at 1400 and 390 px wide, since +some problems show in only one theme or at one width. It emulates reduced +motion, which stops the home page terminal on its `ddev describe` screen. + +Fix every violation the change introduced. To tell which were there before, +run the same paths with `https://ddev.com` as the base; report those, and +leave them for a separate PR unless asked. + +## 3. Lighthouse + +The PageSpeed Insights API shares its quota across everyone who calls it +without a key, so it usually fails. Run Lighthouse locally instead: + +```bash +npx -y lighthouse@12 "$base/" --quiet --output=json --output-path=lh.json \ + --chrome-flags="--headless=new --ignore-certificate-errors" +jq '.categories | map_values(.score * 100)' lh.json +``` + +Add `--preset=desktop` for the desktop scores. Locally, best practices loses +points to a 404 for `/s/sponsorship-data.json`, which `public/_redirects` +answers on Cloudflare; ignore that one. + +For the live site, run the same command against `https://ddev.com/`, or a +PR's Cloudflare preview. diff --git a/.claude/skills/a11y-check/axe.mjs b/.claude/skills/a11y-check/axe.mjs new file mode 100644 index 000000000..aa1948616 --- /dev/null +++ b/.claude/skills/a11y-check/axe.mjs @@ -0,0 +1,76 @@ +/** + * Runs axe-core on each path, in both themes, at desktop and phone width, and + * prints the violations as JSON. Modules resolve from the working directory, + * so the repository's package.json doesn't need them. + * + * Usage: node axe.mjs ... + */ +import { createRequire } from "module" +import { readFileSync } from "fs" + +const require = createRequire(`${process.cwd()}/`) +const puppeteer = require("puppeteer-core") +const axeSource = readFileSync(require.resolve("axe-core/axe.min.js"), "utf8") + +const [base, ...paths] = process.argv.slice(2) +const viewports = [ + { width: 1400, height: 900 }, + { width: 390, height: 844, isMobile: true, hasTouch: true }, +] + +const browser = await puppeteer.launch({ + executablePath: process.env.CHROME_PATH ?? "/usr/bin/chromium", + args: ["--no-sandbox", "--ignore-certificate-errors"], +}) +const violations = [] +for (const path of paths) { + for (const scheme of ["light", "dark"]) { + for (const viewport of viewports) { + const page = await browser.newPage() + await page.setViewport(viewport) + // Reduced motion stops the home page terminal on one screen, so every + // run checks the same markup. + await page.emulateMediaFeatures([ + { name: "prefers-color-scheme", value: scheme }, + { name: "prefers-reduced-motion", value: "reduce" }, + ]) + await page.goto(base + path, { + waitUntil: "networkidle2", + timeout: 60000, + }) + await page.evaluate(axeSource) + const result = await page.evaluate(() => + axe.run(document, { + runOnly: { + type: "tag", + values: [ + "wcag2a", + "wcag2aa", + "wcag21a", + "wcag21aa", + "wcag22aa", + "best-practice", + ], + }, + }) + ) + for (const violation of result.violations) { + for (const node of violation.nodes) { + violations.push({ + path, + scheme, + width: viewport.width, + id: violation.id, + impact: violation.impact, + target: node.target.join(" "), + html: node.html.slice(0, 160), + summary: node.failureSummary?.split("\n")[1]?.trim(), + }) + } + } + await page.close() + } + } +} +await browser.close() +console.log(JSON.stringify(violations, null, 1)) diff --git a/AGENTS.md b/AGENTS.md index d5bc03e1e..00785cc65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ its own. | 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` | +| Accessibility checks with axe-core and Lighthouse | `.claude/skills/a11y-check/SKILL.md` | | A commit or PR | `.claude/skills/ddev-commit/SKILL.md` | | A comment, in any code file | `.claude/rules/comments.md` | diff --git a/astro.config.mjs b/astro.config.mjs index 27bb94b8d..c3d229666 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -25,6 +25,7 @@ import searchIndex from "./src/lib/search-index.js" import sitemap from "@astrojs/sitemap" import tailwindcss from "@tailwindcss/vite" import { addCopyButton } from "shiki-transformer-copy-button" +import { focusableCode } from "./src/lib/shiki-focusable-code.mjs" import { SHIKI_THEMES } from "./src/const.ts" import { unified } from "@astrojs/markdown-remark" @@ -134,8 +135,9 @@ export default defineConfig({ // toggle: 3000, // 3 seconds transformers: [ addCopyButton({ - // visibility: 'hover', // if you want it to only show on hover + button: { "aria-label": "Copy code" }, }), + focusableCode(), ], }, }, diff --git a/src/components/AnimatedTerminal.astro b/src/components/AnimatedTerminal.astro index c5a7d8ed5..dbb5cfc41 100644 --- a/src/components/AnimatedTerminal.astro +++ b/src/components/AnimatedTerminal.astro @@ -6,43 +6,44 @@ * things drift out of sync!) */ import { getLatestReleaseVersion } from "../lib/api" +import { PauseIcon, PlayIcon } from "@heroicons/react/20/solid/index.js" const latestRelease = await getLatestReleaseVersion() // Pre-rendered ddev describe output β€” used for both the animation and noscript fallback const versionText = `DDEV version: ${latestRelease}` -const versionLine = `β”‚ ${versionText}${" ".repeat(83 - versionText.length)} β”‚` +const versionLine = `β”‚ ${versionText}${" ".repeat(85 - versionText.length)} β”‚` const describeOutput = `ddev describe -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Project: my-project ~/dev/my-project https://my-project.ddev.site β”‚ -β”‚ Docker platform: docker-desktop β”‚ -β”‚ Router: traefik β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Project: my-project ~/dev/my-project https://my-project.ddev.site β”‚ +β”‚ Docker platform: linux-docker β”‚ +β”‚ Router: traefik β”‚ ${versionLine} -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ SERVICE β”‚ STAT β”‚ URL/PORT β”‚ INFO β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ web β”‚ OK β”‚ https://my-project.ddev.site β”‚ php PHP 8.4 β”‚ -β”‚ β”‚ β”‚ InDocker -> Host: β”‚ Server: nginx-fpm β”‚ -β”‚ β”‚ β”‚ - web:80 -> 127.0.0.1:51046 β”‚ Docroot: '' β”‚ -β”‚ β”‚ β”‚ - web:443 -> 127.0.0.1:51047 β”‚ Perf mode: none β”‚ -β”‚ β”‚ β”‚ - web:8025 -> 127.0.0.1:51048 β”‚ NodeJS: 24 β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ db β”‚ OK β”‚ InDocker -> Host: β”‚ mysql:8.0 β”‚ -β”‚ β”‚ β”‚ - db:3306 -> 127.0.0.1:51045 β”‚ User/Pass: 'db/db' β”‚ -β”‚ β”‚ β”‚ β”‚ or 'root/root' β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ xhgui β”‚ OK β”‚ https://my-project.ddev.site:8142 β”‚ β”‚ -β”‚ β”‚ β”‚ InDocker: β”‚ β”‚ -β”‚ β”‚ β”‚ - xhgui:80 β”‚ β”‚ -β”‚ β”‚ β”‚ Launch: ddev xhgui β”‚ β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Mailpit β”‚ β”‚ Mailpit: https://my-project.ddev.site:8026 β”‚ β”‚ -β”‚ β”‚ β”‚ Launch: ddev mailpit β”‚ β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ All URLs β”‚ β”‚ https://my-project.ddev.site, β”‚ β”‚ -β”‚ β”‚ β”‚ https://127.0.0.1:51047, β”‚ β”‚ -β”‚ β”‚ β”‚ http://my-project.ddev.site, β”‚ β”‚ -β”‚ β”‚ β”‚ http://127.0.0.1:51046 β”‚ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜` +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ SERVICE β”‚ STAT β”‚ URL/PORT β”‚ INFO β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ web β”‚ OK β”‚ https://my-project.ddev.site β”‚ php PHP 8.4 β”‚ +β”‚ β”‚ β”‚ InDocker -> Host: β”‚ Server: nginx-fpm β”‚ +β”‚ β”‚ β”‚ - web:80 -> 127.0.0.1:32865 β”‚ Docroot: 'public' β”‚ +β”‚ β”‚ β”‚ - web:443 -> 127.0.0.1:32866 β”‚ Perf mode: none β”‚ +β”‚ β”‚ β”‚ - web:8025 -> 127.0.0.1:32867 β”‚ Node.js: 24 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ db β”‚ OK β”‚ InDocker -> Host: β”‚ mariadb:11.8 β”‚ +β”‚ β”‚ β”‚ - db:3306 -> 127.0.0.1:32868 β”‚ User/Pass: 'db/db' β”‚ +β”‚ β”‚ β”‚ β”‚ or 'root/root' β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ xhgui β”‚ OK β”‚ https://my-project.ddev.site:8142 β”‚ β”‚ +β”‚ β”‚ β”‚ InDocker: β”‚ β”‚ +β”‚ β”‚ β”‚ - xhgui:80 β”‚ β”‚ +β”‚ β”‚ β”‚ Launch: ddev xhgui β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Mailpit β”‚ β”‚ Mailpit: https://my-project.ddev.site:8026 β”‚ β”‚ +β”‚ β”‚ β”‚ Launch: ddev mailpit β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Project URLs β”‚ β”‚ https://my-project.ddev.site β”‚ β”‚ +β”‚ β”‚ β”‚ https://127.0.0.1:32866 β”‚ β”‚ +β”‚ β”‚ β”‚ http://my-project.ddev.site β”‚ β”‚ +β”‚ β”‚ β”‚ http://127.0.0.1:32865 β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜` --- @@ -251,9 +319,10 @@ Waiting for ddev-router to become ready... ready in 1.0s /* data-theme is set by the inline head script before body renders, * so these rules apply on the very first paint with no flash. */ html[data-theme] #noscript-pre { display: none; } - html:not([data-theme]) #animated-pre { display: none; } + html:not([data-theme]) #animated-pre, + html:not([data-theme]) #animation-toggle { display: none; } - /* The output is a fixed 87x32 character grid: this font size fits 87 columns + /* The output is a fixed 89x32 character grid: this font size fits 89 columns * inside the 40rem the caller gives it, and the height is in `em` so the box * stays in step with the font. */ .terminal-body { @@ -264,12 +333,26 @@ Waiting for ddev-router to become ready... ready in 1.0s
-
+
+ +
-
+

+ A terminal runs ddev start, which starts the project's containers, then ddev + describe, which lists the project's services and their URLs. +

+
diff --git a/src/components/BlogPostCard.astro b/src/components/BlogPostCard.astro index 4681c4d26..42ed2eb0a 100644 --- a/src/components/BlogPostCard.astro +++ b/src/components/BlogPostCard.astro @@ -8,13 +8,16 @@ import { Picture } from 'astro:assets' import { resolveImage } from "../lib/images" interface Props { post: CollectionEntry<"blog"> + // The title's heading level: 3 under a section heading, 2 on a listing page + level?: 2 | 3 } import CategoryPills from "./CategoryPills.astro" import { getEntry } from "astro:content" import { getSlug, formatDate } from "../lib/api" -const { post } = Astro.props +const { post, level = 2 } = Astro.props +const Title = `h${level}` const { remarkPluginFrontmatter } = await render(post) const author = await getEntry("authors", getSlug(post.data.author)) @@ -75,11 +78,11 @@ const categories = post.data.categories.length ? post.data.categories : []
{ diff --git a/src/components/CtaButton.astro b/src/components/CtaButton.astro index ada45c8ac..c0b7c0cae 100644 --- a/src/components/CtaButton.astro +++ b/src/components/CtaButton.astro @@ -11,7 +11,7 @@ export interface Props { const typeClasses = { default: `border border-transparent text-white bg-blue-650 hover:bg-blue-800`, - hollow: `border border-blue-650 text-blue-650 hover:border-blue-800 hover:text-blue-800 dark:border-slate-400 dark:text-slate-300 dark:hover:border-white dark:hover:text-white`, + hollow: `border border-blue-700 text-blue-700 hover:border-blue-800 hover:text-blue-800 dark:border-slate-400 dark:text-slate-300 dark:hover:border-white dark:hover:text-white`, } const sizeClasses = { @@ -31,7 +31,6 @@ const { text, href, type = "default", size = "default", target, title } = typeClasses[type]} target={target} title={title} - role="button" > {text} diff --git a/src/components/FeaturedSponsors.astro b/src/components/FeaturedSponsors.astro index 7836d6797..d385dd526 100644 --- a/src/components/FeaturedSponsors.astro +++ b/src/components/FeaturedSponsors.astro @@ -1,5 +1,6 @@ --- import featuredSponsors from "../featured-sponsors.json" +import { logoUrl } from "../lib/images" /** * Heights for logos that must render larger than the rest, keyed by the name @@ -36,12 +37,12 @@ regularSponsors.map((sponsor) => { target="_blank" > {`${sponsor.name} @@ -65,12 +66,12 @@ regularSponsors.map((sponsor) => { target="_blank" > {`${sponsor.name} diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 60763ce63..0bf080438 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -19,6 +19,7 @@ import { } from "../const" import Icon from "./Icon.astro" +import { logoUrl } from "../lib/images" /* `me` marks a link that identifies the project itself, for rel="me". */ interface FooterLink { @@ -89,7 +90,7 @@ const feeds = [ DDEV home { + if (event.key === "Escape" && menu.open) { + menu.open = false + menu.querySelector("summary")?.focus() + } + }) + document.addEventListener("click", (event) => { + if (menu.open && !menu.contains(event.target as Node)) menu.open = false + }) + } + // Links leave the bar one at a time as it fills up, instead of all at once at - // a breakpoint that can only guess whether they fit. Opening and closing the - // menu is the browser's job, so there's none of it here. + // a breakpoint that can only guess whether they fit. if (nav && list && logo && icons && menu) { const items = Array.from(list.querySelectorAll("[data-nav-item]")) const menuItems = Array.from(menu.querySelectorAll("[data-nav-item]")) @@ -111,10 +125,10 @@ const navItems = [ aria-label="Main" > diff --git a/src/components/SponsorsBanner.astro b/src/components/SponsorsBanner.astro index 680e72d12..65db044b3 100644 --- a/src/components/SponsorsBanner.astro +++ b/src/components/SponsorsBanner.astro @@ -172,7 +172,7 @@ const isOnSustainabilityPage = currentPath === '/blog/sustainability-for-ddev/' } -
+