Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '../../../site/lib/diagrams/client.ts'
249 changes: 249 additions & 0 deletions blog/2026/inside-the-domstack-build-and-watch-cycle/page.draft.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions blog/2026/inside-the-domstack-build-and-watch-cycle/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import '../../../site/lib/diagrams/style.css';

@layer domstack.page {
.blog-column {
max-inline-size: 72rem;
}

/* Give diagrams room without lengthening the prose's reading measure. */
.blog-article-meta,
.blog-article-footer,
.blog-prose > :not(.mermaid) {
max-inline-size: 72ch;
margin-inline: auto;
}

.mermaid > svg {
max-inline-size: 100%;
max-width: 100%;
block-size: auto;
height: auto;
}
}
88 changes: 88 additions & 0 deletions browser-tests/blog-diagrams.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { resolve } from 'node:path'
import { expect, test, websiteOptions } from './support.js'

const postPath = '/blog/2026/inside-the-domstack-build-and-watch-cycle/'

test.use({
siteSrc: resolve(import.meta.dirname, '..'),
siteOptions: { ...websiteOptions, buildDrafts: true },
})

test('the architecture draft renders page-scoped diagrams in both themes and on narrow screens', async ({ page, siteURL }) => {
const errors = []
page.on('pageerror', error => errors.push(error.message))
await page.goto(`${siteURL}${postPath}`)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Inside the DOMStack build and watch cycle')

const diagrams = page.locator('.mermaid')
await expect(diagrams).toHaveCount(4)
for (const diagram of await diagrams.all()) {
await expect(diagram.locator('svg')).toBeVisible()
await expect(diagram.locator('.error-icon')).toHaveCount(0)
}

const colors = new Set()
for (const colorScheme of ['light', 'dark']) {
await page.emulateMedia({ colorScheme })
const color = await page.locator('body').evaluate(element => getComputedStyle(element).color)
colors.add(color)
for (const diagram of await diagrams.all()) {
await expect(diagram.locator('.flowchart-link').first()).toHaveCSS('stroke', color)
await expect(diagram.locator('.marker path').first()).toHaveCSS('fill', color)
}
}
expect(colors.size).toBe(2)

for (const width of [1440, 768, 375]) {
await page.setViewportSize({ width, height: 900 })
for (const diagram of await diagrams.all()) {
const dimensions = await diagram.evaluate(element => {
const svg = element.querySelector('svg')
const bounds = svg.getBoundingClientRect()
const viewBox = svg.viewBox.baseVal
return {
overflow: element.scrollWidth - element.clientWidth,
renderedRatio: bounds.width / bounds.height,
sourceRatio: viewBox.width / viewBox.height,
}
})
expect(dimensions.overflow, `diagram overflow at ${width}px`).toBeLessThanOrEqual(1)
expect(dimensions.renderedRatio).toBeCloseTo(dimensions.sourceRatio, 2)
}
const overflowsPage = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth)
expect(overflowsPage).toBe(false)
}

await page.setViewportSize({ width: 1440, height: 900 })
const diagramWidth = await diagrams.first().evaluate(element => element.getBoundingClientRect().width)
const proseWidth = await page.locator('.blog-prose > p').first().evaluate(element => element.getBoundingClientRect().width)
expect(diagramWidth).toBeGreaterThan(proseWidth)
expect(errors).toEqual([])
})

test.describe('without JavaScript', () => {
test.use({ javaScriptEnabled: false })

test('the article and diagram source remain readable', async ({ page, siteURL }) => {
await page.goto(`${siteURL}${postPath}`)
await expect(page.getByRole('heading', { name: 'Global data is not one big cache', exact: true })).toBeVisible()
const diagrams = page.locator('.mermaid')
await expect(diagrams).toHaveCount(4)
for (const diagram of await diagrams.all()) {
await expect(diagram).toContainText('flowchart TD')
await expect(diagram.locator('svg')).toHaveCount(0)
}
})
})

test.describe('production build', () => {
test.use({ siteOptions: websiteOptions })

test('the draft is not published or included in feeds', async ({ page, siteURL }) => {
const response = await page.goto(`${siteURL}${postPath}`)
expect(response?.status()).toBe(404)
const feed = await page.request.get(`${siteURL}/feed.json`)
expect(feed.ok()).toBe(true)
expect(await feed.text()).not.toContain(postPath)
})
})
38 changes: 38 additions & 0 deletions browser-tests/blog-drafts.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { resolve } from 'node:path'
import { expect, test, websiteOptions } from './support.js'

