Added images
This commit is contained in:
@@ -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
7
pnpm-lock.yaml
generated
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
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: >-
|
||||
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
|
||||
---
|
||||

|
||||
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 { 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),
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
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 { 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()
|
||||
|
||||
|
||||
@@ -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
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 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'
|
||||
|
||||
Reference in New Issue
Block a user