Added images
This commit is contained in:
@@ -19,7 +19,8 @@
|
|||||||
"@markdoc/markdoc": "^0.4.0",
|
"@markdoc/markdoc": "^0.4.0",
|
||||||
"astro": "^7.0.4",
|
"astro": "^7.0.4",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8",
|
||||||
|
"sharp": "^0.35.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "^0.9.10",
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -35,6 +35,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.8
|
specifier: ^19.2.8
|
||||||
version: 19.3.0(react@19.3.0)
|
version: 19.3.0(react@19.3.0)
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.35.4
|
||||||
|
version: 0.35.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@astrojs/check':
|
'@astrojs/check':
|
||||||
specifier: ^0.9.10
|
specifier: ^0.9.10
|
||||||
@@ -4135,8 +4138,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@img/colour@1.1.0':
|
'@img/colour@1.1.0': {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.35.4':
|
'@img/sharp-darwin-arm64@0.35.4':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -6572,7 +6574,6 @@ snapshots:
|
|||||||
'@img/sharp-win32-arm64': 0.35.4
|
'@img/sharp-win32-arm64': 0.35.4
|
||||||
'@img/sharp-win32-ia32': 0.35.4
|
'@img/sharp-win32-ia32': 0.35.4
|
||||||
'@img/sharp-win32-x64': 0.35.4
|
'@img/sharp-win32-x64': 0.35.4
|
||||||
optional: true
|
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 373 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -14,9 +14,9 @@ type Props = {
|
|||||||
const { collection, id, status, diagnostics = [] } = Astro.props
|
const { collection, id, status, diagnostics = [] } = Astro.props
|
||||||
|
|
||||||
const NOTICE: Partial<Record<Status, string>> = {
|
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',
|
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
|
const site = import.meta.env.DEV ? await getSite() : null
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
---
|
---
|
||||||
|
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
|
||||||
|
|
||||||
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
||||||
|
|
||||||
|
import ContentImage from '@/components/media/ContentImage.astro'
|
||||||
|
|
||||||
|
import Figure from '../media/Figure.astro'
|
||||||
|
|
||||||
type Props = { node: RenderableTreeNode }
|
type Props = { node: RenderableTreeNode }
|
||||||
|
|
||||||
|
const COMPONENTS: Record<string, AstroComponentFactory> = {
|
||||||
|
Figure,
|
||||||
|
Image: ContentImage,
|
||||||
|
}
|
||||||
const { node } = Astro.props
|
const { node } = Astro.props
|
||||||
const Element = Markdoc.Tag.isTag(node) ? node.name : null
|
const Element = Markdoc.Tag.isTag(node) ? node.name : null
|
||||||
---
|
---
|
||||||
|
|||||||
20
src/components/media/ContentImage.astro
Normal file
20
src/components/media/ContentImage.astro
Normal 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>
|
||||||
|
)}
|
||||||
25
src/components/media/Figure.astro
Normal file
25
src/components/media/Figure.astro
Normal 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>
|
||||||
@@ -7,14 +7,10 @@ summary:
|
|||||||
excerpt: >-
|
excerpt: >-
|
||||||
Where we explore the crucible – a materialistic alchemist Worldbuilding
|
Where we explore the crucible – a materialistic alchemist Worldbuilding
|
||||||
Framework
|
Framework
|
||||||
cover:
|
cover: {}
|
||||||
image: /src/assets/content/articles/the-crucible/summary/cover/image.png
|
|
||||||
alt: Just a test
|
|
||||||
caption: Test
|
|
||||||
credit: By ME
|
|
||||||
dates:
|
dates:
|
||||||
published: 2026-09-21T12:33:00.000Z
|
published: 2026-09-21T12:33:00.000Z
|
||||||
tags:
|
tags:
|
||||||
- the-crucible
|
- the-crucible
|
||||||
---
|
---
|
||||||

|
hmm, do you even sharp?
|
||||||
|
|||||||
31
src/keystatic/blocks/general/figure.ts
Normal file
31
src/keystatic/blocks/general/figure.ts
Normal 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',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
5
src/keystatic/blocks/general/index.ts
Normal file
5
src/keystatic/blocks/general/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { figure } from './figure'
|
||||||
|
|
||||||
|
export const generalBlocks = (collection: string) => ({
|
||||||
|
figure: figure(collection),
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { fields } from '@keystatic/core'
|
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) =>
|
export const content = (collection: string) =>
|
||||||
fields.markdoc({
|
fields.markdoc({
|
||||||
@@ -9,4 +10,5 @@ export const content = (collection: string) =>
|
|||||||
heading: [2, 3, 4],
|
heading: [2, 3, 4],
|
||||||
image: assets(collection),
|
image: assets(collection),
|
||||||
},
|
},
|
||||||
|
components: generalBlocks(collection),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fields } from '@keystatic/core'
|
import { fields } from '@keystatic/core'
|
||||||
|
|
||||||
import { assets } from '@/lib/content/assets'
|
import { assets } from '@/lib/images/storage'
|
||||||
|
|
||||||
export const cover = (collection: string) =>
|
export const cover = (collection: string) =>
|
||||||
fields.object(
|
fields.object(
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
import DevBanner from '@/components/dev/DevBanner.astro'
|
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||||
import Node from '@/components/markdoc/Node.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 { Rendered } from '@/lib/markdoc/types'
|
||||||
import type { Article } from '@/lib/site/types'
|
import type { Article } from '@/lib/site/types'
|
||||||
|
|
||||||
import BaseLayout from './BaseLayout.astro'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
article: Article
|
article: Article
|
||||||
rendered: Rendered
|
rendered: Rendered
|
||||||
@@ -23,6 +23,15 @@ const { article, rendered } = Astro.props
|
|||||||
/>
|
/>
|
||||||
<article class="article">
|
<article class="article">
|
||||||
<h1>{article.title}</h1>
|
<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">
|
<div class="prose">
|
||||||
<Node node={rendered.tree} />
|
<Node node={rendered.tree} />
|
||||||
</div>
|
</div>
|
||||||
16
src/lib/constants/figure.ts
Normal file
16
src/lib/constants/figure.ts
Normal 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']
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export const ASSET_ROOT = 'src/assets/content'
|
|
||||||
|
|
||||||
export const assets = (collection: string) => ({
|
|
||||||
directory: `${ASSET_ROOT}/${collection}`,
|
|
||||||
publicPath: `/${ASSET_ROOT}/${collection}/`,
|
|
||||||
})
|
|
||||||
8
src/lib/images/resolve.ts
Normal file
8
src/lib/images/resolve.ts
Normal 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
15
src/lib/images/storage.ts
Normal 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,
|
||||||
|
})
|
||||||
@@ -2,6 +2,7 @@ import Markdoc from '@markdoc/markdoc'
|
|||||||
|
|
||||||
import { createDiagnostics } from '../diagnostics'
|
import { createDiagnostics } from '../diagnostics'
|
||||||
import { createNodes } from './nodes'
|
import { createNodes } from './nodes'
|
||||||
|
import { tags } from './tags'
|
||||||
import type { NodeState, Renderable, Rendered } from './types'
|
import type { NodeState, Renderable, Rendered } from './types'
|
||||||
|
|
||||||
const LEVEL = {
|
const LEVEL = {
|
||||||
@@ -18,6 +19,7 @@ export async function render({ collection, id, content }: Renderable): Promise<R
|
|||||||
}
|
}
|
||||||
const config = {
|
const config = {
|
||||||
nodes: createNodes(state),
|
nodes: createNodes(state),
|
||||||
|
tags,
|
||||||
}
|
}
|
||||||
const problems = createDiagnostics()
|
const problems = createDiagnostics()
|
||||||
|
|
||||||
|
|||||||
@@ -45,5 +45,19 @@ export function createNodes(state: NodeState): Record<string, Schema> {
|
|||||||
return new Tag('div', { class: 'table-scroll', tabindex: 0 }, [table])
|
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
12
src/lib/markdoc/tags.ts
Normal 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
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
import type { GetStaticPaths } from 'astro'
|
import type { GetStaticPaths } from 'astro'
|
||||||
|
|
||||||
import ArticleLayout from '@/layouts/ArticleLayout.astro'
|
import ArticleLayout from '@/layouts/general/ArticleLayout.astro'
|
||||||
import PageLayout from '@/layouts/PageLayout.astro'
|
import PageLayout from '@/layouts/general/PageLayout.astro'
|
||||||
import { assertNoErrors } from '@/lib/diagnostics/report'
|
import { assertNoErrors } from '@/lib/diagnostics/report'
|
||||||
import { render } from '@/lib/markdoc'
|
import { render } from '@/lib/markdoc'
|
||||||
import { getSite } from '@/lib/site'
|
import { getSite } from '@/lib/site'
|
||||||
|
|||||||
Reference in New Issue
Block a user