const draftPath = '/blog/2026/inside-the-domstack-build-and-watch-cycle/'
const publishedPath = '/blog/2026/hello-world/'

test.use({
siteSrc: resolve(import.meta.dirname, '..'),
siteOptions: { ...websiteOptions, buildDrafts: true },
javaScriptEnabled: false,
})

test('draft badges appear on posts, the index and archives without marking published posts', async ({ page, siteURL }) => {
for (const colorScheme of ['light', 'dark']) {
await page.emulateMedia({ colorScheme })
await page.setViewportSize({ width: 375, height: 812 })
await page.goto(`${siteURL}${draftPath}`)
const badge = page.locator('.blog-article-meta .blog-draft-badge')
await expect(badge).toHaveText('Draft')
await expect(badge).toBeVisible()
await expect(badge).toHaveCSS('border-top-style', 'solid')
await expect(badge).toHaveCSS('font-weight', '700')

for (const path of ['/blog/', '/blog/2026/']) {
await page.goto(`${siteURL}${path}`)
const draft = page.locator('.blog-list article').filter({ has: page.locator(`h2 a[href="${draftPath}"]`) })
await expect(draft.locator('.blog-draft-badge')).toBeVisible()
await expect(draft.locator('.blog-draft-badge')).toHaveText('Draft')
const published = page.locator('.blog-list article').filter({ has: page.locator(`h2 a[href="${publishedPath}"]`) })
await expect(published).toHaveCount(1)
await expect(published.locator('.blog-draft-badge')).toHaveCount(0)
}

await page.goto(`${siteURL}${publishedPath}`)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Hello, DOMStack')
await expect(page.locator('.blog-draft-badge')).toHaveCount(0)
}
})
24 changes: 1 addition & 23 deletions docs/implementation/client.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1 @@
/// <reference lib="dom" />

import mermaid from 'mermaid'

mermaid.initialize({
startOnLoad: true,
// Keep explicit line breaks and parallel lanes instead of shrinking a wide
// graph to fit the article. The page stylesheet provides horizontal scrolling.
markdownAutoWrap: false,
htmlLabels: false,
flowchart: {
useMaxWidth: false,
curve: 'linear',
nodeSpacing: 24,
rankSpacing: 36,
padding: 12,
wrappingWidth: 320,
subGraphTitleMargin: { top: 8, bottom: 16 },
},
themeVariables: {
fontFamily: 'system-ui, sans-serif'
}
})
import '../../site/lib/diagrams/client.ts'
47 changes: 1 addition & 46 deletions docs/implementation/style.css
Original file line number Diff line number Diff line change
@@ -1,46 +1 @@
@layer domstack.page {
.mermaid {
max-inline-size: 100%;
overflow-x: auto;
padding: 1rem;
border: 1px solid var(--site-border);
border-radius: 0.4rem;
background: var(--background);
box-shadow: none;
}

.mermaid > svg {
display: block;
/* Preserve Mermaid's intrinsic dimensions so text stays readable. */
max-inline-size: none;
max-width: none;
margin-inline: auto;
}

/* Mermaid embeds ID-scoped theme rules in each SVG. Override only its
surfaces, labels, and edges so diagrams follow the site's live light/dark
theme without passing CSS color keywords to Mermaid's color parser. */
.mermaid .node :is(rect, polygon, circle, path),
.mermaid .edgeLabel rect {
fill: var(--background) !important;
stroke: var(--site-muted) !important;
}

.mermaid .cluster rect {
fill: color-mix(in srgb, var(--text) 3%, var(--background)) !important;
stroke: var(--site-muted) !important;
}

.mermaid .flowchart-link {
stroke: var(--text) !important;
}

.mermaid .marker {
fill: var(--text) !important;
stroke: var(--text) !important;
}

.mermaid text {
fill: var(--text) !important;
}
}
@import '../../site/lib/diagrams/style.css';
13 changes: 13 additions & 0 deletions site/layouts/blog/blog.layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@
.blog-byline,
.blog-authors { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.5rem; }
.blog-authors { gap: 0.5rem 1rem; }
.blog-draft-badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border: 1px solid currentColor;
border-radius: 0.25rem;
color: var(--link-text);
background: color-mix(in srgb, var(--link-text) 10%, var(--background));
font-size: 0.75rem;
font-weight: 700;
line-height: 1.4;
letter-spacing: 0.04em;
white-space: nowrap;
}
.blog-author { display: inline-flex; align-items: center; gap: 0.5rem; min-width: 0; }
.blog-author img { flex-shrink: 0; border-radius: 50%; object-fit: cover; width: 1.5rem; height: 1.5rem; }
.blog-article-meta { margin-block-end: 2rem; padding-block-end: 1.5rem; border-block-end: 1px solid var(--site-border); }
Expand Down
1 change: 1 addition & 0 deletions site/layouts/blog/blog.layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const blogLayout: AsyncLayoutFunction<BlogPostVars, string, string> = async ({ v
`)}
</span>
<time datetime="${post.publishDate}">${formatBlogDate(post.publishDate)}</time>
${page.draft ? html`<span class="blog-draft-badge">Draft</span>` : null}
</p>
${post.updatedDate
? html`<p class="blog-updated">Updated <time datetime="${post.updatedDate}">${formatBlogDate(post.updatedDate)}</time></p>`
Expand Down
2 changes: 1 addition & 1 deletion site/lib/blog-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function blogIndex (posts: readonly BlogSummary[], archives: readonly Pic
`)}
</span>
<time datetime="${post.publishDate}">${formatBlogDate(post.publishDate)}</time>
${post.draft ? html`<span>Draft</span>` : null}
${post.draft ? html`<span class="blog-draft-badge">Draft</span>` : null}
</p>
<p class="blog-summary">${post.description}</p>
</article>
Expand Down
10 changes: 10 additions & 0 deletions site/lib/blog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ test('blog index feed links expose icons, feed types, and alternate relations',
])
})

