Blog #1

Merged
Dave merged 22 commits from blog into main 2026-09-30 16:48:38 +02:00
24 changed files with 185 additions and 24 deletions
Showing only changes of commit 6bd917e81b - Show all commits

View File

@@ -19,7 +19,8 @@
"@markdoc/markdoc": "^0.4.0",
"astro": "^7.0.4",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"sharp": "^0.35.4"
},
"devDependencies": {
"@astrojs/check": "^0.9.10",

7
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
react-dom:
specifier: ^19.2.8
version: 19.3.0(react@19.3.0)
sharp:
specifier: ^0.35.4
version: 0.35.4
devDependencies:
'@astrojs/check':
specifier: ^0.9.10
@@ -4135,8 +4138,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@img/colour@1.1.0':
optional: true
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.4':
optionalDependencies:
@@ -6572,7 +6574,6 @@ snapshots:
'@img/sharp-win32-arm64': 0.35.4
'@img/sharp-win32-ia32': 0.35.4
'@img/sharp-win32-x64': 0.35.4
optional: true
shebang-command@2.0.0:
dependencies:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -14,9 +14,9 @@ type Props = {
const { collection, id, status, diagnostics = [] } = Astro.props
const NOTICE: Partial<Record<Status, string>> = {
draft: 'Draft – not build in production',
draft: 'Draft – not built in production',
unlisted: 'Unlisted – has page, but appears in no list',
archived: 'Archived - has page, but appears in no list',
archived: 'Archived – has page, but appears in no list',
}
const site = import.meta.env.DEV ? await getSite() : null

View File

@@ -1,8 +1,18 @@
---
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
import ContentImage from '@/components/media/ContentImage.astro'
import Figure from '../media/Figure.astro'
type Props = { node: RenderableTreeNode }
const COMPONENTS: Record<string, AstroComponentFactory> = {
Figure,
Image: ContentImage,
}
const { node } = Astro.props
const Element = Markdoc.Tag.isTag(node) ? node.name : null
---

View File

@@ -0,0 +1,20 @@
---
import { Image } from 'astro:assets'
import { resolveImage } from '@/lib/images/resolve'
type Props = {
src: string
alt: string
}
const { src, alt } = Astro.props
const image = resolveImage(src)
if (!image && import.meta.env.PROD) throw new Error(`Image not found: ${src}`)
---
{image ? (
<Image src={image} alt={alt} />
) : (
<span class="missing-image">Missing image: {src}</span>
)}

View File

@@ -0,0 +1,25 @@
---
import type { FigureSize } from '@/lib/constants/figure'
import ContentImage from './ContentImage.astro'
type Props = {
image: string
alt: string
caption?: string
credit?: string
size?: FigureSize
}
const { image, alt, caption, credit, size = 'inline' } = Astro.props
---
<figure class:list={['figure', `figure--${size}`]}>
<ContentImage src={image} alt={alt} />
{(caption || credit) && (
<figcaption>
{caption && <span class="caption">{caption}</span>}
{credit && <span class="credit">{credit}</span>}
</figcaption>
)}
</figure>

View File

@@ -7,14 +7,10 @@ summary:
excerpt: >-
Where we explore the crucible – a materialistic alchemist Worldbuilding
Framework
cover:
image: /src/assets/content/articles/the-crucible/summary/cover/image.png
alt: Just a test
caption: Test
credit: By ME
cover: {}
dates:
published: 2026-09-21T12:33:00.000Z
tags:
- the-crucible
---
![](/src/assets/content/articles/the-crucible/Screenshot%202026-09-15%20at%2010.21.17.png)
hmm, do you even sharp?

View File

@@ -0,0 +1,31 @@
import { fields } from '@keystatic/core'
import { block } from '@keystatic/core/content-components'
import { FIGURE_SIZES } from '@/lib/constants/figure'
import { assets } from '@/lib/images/storage'
export const figure = (collection: string) =>
block({
label: 'Figure',
description: 'An image with alt text, and optionally a caption and credit.',
schema: {
image: fields.image({
label: 'Image',
...assets(collection),
validation: {
isRequired: true,
},
}),
alt: fields.text({
label: 'Caption',
}),
credit: fields.text({
label: 'Credit',
}),
size: fields.select({
label: 'Size',
options: FIGURE_SIZES,
defaultValue: 'inline',
}),
},
})

View File

@@ -0,0 +1,5 @@
import { figure } from './figure'
export const generalBlocks = (collection: string) => ({
figure: figure(collection),
})

View File

@@ -1,6 +1,7 @@
import { fields } from '@keystatic/core'
import { assets } from '@/lib/content/assets'
import { generalBlocks } from '@/keystatic/blocks/general'
import { assets } from '@/lib/images/storage'
export const content = (collection: string) =>
fields.markdoc({
@@ -9,4 +10,5 @@ export const content = (collection: string) =>
heading: [2, 3, 4],
image: assets(collection),
},
components: generalBlocks(collection),
})

View File

@@ -1,6 +1,6 @@
import { fields } from '@keystatic/core'
import { assets } from '@/lib/content/assets'
import { assets } from '@/lib/images/storage'
export const cover = (collection: string) =>
fields.object(

View File

@@ -1,11 +1,11 @@
---
import DevBanner from '@/components/dev/DevBanner.astro'
import Node from '@/components/markdoc/Node.astro'
import Figure from '@/components/media/Figure.astro'
import BaseLayout from '@/layouts/BaseLayout.astro'
import type { Rendered } from '@/lib/markdoc/types'
import type { Article } from '@/lib/site/types'
import BaseLayout from './BaseLayout.astro'
type Props = {
article: Article
rendered: Rendered
@@ -23,6 +23,15 @@ const { article, rendered } = Astro.props
/>
<article class="article">
<h1>{article.title}</h1>
{article.summary.cover.image && (
<Figure
image={article.summary.cover.image}
alt={article.summary.cover.alt}
caption={article.summary.cover.caption}
credit={article.summary.cover.credit}
size="wide"
/>
)}
<div class="prose">
<Node node={rendered.tree} />
</div>

View File

@@ -0,0 +1,16 @@
export const FIGURE_SIZES = [
{
label: 'Inline',
value: 'inline',
},
{
label: 'Wide',
value: 'wide',
},
{
label: 'Full',
value: 'full',
},
]
export type FigureSize = (typeof FIGURE_SIZES)[number]['value']

View File

@@ -1,6 +0,0 @@
export const ASSET_ROOT = 'src/assets/content'
export const assets = (collection: string) => ({
directory: `${ASSET_ROOT}/${collection}`,
publicPath: `/${ASSET_ROOT}/${collection}/`,
})

View File

@@ -0,0 +1,8 @@
import type { ImageMetadata } from 'astro'
const images = import.meta.glob<ImageMetadata>(
'/src/assets/content/**/*.{png,jpg,jpeg,webp,avif,gif,svg,PNG,JPG,JPEG,WEBP,AVIF,GIF,SVG}',
{ eager: true, import: 'default' },
)
export const resolveImage = (src: string): ImageMetadata | undefined => images[decodeURI(src)]

15
src/lib/images/storage.ts Normal file
View File

@@ -0,0 +1,15 @@
import { slugify } from '../content/slug'
export const ASSET_ROOT = 'src/assets/content'
export function cleanFilename(filename: string): string {
const dot = filename.lastIndexOf('.')
if (dot <= 0) return slugify(filename)
return `${slugify(filename.slice(0, dot)) || 'image'}${filename.slice(dot).toLowerCase()}`
}
export const assets = (collection: string) => ({
directory: `${ASSET_ROOT}/${collection}`,
publicPath: `/${ASSET_ROOT}/${collection}/`,
transformFilename: cleanFilename,
})

View File

@@ -2,6 +2,7 @@ import Markdoc from '@markdoc/markdoc'
import { createDiagnostics } from '../diagnostics'
import { createNodes } from './nodes'
import { tags } from './tags'
import type { NodeState, Renderable, Rendered } from './types'
const LEVEL = {
@@ -18,6 +19,7 @@ export async function render({ collection, id, content }: Renderable): Promise<R
}
const config = {
nodes: createNodes(state),
tags,
}
const problems = createDiagnostics()

View File

@@ -45,5 +45,19 @@ export function createNodes(state: NodeState): Record<string, Schema> {
return new Tag('div', { class: 'table-scroll', tabindex: 0 }, [table])
},
},
image: {
...Markdoc.nodes.image,
render: 'Image',
validate: (node) =>
node.attributes.alt
? []
: [
{
id: 'image-alt',
level: 'warning',
message: 'An image has no alt text',
},
],
},
}
}

12
src/lib/markdoc/tags.ts Normal file
View File

@@ -0,0 +1,12 @@
import { fields } from '@keystatic/core'
import { generalBlocks } from '@/keystatic/blocks/general'
export const tags = fields.markdoc.createMarkdocConfig({
components: generalBlocks('articles'),
render: {
tags: {
figure: 'Figure',
},
},
}).tags

View File

@@ -1,8 +1,8 @@
---
import type { GetStaticPaths } from 'astro'
import ArticleLayout from '@/layouts/ArticleLayout.astro'
import PageLayout from '@/layouts/PageLayout.astro'
import ArticleLayout from '@/layouts/general/ArticleLayout.astro'
import PageLayout from '@/layouts/general/PageLayout.astro'
import { assertNoErrors } from '@/lib/diagnostics/report'
import { render } from '@/lib/markdoc'
import { getSite } from '@/lib/site'