Skip to content
Open
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
2 changes: 2 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import prefetch from "@astrojs/prefetch"
import react from "@astrojs/react"
import { rehypeAccessibleEmojis } from "rehype-accessible-emojis"
import rehypeAstroRelativeMarkdownLinks from "astro-rehype-relative-markdown-links"
import { rehypeWrapTables } from "./src/lib/rehype-wrap-tables.mjs"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import rehypeExternalLinks from "rehype-external-links"
import rehypeFigure from "rehype-figure"
Expand Down Expand Up @@ -118,6 +119,7 @@ export default defineConfig({
headingTags: ["h2", "h3"],
}),
rehypeAstroRelativeMarkdownLinks,
rehypeWrapTables,
],
},
image: {
Expand Down
86 changes: 48 additions & 38 deletions src/components/AnimatedTerminal.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@
*/
import { getLatestReleaseVersion } from "../lib/api"
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 describeOutput = `ddev describe
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Project: my-project ~/dev/my-project https://my-project.ddev.site │
│ Docker platform: docker-desktop │
│ Router: traefik │
${versionLine}
├────────────┬──────┬────────────────────────────────────────────┬────────────────────┤
│ SERVICE │ STAT │ URL/PORT │ INFO │
├────────────┼──────┼────────────────────────────────────────────┼────────────────────┤
│ web │ <span class="text-green-500">OK</span> │ 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 │ <span class="text-green-500">OK</span> │ InDocker -> Host: │ mysql:8.0 │
│ │ │ - db:3306 -> 127.0.0.1:51045 │ User/Pass: 'db/db' │
│ │ │ │ or 'root/root' │
├────────────┼──────┼────────────────────────────────────────────┼────────────────────┤
│ xhgui │ <span class="text-green-500">OK</span> │ 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 │ │
└────────────┴──────┴────────────────────────────────────────────┴────────────────────┘`
---

<script>
Expand All @@ -27,11 +63,8 @@ const latestRelease = await getLatestReleaseVersion()
wait?: number
}

// Get the latest release version from the data attribute
const terminalWrapper = document.querySelector('.terminal-wrapper') as HTMLElement
// Build the DDEV version line with proper padding (line should be 85 chars total including │ symbols)
const versionText = `DDEV version: ${terminalWrapper?.dataset.latestRelease}` || 'v1.25.0'
const versionLine = `│ ${versionText}${' '.repeat(83 - versionText.length)} │`
const describeOutput = terminalWrapper?.dataset.describeOutput ?? ''

/**
* Output and animation settings for the things we want to show in the terminal.
Expand Down Expand Up @@ -66,38 +99,7 @@ Waiting for ddev-router to become ready... ready in 1.0s
{ transition: "type", lines: 1, wait: 250 },
{ transition: "show", lines: 31 },
],
output: `ddev describe
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ Project: my-project ~/dev/my-project https://my-project.ddev.site │
│ Docker platform: docker-desktop │
│ Router: traefik │
${versionLine}
├────────────┬──────┬────────────────────────────────────────────┬────────────────────┤
│ SERVICE │ STAT │ URL/PORT │ INFO │
├────────────┼──────┼────────────────────────────────────────────┼────────────────────┤
│ web │ <span class="text-green-500">OK</span> │ 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 │ <span class="text-green-500">OK</span> │ InDocker -> Host: │ mysql:8.0 │
│ │ │ - db:3306 -> 127.0.0.1:51045 │ User/Pass: 'db/db' │
│ │ │ │ or 'root/root' │
├────────────┼──────┼────────────────────────────────────────────┼────────────────────┤
│ xhgui │ <span class="text-green-500">OK</span> │ 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 │ │
└────────────┴──────┴────────────────────────────────────────────┴────────────────────┘`,
output: describeOutput,
},
]

Expand Down Expand Up @@ -245,15 +247,23 @@ ${versionLine}
})
</script>

<div class="terminal-wrapper rounded-lg shadow-lg" style="background: #2e3440;" data-latest-release={latestRelease}>
<style>
/* 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; }
</style>

<div class="terminal-wrapper rounded-lg shadow-lg" style="background: #2e3440;" data-describe-output={describeOutput}>
<div class="top-bar flex p-3 space-x-2 rounded-t-lg" style="background: #2e3440;">
<div class="block rounded-full bg-slate-500 w-3 h-3"></div>
<div class="block rounded-full bg-slate-500 w-3 h-3"></div>
<div class="block rounded-full bg-slate-500 w-3 h-3"></div>
</div>
<div class="overflow-hidden my-0">
<div class="h-full p-4 text-slate-300 text-xs" style="height: 540px;">
<pre><span class="select-none text-slate-400">→ </span><span id="animation-stage" /></pre>
<pre id="animated-pre"><span class="select-none text-slate-400">→ </span><span id="animation-stage" /></pre>
<pre id="noscript-pre"><span class="select-none text-slate-400">→ </span><span set:html={describeOutput} /></pre>
</div>
</div>
</div>
164 changes: 164 additions & 0 deletions src/components/BlogPostGrid.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
/**
* Renders a grid of blog post cards with a "Load More" button or infinite scroll
* for fetching additional pages via HTML fragment fetching.
* Pass `nextUrl` to enable loading more posts.
* The user's preference (button vs auto-scroll) is persisted in localStorage.
*/
import type { CollectionEntry } from "astro:content"
import BlogPostCard from "./BlogPostCard.astro"

