diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 761f2241..bb8acbad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,3 +78,12 @@ jobs: preid: ${{ github.event.inputs['preid'] }} dist-tag: ${{ github.event.inputs['dist-tag'] }} github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Build website + run: npm run build + - name: Deploy website to Neocities + uses: bcomnes/deploy-to-neocities@v3 + with: + api_key: ${{ secrets.NEOCITIES_DOMSTACK_API_TOKEN }} + cleanup: true + neocities_supporter: true + preview_before_deploy: true diff --git a/authors/bcomnes/author-meta.json b/authors/bcomnes/author-meta.json new file mode 100644 index 00000000..5128c2d9 --- /dev/null +++ b/authors/bcomnes/author-meta.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "username": "bcomnes", + "name": "Bret Comnes", + "profile": "https://github.com/bcomnes", + "website": "https://bret.io/", + "links": [ + { + "provider": "twitter", + "url": "https://x.com/bcomnes" + }, + { + "provider": "mastodon", + "url": "https://fosstodon.org/@bcomnes" + }, + { + "provider": "bluesky", + "url": "https://bsky.app/profile/bret.io" + }, + { + "provider": "npm", + "url": "https://www.npmjs.com/~bret" + } + ], + "avatar": "avatar.jpg", + "keys": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIER/jKDDJvSthkj8vbnPLik2TT+DVW0pIZZpzxFSLWc0\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOAuw2eZOGpbPnuicq7MQWLa03N1zLq0F0iJbWI3Dg2V\n", + "gpg": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n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\n=VW1V\n-----END PGP PUBLIC KEY BLOCK-----" +} diff --git a/authors/bcomnes/avatar.jpg b/authors/bcomnes/avatar.jpg new file mode 100644 index 00000000..9a04982b Binary files /dev/null and b/authors/bcomnes/avatar.jpg differ diff --git a/blog/2026/hello-world/page.md b/blog/2026/hello-world/page.md new file mode 100644 index 00000000..459e4022 --- /dev/null +++ b/blog/2026/hello-world/page.md @@ -0,0 +1,17 @@ +--- +layout: blog +title: "Hello, DOMStack" +description: "A first look at the DOMStack blog and the ideas behind building static sites from ordinary files." +publishDate: "2026-09-19T12:00:00Z" +authors: + - bcomnes +--- + + +This is the DOMStack blog: a place for release notes, technical walkthroughs, and notes from building static websites with ordinary HTML, Markdown, CSS, and JavaScript. + +DOMStack keeps the source tree close to the output. Pages are files, layouts are composable functions, and shared indexes and feeds are built from the same page data. + +## Follow along + +Subscribe with [Atom](/feed.xml) or [JSON Feed](/feed.json) to follow future posts. diff --git a/blog/archives.pages.ts b/blog/archives.pages.ts new file mode 100644 index 00000000..cc37020f --- /dev/null +++ b/blog/archives.pages.ts @@ -0,0 +1,19 @@ +import type { DataDeps, PagesFunction } from '@domstack/static/types.js' +import type { BlogData } from '../site/lib/blog.ts' +import { blogIndex } from '../site/lib/blog-index.ts' + +type ArchiveData = Pick +export const dataDeps = ['blogArchives'] satisfies DataDeps + +const archives: PagesFunction<{ layout: 'blog-index'; title: string; description: string }, string, Record, ArchiveData> = ({ data }) => + data.blogArchives.map(archive => ({ + outputName: `${archive.year}/index.html`, + vars: { + layout: 'blog-index' as const, + title: `${archive.year} · Blog · domstack`, + description: `Posts from the ${archive.year} domstack blog archive.`, + }, + children: blogIndex(archive.posts, data.blogArchives, archive.year), + })) + +export default archives diff --git a/blog/atom.svg b/blog/atom.svg new file mode 100644 index 00000000..2ab1936f --- /dev/null +++ b/blog/atom.svg @@ -0,0 +1 @@ + diff --git a/blog/jsonfeed.svg b/blog/jsonfeed.svg new file mode 100644 index 00000000..50371ba6 --- /dev/null +++ b/blog/jsonfeed.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/blog/page.ts b/blog/page.ts new file mode 100644 index 00000000..fe32c9d8 --- /dev/null +++ b/blog/page.ts @@ -0,0 +1,12 @@ +import type { DataDeps, PageFunction } from '@domstack/static/types.js' +import type { BlogData } from '../site/lib/blog.ts' +import { blogIndex } from '../site/lib/blog-index.ts' + +export const vars = { + layout: 'blog-index', + title: 'Blog · domstack', + dataDeps: ['blogPosts', 'blogArchives'] satisfies DataDeps>, +} + +const page: PageFunction> = ({ data }) => blogIndex(data.blogPosts, data.blogArchives) +export default page diff --git a/feed.json.template.ts b/feed.json.template.ts new file mode 100644 index 00000000..3feed243 --- /dev/null +++ b/feed.json.template.ts @@ -0,0 +1,7 @@ +import type { DataDeps, TemplateFunction } from '@domstack/static/types.js' +import type { BlogData } from './site/lib/blog.ts' +import { jsonFeed } from './site/lib/blog-feeds.ts' + +export const dataDeps = ['blogFeed'] satisfies DataDeps> +const template: TemplateFunction<{ siteUrl: string }, Pick> = ({ data, vars }) => jsonFeed(data.blogFeed, vars.siteUrl) +export default template diff --git a/feed.xml.template.ts b/feed.xml.template.ts new file mode 100644 index 00000000..9fd83f43 --- /dev/null +++ b/feed.xml.template.ts @@ -0,0 +1,7 @@ +import type { DataDeps, TemplateFunction } from '@domstack/static/types.js' +import type { BlogData } from './site/lib/blog.ts' +import { atomFeed } from './site/lib/blog-feeds.ts' + +export const dataDeps = ['blogFeed'] satisfies DataDeps> +const template: TemplateFunction<{ siteUrl: string }, Pick> = ({ data, vars }) => atomFeed(data.blogFeed, vars.siteUrl) +export default template diff --git a/package.json b/package.json index bacf34ad..ef38a9ea 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,9 @@ "build": "npm run clean && npm run build:defaults && run-p build:domstack build:declaration build:schema", "build:defaults": "node scripts/build-defaults.js", "build:domstack": "./bin.js --src . --ignore examples,test-cases,test-fixtures,type-tests,coverage,*.tsconfig.json,fonts,/lib,/types,/scripts,/plans,/browser-tests,/test-results,/lcov.info,/tsconfig.json,/AGENTS.md,/AGENT.md,/agents.md", + "new-blogpost": "node tools/blog/create.ts", + "publish-draft": "node tools/blog/publish.ts", + "import-author": "node tools/authors/import.ts", "build:declaration": "tsc -p declaration.tsconfig.json", "build:schema": "node scripts/domstack-manifest-schema.js", "watch": "npm run clean && run-p watch:*", diff --git a/site/globals/global.data.ts b/site/globals/global.data.ts index 2c2173a9..fafbd5e4 100644 --- a/site/globals/global.data.ts +++ b/site/globals/global.data.ts @@ -1,5 +1,6 @@ import type { GlobalDataFunctionParams } from '../../types.ts' import type { DocsNavigationIndex, DocsPageVars } from '../layouts/docs/navigation.js' +import { projectBlog, readBlogPost, type BlogData } from '../lib/blog.ts' import { cpus } from 'node:os' import pMap from 'p-map' import { render } from 'fragtml' @@ -7,7 +8,9 @@ import { readDocsNavigationPage, projectDocsNavigation, docsIndex } from '../lay const MAX_CONCURRENCY = Math.min(cpus().length, 24) -export default async function ({ pages, previousState, changes, setState }: GlobalDataFunctionParams) { +type SourcePageVars = DocsPageVars & { layout?: string } + +export default async function ({ pages, previousState, changes, setState }: GlobalDataFunctionParams): Promise<{ docsNavigation: ReturnType; docsIndexHtml: string } & BlogData> { let index: DocsNavigationIndex let inputs: typeof pages switch (changes.kind) { @@ -33,6 +36,8 @@ export default async function ({ pages, previousState, changes, setState }: Glob const docsNavigation = projectDocsNavigation(index.values()) const docsIndexHtml = render(docsIndex(docsNavigation)) + const blogPosts = await pMap(pages.filter(page => page.vars.layout === 'blog'), readBlogPost, { concurrency: MAX_CONCURRENCY }) + const blog = projectBlog(blogPosts) setState(index) - return { docsNavigation, docsIndexHtml } + return { docsNavigation, docsIndexHtml, ...blog } } diff --git a/site/globals/global.vars.ts b/site/globals/global.vars.ts new file mode 100644 index 00000000..483dd876 --- /dev/null +++ b/site/globals/global.vars.ts @@ -0,0 +1,6 @@ +export default { + siteName: 'domstack', + siteUrl: 'https://domstack.net', + editRepository: 'https://github.com/bcomnes/domstack/edit/master', + lang: 'en', +} diff --git a/site/layouts/blog-index/blog-index.layout.css b/site/layouts/blog-index/blog-index.layout.css new file mode 100644 index 00000000..5b2e2be8 --- /dev/null +++ b/site/layouts/blog-index/blog-index.layout.css @@ -0,0 +1,83 @@ +@import '../blog/blog.layout.css'; + +@layer domstack.layout { + .blog-index-header { + margin-block-end: 0; + } + + .blog-index-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + } + + .blog-index-heading h1 { + margin: 0; + letter-spacing: -0.03em; + } + + .blog-index-heading .blog-feeds { + flex: none; + margin: 0; + } + + .blog-index-description { + max-inline-size: 56ch; + margin: 1rem 0 1.5rem; + color: var(--site-muted); + } + + .blog-years { + align-items: center; + gap: 0.5rem 1.5rem; + margin-block-end: 2rem; + font-size: 0.875rem; + } + + .blog-years [aria-current="page"] { + color: var(--text); + font-weight: 700; + } + + .blog-list > li { + padding-block: 2rem; + } + + .blog-list h2 { + margin: 0 0 0.5rem; + font-size: clamp(1.5rem, 3vw, 1.75rem); + letter-spacing: -0.02em; + } + + .blog-list .blog-byline { + margin-block-end: 1rem; + } + + .blog-list .blog-summary { + max-inline-size: 64ch; + margin: 0; + color: var(--site-muted); + } + + .blog-empty { + margin: 0; + padding-block: 2rem; + border-block-start: 1px solid var(--site-border); + color: var(--site-muted); + } + + @media (max-width: 40rem) { + .blog-index-heading { + align-items: flex-start; + } + + .blog-index-description { + margin-block: 1rem 1.5rem; + } + + .blog-years { + gap: 0.25rem 1rem; + } + } +} diff --git a/site/layouts/blog-index/blog-index.layout.ts b/site/layouts/blog-index/blog-index.layout.ts new file mode 100644 index 00000000..0fcd19b1 --- /dev/null +++ b/site/layouts/blog-index/blog-index.layout.ts @@ -0,0 +1,28 @@ +import { html, raw, render } from 'fragtml' +import type { LayoutFunction } from '@domstack/static/types.js' + +export interface BlogIndexVars { + layout: 'blog-index' + title?: string + description?: string + [key: string]: unknown +} + +export const parentLayout = 'root' as const + +const blogIndexLayout: LayoutFunction = ({ children }) => render(html` +
+
${raw(children)}
+
+`) + +export default blogIndexLayout + +declare module '@domstack/static/types.js' { + interface LayoutRegistry { + 'blog-index': { + parentLayout: typeof parentLayout + render: typeof blogIndexLayout + } + } +} diff --git a/site/layouts/blog/blog.layout.css b/site/layouts/blog/blog.layout.css new file mode 100644 index 00000000..3b271b7c --- /dev/null +++ b/site/layouts/blog/blog.layout.css @@ -0,0 +1,54 @@ +@layer domstack.layout { + .blog-main { + padding: 3rem max(1.25rem, env(safe-area-inset-right)) 5rem; + } + + .blog-column { + max-inline-size: 72ch; + margin-inline: auto; + font-size: 1.1rem; + line-height: 1.65; + overflow-wrap: anywhere; + } + + .blog-column h1, + .blog-column h2, + .blog-column h3 { + line-height: 1.2; + } + + .blog-column h1 { font-size: clamp(2rem, 6vw, 3rem); } + .blog-column h2 { margin-block-start: 2.5rem; } + .blog-feeds, + .blog-years { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; } + .blog-feeds { margin-block: 1.5rem; } + .blog-feeds a { display: inline-flex; align-items: center; justify-content: center; min-inline-size: 1.5rem; min-block-size: 1.5rem; } + .blog-feeds img { display: block; inline-size: 1.5rem; block-size: 1.5rem; border-radius: 0.2rem; } + .blog-years { margin-block-end: 2rem; } + .blog-years [aria-current="page"] { font-weight: 700; } + .blog-breadcrumb { margin-block-end: 1rem; } + .blog-list { list-style: none; padding: 0; margin: 0; } + .blog-list > li { border-block-start: 1px solid var(--site-border); padding-block: 2rem; } + .blog-list h2 { margin-block: 0.5rem 0.25rem; } + .blog-list p { margin-block: 1rem 0; } + .blog-list .blog-byline { margin: 0 0 1rem; } + .blog-list .blog-summary { margin: 0; } + .blog-byline, + .blog-updated { color: var(--site-muted); font-size: 0.875rem; } + .blog-byline, + .blog-authors { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.5rem; } + .blog-authors { gap: 0.5rem 1rem; } + .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); } + .blog-article-meta .blog-breadcrumb { margin-block-end: 1rem; } + .blog-article-meta .blog-title { max-inline-size: 24ch; margin: 0 0 0.5rem; text-wrap: balance; } + .blog-article-meta .blog-author img { width: 2rem; height: 2rem; } + .blog-article-meta .blog-byline { margin: 0; } + .blog-article-meta .blog-updated { margin-block-start: 0.5rem; } + .blog-article-footer { border-block-start: 1px solid var(--site-border); margin-block-start: 3rem; padding-block-start: 1.5rem; } + .blog-prose img, + .blog-prose video { max-inline-size: 100%; block-size: auto; } + .blog-prose pre { overflow-x: auto; padding: 1rem; border-radius: 0.4rem; } + .blog-prose blockquote { margin-inline: 0; padding-inline-start: 1.5rem; border-inline-start: 4px solid var(--link-text); } +} diff --git a/site/layouts/blog/blog.layout.ts b/site/layouts/blog/blog.layout.ts new file mode 100644 index 00000000..85f56eb5 --- /dev/null +++ b/site/layouts/blog/blog.layout.ts @@ -0,0 +1,56 @@ +import { html, raw, render } from 'fragtml' +import type { AsyncLayoutFunction } from '@domstack/static/types.js' +import type { BlogPostVars } from '../../lib/blog.ts' +import { blogYear, formatBlogDate, validateBlogVars } from '../../lib/blog.ts' + +export const parentLayout = 'root' as const + +const blogLayout: AsyncLayoutFunction = async ({ vars, children, page }) => { + const post = await validateBlogVars({ ...vars }, page.url) + const year = blogYear(page.url) + + return render(html` +
+
+
+ +
${raw(children)}
+ +
+
+
+ `) +} + +export default blogLayout + +declare module '@domstack/static/types.js' { + interface LayoutRegistry { + blog: { + parentLayout: typeof parentLayout + render: typeof blogLayout + } + } +} diff --git a/site/layouts/docs/navigation-watch.test.js b/site/layouts/docs/navigation-watch.test.js index 486df97b..60764095 100644 --- a/site/layouts/docs/navigation-watch.test.js +++ b/site/layouts/docs/navigation-watch.test.js @@ -27,7 +27,9 @@ test('heading changes refresh shared navigation; body edits leave other pages al 'site/globals/global.data.ts', 'site/layouts/docs/navigation.js', 'site/layouts/docs/docs.layout.js', - 'site/layouts/root/root.layout.js', + 'site/layouts/root/root.layout.ts', + 'site/lib/authors.ts', + 'site/lib/blog.ts', ]) { await mkdir(dirname(join(src, file)), { recursive: true }) await cp(join(root, file), join(src, file)) @@ -104,7 +106,7 @@ test('docs producer retains navigation while rendering only the required sources setState (next) { state = structuredClone(next) }, }) assert.deepEqual(rendered.sort(), expectedRenders, 'actual renderInnerPage calls, including duplicates') - assert.deepEqual(Object.keys(data).sort(), ['docsIndexHtml', 'docsNavigation']) + assert.deepEqual(Object.keys(data).sort(), ['blogArchives', 'blogFeed', 'blogPosts', 'docsIndexHtml', 'docsNavigation']) return data } diff --git a/site/layouts/root/root.layout.css b/site/layouts/root/root.layout.css index bcf4b975..5e8a0ac6 100644 --- a/site/layouts/root/root.layout.css +++ b/site/layouts/root/root.layout.css @@ -154,6 +154,11 @@ margin: 0; } + .page-edit { + margin-inline-start: auto; + white-space: nowrap; + } + @media (max-width: 40rem) { .site-header-inner, .site-links { @@ -168,5 +173,9 @@ align-items: start; flex-direction: column; } + + .page-edit { + margin-inline-start: 0; + } } } diff --git a/site/layouts/root/root.layout.test.js b/site/layouts/root/root.layout.test.js index 79383c11..d3962bac 100644 --- a/site/layouts/root/root.layout.test.js +++ b/site/layouts/root/root.layout.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { test } from 'node:test' import { load } from 'cheerio' import { html, raw } from 'fragtml' -import rootLayout from './root.layout.js' +import rootLayout from './root.layout.ts' import docsLayout from '../docs/docs.layout.js' test('site shell preserves children, escapes metadata, and creates exactly one main landmark', async () => { diff --git a/site/layouts/root/root.layout.js b/site/layouts/root/root.layout.ts similarity index 61% rename from site/layouts/root/root.layout.js rename to site/layouts/root/root.layout.ts index d9e05030..f102eed0 100644 --- a/site/layouts/root/root.layout.js +++ b/site/layouts/root/root.layout.ts @@ -1,27 +1,36 @@ -/** - * @import { LayoutFunction } from '#types' - * @import { HtmlResult } from 'fragtml/types.js' - */ import { html, raw, render } from 'fragtml' +import type { HtmlResult } from 'fragtml/types.js' +import type { LayoutFunction } from '@domstack/static/types.js' import { navigationHref } from '../docs/navigation.js' -function year () { +export interface RootVars { + title?: string + layout?: string + lang?: string + basePath?: string + siteUrl?: string + [key: string]: unknown +} + +function year (): number { return new Date().getFullYear() } -/** - * The website owns its document shell; the docs layout owns the sidebar and - * main landmark. Other pages get a simple main landmark here instead. - * @type {LayoutFunction<{ title?: string, layout?: string, lang?: string, basePath?: string }, string | HtmlResult, string>} - */ -export default function rootLayout ({ children, vars, page, scripts, styles }) { +function editUrl (sourceRelname: string): string { + return 'https://github.com/bcomnes/domstack/edit/master/' + sourceRelname.replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/') +} + +const rootLayout: LayoutFunction = ({ children, vars, page, scripts, styles }) => { const isDocs = vars.layout === 'docs' + const ownsMain = isDocs || vars.layout === 'blog' || vars.layout === 'blog-index' const content = typeof children === 'string' ? raw(children) : children const home = navigationHref(page.url, '/') const docs = navigationHref(page.url, '/docs/') const examples = navigationHref(page.url, '/docs/example-projects/') - /** @param {string} path */ - const assetUrl = path => path.startsWith('/') ? `${vars.basePath ?? ''}${path}` : path + const blog = navigationHref(page.url, '/blog/') + const assetUrl = (path: string): string => path.startsWith('/') ? `${vars.basePath ?? ''}${path}` : path + const editSource = page.generated?.pagesFile.pagesFile.relname ?? page.pageFile?.relname ?? 'README.md' + const menuToggle = html`