test('blog index and archives badge only draft posts', () => {
const draft = { ...post, url: '/blog/2026/draft/', draft: true }
for (const year of [undefined, '2026']) {
const $ = load(blogIndex([post, draft], [{ year: '2026', url: '/blog/2026/' }], year))
assert.equal($('.blog-draft-badge').length, 1)
assert.equal($('.blog-draft-badge').text(), 'Draft')
assert.equal($('.blog-draft-badge').closest('article').find('h2 a').attr('href'), draft.url)
}
})

test('feeds include ordered author metadata, avatars, and absolute article assets', () => {
const $ = load(feedHtml(post, site))
assert.equal($('a').attr('href'), `${site}/blog/2026/other/`)
Expand Down
23 changes: 23 additions & 0 deletions site/lib/diagrams/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference lib="dom" />

import mermaid from 'mermaid'

mermaid.initialize({
startOnLoad: true,
// Keep explicit line breaks and parallel lanes instead of shrinking a wide
// graph to fit the article. The page stylesheet provides horizontal scrolling.
markdownAutoWrap: false,
htmlLabels: false,
flowchart: {
useMaxWidth: false,
curve: 'linear',
nodeSpacing: 24,
rankSpacing: 36,
padding: 12,
wrappingWidth: 320,
subGraphTitleMargin: { top: 8, bottom: 16 },
},
themeVariables: {
fontFamily: 'system-ui, sans-serif'
}
})
46 changes: 46 additions & 0 deletions site/lib/diagrams/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@layer domstack.page {
.mermaid {
max-inline-size: 100%;
overflow-x: auto;
padding: 1rem;
border: 1px solid var(--site-border);
border-radius: 0.4rem;
background: var(--background);
box-shadow: none;
}

.mermaid > svg {
display: block;
/* Preserve Mermaid's intrinsic dimensions so text stays readable. */
max-inline-size: none;
max-width: none;
margin-inline: auto;
}

/* Mermaid embeds ID-scoped theme rules in each SVG. Override only its
surfaces, labels, and edges so diagrams follow the site's live light/dark
theme without passing CSS color keywords to Mermaid's color parser. */
.mermaid .node :is(rect, polygon, circle, path),
.mermaid .edgeLabel rect {
fill: var(--background) !important;
stroke: var(--site-muted) !important;
}

.mermaid .cluster rect {
fill: color-mix(in srgb, var(--text) 3%, var(--background)) !important;
stroke: var(--site-muted) !important;
}

.mermaid .flowchart-link {
stroke: var(--text) !important;
}

.mermaid .marker {
fill: var(--text) !important;
stroke: var(--text) !important;
}

.mermaid text {
fill: var(--text) !important;
}
}
Loading