interface Props {
posts: CollectionEntry<"blog">[]
nextUrl?: string
prevUrl?: string
}

const { posts, nextUrl, prevUrl } = Astro.props
---

<div
id="post-grid"
data-next-url={nextUrl}
class="mt-12 px-6 sm:px-4 lg:px-0 grid gap-5 sm:grid-cols-2 lg:grid-cols-3"
>
{posts.map((post) => <BlogPostCard post={post} />)}
</div>

<div
id="load-mode-toggle"
class="flex justify-end mt-6 px-6 lg:px-0"
style="display: none"
>
<label class="flex items-center gap-2 text-sm text-gray-500 dark:text-slate-400 cursor-pointer select-none">
<input type="checkbox" id="scroll-toggle" class="cursor-pointer" />
Auto-load on scroll
</label>
</div>

{
nextUrl && (
<>
<div
id="load-more-container"
class="flex justify-center my-8 pb-16"
style="display: none"
>
<button
id="load-more-btn"
class="px-6 py-2 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 font-bold rounded-md transition-colors cursor-pointer"
>
Load More
</button>
</div>
<div id="scroll-sentinel" class="h-16" />
</>
)
}
{
(prevUrl || nextUrl) && (
<noscript>
<nav class="max-w-4xl mx-auto my-8 pb-24 flex px-6 lg:px-0">
{prevUrl && <a href={prevUrl} class="font-bold text-black">← Newer Posts</a>}
{nextUrl && <a href={nextUrl} class="font-bold text-black ml-auto">Older Posts →</a>}
</nav>
</noscript>
)
}

<script>
const STORAGE_KEY = "blogLoadMode"

const grid = document.getElementById("post-grid")
const container = document.getElementById(
"load-more-container"
) as HTMLElement | null
const btn = document.getElementById("load-more-btn") as HTMLButtonElement | null
const toggleContainer = document.getElementById(
"load-mode-toggle"
) as HTMLElement | null
const scrollCheckbox = document.getElementById(
"scroll-toggle"
) as HTMLInputElement | null
const sentinel = document.getElementById("scroll-sentinel")

const savedMode = localStorage.getItem(STORAGE_KEY) ?? "button"

if (toggleContainer) toggleContainer.style.display = ""
if (scrollCheckbox) scrollCheckbox.checked = savedMode === "scroll"

let applyMode = (_mode: string) => {}

if (btn && grid?.dataset.nextUrl) {
let observer: IntersectionObserver | null = null

const loadMore = async () => {
if (!grid.dataset.nextUrl) return
btn.disabled = true
btn.textContent = "Loading…"

try {
const res = await fetch(grid.dataset.nextUrl)
if (!res.ok) throw new Error(`HTTP ${res.status}`)

const doc = new DOMParser().parseFromString(await res.text(), "text/html")

doc.querySelectorAll("#post-grid > div").forEach((card) => {
grid.appendChild(card)
})

const newNextUrl = doc.getElementById("post-grid")?.dataset.nextUrl
if (newNextUrl) {
grid.dataset.nextUrl = newNextUrl
btn.disabled = false
btn.textContent = "Load More"
} else {
grid.removeAttribute("data-next-url")
container?.remove()
sentinel?.remove()
observer?.disconnect()
}
} catch (err) {
console.error("Failed to load more posts:", err)
btn.disabled = false
btn.textContent = "Load More"
}
}

const enableScrollMode = () => {
if (container) container.style.display = "none"
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && grid.dataset.nextUrl) {
loadMore()
}
},
{ rootMargin: "200px" }
)
if (sentinel) observer.observe(sentinel)
}

const enableButtonMode = () => {
observer?.disconnect()
observer = null
if (container) container.style.display = ""
}

applyMode = (mode: string) => {
if (mode === "scroll") {
enableScrollMode()
} else {
enableButtonMode()
}
}

applyMode(savedMode)
btn.addEventListener("click", loadMore)
}

scrollCheckbox?.addEventListener("change", () => {
const mode = scrollCheckbox.checked ? "scroll" : "button"
localStorage.setItem(STORAGE_KEY, mode)
applyMode(mode)
})
</script>
52 changes: 0 additions & 52 deletions src/components/Paging.astro

This file was deleted.

2 changes: 1 addition & 1 deletion src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ORG_SAME_AS = [
"https://localdev.foundation",
]
export const BLOG_DESCRIPTION = `Posts about DDEV, Docker, and local development.`
export const BLOG_PAGE_SIZE = 12
export const BLOG_PAGE_SIZE = 15
export const SHIKI_THEMES = {
light: "github-light-high-contrast",
dark: "github-dark-high-contrast",
Expand Down
2 changes: 1 addition & 1 deletion src/content/blog/2025-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Upcoming v1.25.0:
- **PHP 8.5** support added with a limited set of extensions (in v1.24.10)
- **MariaDB 11.8** support added
- **PostgreSQL 18** support added
- [Node.js as primary web server](https://ddev.readthedocs.io/en/stable/users/extend/customization-extendibility/#using-nodejs-as-ddevs-primary-web-server) support
- [Node.js as primary web server](https://docs.ddev.com/en/stable/users/extend/customization-extendibility/#using-nodejs-as-ddevs-primary-web-server) support

Upcoming v1.25.0:

Expand Down
Loading
Loading