From cf42f084fe68ee0a1b0982e62a73ba26cf4a8bc8 Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk Date: Wed, 29 Jul 2026 19:33:15 +0300 Subject: [PATCH 1/2] layout: one width for every page, blog post sidebar, rebuilt footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixes https://github.com/ddev/ddev.com/issues/161: the hero and nav stayed at `max-w-4xl` while the viewport kept growing, so from 1024px up the extra space turned into void margin instead of page. - Fixes https://github.com/ddev/ddev.com/issues/439: the homepage terminal was absolutely positioned at `right: -40rem` inside a `h-72` box, so on a 13" screen it hung off the side at the wrong size. Both of those live in the shell of the page, so this takes on the inconsistencies around them at the same time. Every page picked its own width (`max-w-4xl`, `-5xl`, `-6xl`, `-7xl`) and its own gutter, so nothing lined up between blocks or between pages. The footer had grown into one flat list that mixed social accounts with pages and named several of them twice. A blog post opened with a full-width feature image, which meant scrolling before you could read the first paragraph, and the space beside the text was doing nothing. Preview base: https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/ | Page | Before | After | Preview | Live | | --- | --- | --- | --- | --- | | Every page | `max-w-4xl` (56rem) on most pages, `max-w-5xl`/`-6xl`/`-7xl` on the homepage's sections, and a different `px-*` gutter per block | `--site-width: 72rem` via `.site-container`, one `.site-gutter` (1.5rem, 2rem from 64rem up). Long-form text is capped separately at `--reading-measure` (42rem), wired into the typography plugin, so a wider page didn't make lines harder to read | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/) | [live](https://ddev.com/) | | Header | nav at `max-w-4xl`, logo sitting left of the content it heads | the site container and gutter, so the logo lines up with the page at every width (#161) | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/) | [live](https://ddev.com/) | | Homepage hero | headline, buttons, platform list in a 4xl column with the terminal floated off the right edge | two columns: copy holds a minimum width, the terminal is a fixed 40rem beside it (its output is a character grid that can't reflow, so it can only be clipped, never squeezed), clipped to a peek between `md` and the width where both fit, hidden below `md`. Headline now names Node.js alongside PHP; the platform badges came out (#439) | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/) | [live](https://ddev.com/) | | Homepage stack logos | eight logos with `p-10` each, wrapping onto two lines at full width | one row spaced with a gap instead of per-logo padding, and the whole set is a data array — all eight fit on one line | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/) | [live](https://ddev.com/) | | Blog post | full-width feature image above the post, "Posted In" as the only thing in the right column | feature image beside the title, categories as pills under it, and a sticky sidebar with a newsletter callout and Similar Posts. The post itself starts at the top of the page | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/blog/release-v1-25-3/) | [live](https://ddev.com/blog/release-v1-25-3/) | | Blog listing | SVG feature images letterboxed inside the card while photos filled it | SVG cards fill the box the same way, and the six release/how-to banner SVGs were re-canvassed to 3:2 with side padding so nothing is cropped and nothing touches the edge | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/blog/) | [live](https://ddev.com/blog/) | | `/download` | a list of files and nothing else | sidebar with the release it's showing, release notes, all releases, checksums, installation steps, and a "Need Help?" card | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/download/) | [live](https://ddev.com/download/) | | `/get-started` | instructions only; if a step failed you had to go looking | same "Need Help?" card, sticky beside the instructions | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/get-started/) | [live](https://ddev.com/get-started/) | | `/sponsor` | tier cards with their own black-and-white buttons (no dark-mode hover); PayPal and invoicing buried in prose | "Ways to Sponsor" sidebar — GitHub Sponsors, PayPal, invoice or transfer — plus "Other Ways to Help"; tier cards now use the site's own buttons | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/sponsor/) | [live](https://ddev.com/sponsor/) | | `/about` | five oversized stats stacked in a bare column | the same numbers in a "DDEV Today" sidebar card, and the story now ends with the sponsorship ask | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/about/) | [live](https://ddev.com/about/) | | `/usage-stats` | a long single column with no way to jump between sections | "On This Page" index beside the charts, sponsorship ask at the end | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/usage-stats/) | [live](https://ddev.com/usage-stats/) | | `/brand`, `/contact`, `/foundation`, `/privacy` | plain markdown column, nothing beside it | "On This Page" index, opt-in per page with `index: true` in frontmatter; `/foundation` also ends with the sponsorship ask | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/foundation/) | [live](https://ddev.com/foundation/) | | Footer | one flat list mixing social accounts with pages, with Discord, GitHub and the blog appearing twice and the feeds in the middle of it | three bands: logo and social icons, then Product / Community / Project columns, then the copyright line with Privacy and the RSS/JSON feeds | [preview](https://20260729-stasadev-revamp.ddev-com-front-end.pages.dev/) | [live](https://ddev.com/) | ## What's new - `NewsletterCallout` and `SimilarPosts` in the blog post sidebar, and `CategoryPills` shared by the post and its cards. - `PageIndex`, an "On This Page" nav built in the browser from the page's own `h2` ids, so it can't drift from the content. - `SponsorCTA`, one sponsorship ask with one look and one top margin, on `/about`, `/foundation`, `/usage-stats` and the end of each Get Started walkthrough. It replaces `quickstart/CommunityCTA`, which said much the same thing in a different box. - `SidebarLinks`, `HelpLinks`, `ReleaseLinks`, `SponsorWays` for the sidebar cards, and `size="small"` on `CtaButton` for buttons in a sidebar. - `lib/posts.ts` (ordering, adjacent posts, similarity) and `lib/images.ts` (the `public/img` glob that three components were each declaring). - CSS: `.site-container` / `.site-gutter`, `.page-grid` / `.content-column` / `.sidebar-column`, `.sidebar-card` / `.sidebar-label`, `.sponsor-cta`. ## Notes - The newsletter callout beside the post and the shape of the new footer were inspired by https://upsun.com/blog/ai-fatigue-engineering-teams/. - Similar Posts ranks by what the posts say, not just their categories: a tf-idf vector per post over its title, summary, categories and body, compared by cosine similarity, with a shared category and a shared author as smaller nudges. Categories alone were far too coarse — nine of them cover 140 posts, so every release note matched every other one equally. The list cuts off relative to its best match, so a post on a subject nothing else covers shows three good matches instead of five padded ones, and newsletters only match other newsletters (a monthly digest has a word in common with almost everything). - Every page above was checked against the dev server, and the new `@apply` utilities were checked in the compiled CSS. 🤖 Developed with assistance from [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../10/banner-ddev-10-minutes-linux-dark.svg | 2 +- .../2025/10/banner-ddev-10-minutes-linux.svg | 2 +- .../blog/2026/02/banner-ddev-v1.25.0-dark.svg | 2 +- .../img/blog/2026/02/banner-ddev-v1.25.0.svg | 2 +- .../blog/2026/07/banner-ddev-v1-25-3-dark.svg | 2 +- .../img/blog/2026/07/banner-ddev-v1-25-3.svg | 2 +- src/components/AnimatedTerminal.astro | 11 +- src/components/AppLogos.astro | 2 +- src/components/Benefits.astro | 2 +- src/components/BlogFeatures.astro | 4 +- src/components/BlogPostCard.astro | 35 +- src/components/BlogPostFooter.astro | 27 +- src/components/BlogPostGrid.astro | 100 +- src/components/BlogPostLinks.astro | 29 + src/components/CategoryPills.astro | 27 + src/components/Contributors.astro | 4 +- src/components/CtaButton.astro | 16 +- src/components/FeatureImage.astro | 22 +- src/components/Features.astro | 2 +- src/components/Footer.astro | 287 +++-- src/components/Header.astro | 6 +- src/components/Heading.astro | 163 +-- src/components/HelpLinks.astro | 27 + src/components/Hero.astro | 107 +- src/components/NewsletterCallout.astro | 19 + src/components/PageIndex.astro | 52 + src/components/PlatformPicker.astro | 3 +- src/components/PostUpdate.astro | 2 +- src/components/ReleaseLinks.astro | 24 + src/components/SidebarLinks.astro | 31 + src/components/SimilarPosts.astro | 82 ++ src/components/SoftwareLogos.astro | 109 +- src/components/SponsorCTA.astro | 27 + src/components/SponsorWays.astro | 50 + src/components/SponsorsBanner.astro | 6 +- src/components/quickstart/Cloud.astro | 6 +- src/components/quickstart/CommunityCTA.astro | 7 - src/components/quickstart/Linux.astro | 4 +- src/components/quickstart/Mac.astro | 4 +- src/components/quickstart/Windows.astro | 4 +- src/const.ts | 3 + src/layouts/MarkdownLayout.astro | 30 +- src/lib/images.ts | 15 + src/lib/posts.ts | 205 ++++ src/pages/404.astro | 2 +- src/pages/about.astro | 75 +- src/pages/blog/[id].astro | 92 +- src/pages/blog/[page].astro | 2 +- src/pages/blog/author/[id].astro | 6 +- src/pages/blog/author/[id]/[page].astro | 4 +- src/pages/blog/author/index.astro | 4 +- src/pages/blog/category/[slug].astro | 4 +- src/pages/blog/category/[slug]/[page].astro | 4 +- src/pages/blog/category/index.astro | 4 +- src/pages/blog/index.astro | 2 +- src/pages/brand.mdx | 1 + src/pages/contact.mdx | 1 + src/pages/download.astro | 85 +- src/pages/foundation.mdx | 4 + src/pages/get-started.astro | 53 +- src/pages/index.astro | 6 +- src/pages/privacy.mdx | 1 + src/pages/search.astro | 4 +- src/pages/sponsor.astro | 357 +++---- src/pages/usage-stats.astro | 985 +++++++++--------- src/styles/global.css | 59 ++ tailwind.config.cjs | 2 + 67 files changed, 2021 insertions(+), 1302 deletions(-) create mode 100644 src/components/BlogPostLinks.astro create mode 100644 src/components/CategoryPills.astro create mode 100644 src/components/HelpLinks.astro create mode 100644 src/components/NewsletterCallout.astro create mode 100644 src/components/PageIndex.astro create mode 100644 src/components/ReleaseLinks.astro create mode 100644 src/components/SidebarLinks.astro create mode 100644 src/components/SimilarPosts.astro create mode 100644 src/components/SponsorCTA.astro create mode 100644 src/components/SponsorWays.astro delete mode 100644 src/components/quickstart/CommunityCTA.astro create mode 100644 src/lib/images.ts create mode 100644 src/lib/posts.ts diff --git a/public/img/blog/2025/10/banner-ddev-10-minutes-linux-dark.svg b/public/img/blog/2025/10/banner-ddev-10-minutes-linux-dark.svg index cdf5ecac4..b62b62612 100644 --- a/public/img/blog/2025/10/banner-ddev-10-minutes-linux-dark.svg +++ b/public/img/blog/2025/10/banner-ddev-10-minutes-linux-dark.svg @@ -1,4 +1,4 @@ - +
@@ -261,7 +270,7 @@ Waiting for ddev-router to become ready... ready in 1.0s
-
+
diff --git a/src/components/AppLogos.astro b/src/components/AppLogos.astro index 8e1db8ee9..9cbc66c0f 100644 --- a/src/components/AppLogos.astro +++ b/src/components/AppLogos.astro @@ -14,7 +14,7 @@ const logos = [ ---
-
+
{ logos.map((logo) => ( diff --git a/src/components/Benefits.astro b/src/components/Benefits.astro index 54bf6514b..59edbdc5a 100644 --- a/src/components/Benefits.astro +++ b/src/components/Benefits.astro @@ -40,7 +40,7 @@ const features = [ ---
-
+

Benefits

diff --git a/src/components/BlogFeatures.astro b/src/components/BlogFeatures.astro index 78d324d7a..bdfa24d34 100644 --- a/src/components/BlogFeatures.astro +++ b/src/components/BlogFeatures.astro @@ -14,8 +14,8 @@ const recentPosts = posts .slice(0, 3) --- -
-
+
+

Latest Blog Posts diff --git a/src/components/BlogPostCard.astro b/src/components/BlogPostCard.astro index dba2e86bc..4681c4d26 100644 --- a/src/components/BlogPostCard.astro +++ b/src/components/BlogPostCard.astro @@ -5,13 +5,14 @@ import type { CollectionEntry } from "astro:content" import { render } from "astro:content" import { Picture } from 'astro:assets' -import type { ImageMetadata } from 'astro' +import { resolveImage } from "../lib/images" interface Props { post: CollectionEntry<"blog"> } +import CategoryPills from "./CategoryPills.astro" import { getEntry } from "astro:content" -import { getSlug, formatDate, getCategoryUrl } from "../lib/api" +import { getSlug, formatDate } from "../lib/api" const { post } = Astro.props const { remarkPluginFrontmatter } = await render(post) @@ -25,13 +26,6 @@ const featureImageUrl = featureImage?.src ?? `/img/generic-thumbnail.png` const featureImageAlt = featureImage?.alt ?? `` const featureImageDarkUrl = featureImage?.srcDark ?? null -const images = import.meta.glob<{ default: ImageMetadata }>( - '/public/img/**/*.{png,jpg,jpeg,webp,gif}' -) -const resolveImage = async (path: string) => { - const key = `/public${path}` - return images[key] ? (await images[key]()).default : path -} const resolvedSrc = await resolveImage(featureImageUrl) const resolvedSrcDark = featureImageDarkUrl ? await resolveImage(featureImageDarkUrl) : null @@ -42,15 +36,19 @@ const categories = post.data.categories.length ? post.data.categories : [] /* `isolation: isolate` is here to convince Safari not to display un-rounded corners during animation */ }
-
+ {/* No background of its own: logo SVGs are drawn to sit on the page. The rule + is what separates the image from the text below it. */} +
{ featureImageUrl.includes(".svg") ? ( + /* The same box, filled the same way, as the raster images below, so the + cards in a row line up. */ <> - {featureImageAlt} - {featureImageDarkUrl && } + {featureImageAlt} + {featureImageDarkUrl && } ) : ( <> @@ -76,16 +74,7 @@ const categories = post.data.categories.length ? post.data.categories : []
-
- {categories.map((category) => ( - - {category} - - ))} -
+

{post.data.title} diff --git a/src/components/BlogPostFooter.astro b/src/components/BlogPostFooter.astro index 88dc13e5c..43600e6fb 100644 --- a/src/components/BlogPostFooter.astro +++ b/src/components/BlogPostFooter.astro @@ -4,30 +4,13 @@ export interface Props { } const { activeSlug } = Astro.props -import { getCollection } from "astro:content" +import { getAdjacentPosts } from "../lib/posts" -let next, prev - -const posts = await getCollection("blog") -const orderedPosts = posts.sort((a, b) => { - return new Date(a.data.pubDate) > new Date(b.data.pubDate) ? -1 : 1 -}) - -orderedPosts.forEach((post, index) => { - if (post.id === activeSlug) { - if (posts[index - 1]) { - prev = posts[index - 1] - } - - if (posts[index + 1]) { - next = posts[index + 1] - } - } -}) +const { prev, next } = await getAdjacentPosts(activeSlug) ---