Added layouts + routes
This commit is contained in:
@@ -5,6 +5,8 @@ import keystatic from '@keystatic/astro'
|
|||||||
import node from '@astrojs/node'
|
import node from '@astrojs/node'
|
||||||
import react from '@astrojs/react'
|
import react from '@astrojs/react'
|
||||||
|
|
||||||
|
import { contentReload } from './src/integrations/content-reload'
|
||||||
|
|
||||||
const devOnly = (integration) => ({
|
const devOnly = (integration) => ({
|
||||||
name: `${integration.name}:dev-only`,
|
name: `${integration.name}:dev-only`,
|
||||||
hooks: {
|
hooks: {
|
||||||
@@ -17,7 +19,7 @@ const devOnly = (integration) => ({
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://dave-dmg.de',
|
site: 'https://dave-dmg.de',
|
||||||
integrations: [react(), devOnly(keystatic())],
|
integrations: [react(), devOnly(keystatic()), contentReload()],
|
||||||
|
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: 'standalone',
|
mode: 'standalone',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"@astrojs/react": "^6.0.0",
|
"@astrojs/react": "^6.0.0",
|
||||||
"@keystatic/astro": "^6.0.0",
|
"@keystatic/astro": "^6.0.0",
|
||||||
"@keystatic/core": "^0.6.7",
|
"@keystatic/core": "^0.6.7",
|
||||||
"@markdoc/markdoc": "^0.5.10",
|
"@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"
|
||||||
|
|||||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -24,8 +24,8 @@ importers:
|
|||||||
specifier: ^0.6.7
|
specifier: ^0.6.7
|
||||||
version: 0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0)
|
version: 0.6.9(@keystar/ui@0.10.0(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0))(react-aria@3.50.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react-stately@3.48.0(react@19.3.0))(react@19.3.0)
|
||||||
'@markdoc/markdoc':
|
'@markdoc/markdoc':
|
||||||
specifier: ^0.5.10
|
specifier: ^0.4.0
|
||||||
version: 0.5.10(@types/react@19.3.0)(react@19.3.0)
|
version: 0.4.0(@types/react@19.3.0)(react@19.3.0)
|
||||||
astro:
|
astro:
|
||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
version: 7.3.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(idb-keyval@6.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||||
@@ -995,18 +995,6 @@ packages:
|
|||||||
react:
|
react:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@markdoc/markdoc@0.5.10':
|
|
||||||
resolution: {integrity: sha512-5HwkG2cLG/ivffeJlxGmraJ9erYfsaYz9rjG1KCf46PLy+yhrBQWw26h69GewNUHbyKNW6FrXuDYgbl2kvIYdA==}
|
|
||||||
engines: {node: '>=14.7.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/react': '*'
|
|
||||||
react: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/react':
|
|
||||||
optional: true
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.2.4':
|
'@napi-rs/wasm-runtime@1.2.4':
|
||||||
resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
|
resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
|
||||||
@@ -4399,13 +4387,6 @@ snapshots:
|
|||||||
'@types/react': 19.3.0
|
'@types/react': 19.3.0
|
||||||
react: 19.3.0
|
react: 19.3.0
|
||||||
|
|
||||||
'@markdoc/markdoc@0.5.10(@types/react@19.3.0)(react@19.3.0)':
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/linkify-it': 3.0.5
|
|
||||||
'@types/markdown-it': 12.2.3
|
|
||||||
'@types/react': 19.3.0
|
|
||||||
react: 19.3.0
|
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
|
'@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.11.1
|
'@emnapi/core': 1.11.1
|
||||||
|
|||||||
47
src/components/dev/DevBanner.astro
Normal file
47
src/components/dev/DevBanner.astro
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
import type { Status } from '@/lib/constants/status'
|
||||||
|
import { forEntry, formatDiagnostics } from '@/lib/diagnostics/report'
|
||||||
|
import type { Diagnostic } from '@/lib/diagnostics/types'
|
||||||
|
import { getSite } from '@/lib/site'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
collection: string
|
||||||
|
id: string
|
||||||
|
status?: Status
|
||||||
|
diagnostics?: Diagnostic[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { collection, id, status, diagnostics = [] } = Astro.props
|
||||||
|
|
||||||
|
const NOTICE: Partial<Record<Status, string>> = {
|
||||||
|
draft: 'Draft – not build in production',
|
||||||
|
unlisted: 'Unlisted – 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 here = site ? [...forEntry(site.diagnostics, collection, id), ...diagnostics] : []
|
||||||
|
const elsewhere = site ? site.diagnostics.filter((d) => !here.includes(d)) : []
|
||||||
|
const notice = status ? NOTICE[status] : undefined
|
||||||
|
---
|
||||||
|
|
||||||
|
{site && (here.length || elsewhere.length || notice) && (
|
||||||
|
<aside class="dev-banner">
|
||||||
|
{notice && <p class="notice">{notice}</p>}
|
||||||
|
{here.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{here.map((d) => (
|
||||||
|
<li class={d.level}>
|
||||||
|
{d.level === 'error' ? '✖' : '▲'} {d.message}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{elsewhere.length > 0 && (
|
||||||
|
<details>
|
||||||
|
<summary>{elsewhere.length} problem(s) elsewhere</summary>
|
||||||
|
<pre>{formatDiagnostics(elsewhere)}</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
)}
|
||||||
20
src/components/markdoc/Node.astro
Normal file
20
src/components/markdoc/Node.astro
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
||||||
|
|
||||||
|
type Props = { node: RenderableTreeNode }
|
||||||
|
|
||||||
|
const { node } = Astro.props
|
||||||
|
const Element = Markdoc.Tag.isTag(node) ? node.name : null
|
||||||
|
---
|
||||||
|
|
||||||
|
{Array.isArray(node) ? (
|
||||||
|
node.map((child) => <Astro.self node={child} />)
|
||||||
|
) : Markdoc.Tag.isTag(node) && Element ? (
|
||||||
|
<Element {...node.attributes}>
|
||||||
|
{node.children.map((child) => (
|
||||||
|
<Astro.self node={child} />
|
||||||
|
))}
|
||||||
|
</Element>
|
||||||
|
) : typeof node === 'string' || typeof node === 'number' ? (
|
||||||
|
node
|
||||||
|
) : null}
|
||||||
18
src/integrations/content-reload.ts
Normal file
18
src/integrations/content-reload.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { AstroIntegration } from 'astro'
|
||||||
|
|
||||||
|
export const contentReload = (): AstroIntegration => ({
|
||||||
|
name: 'content-reload',
|
||||||
|
hooks: {
|
||||||
|
'astro:server:setup': ({ server }) => {
|
||||||
|
server.watcher.on('all', (_event, file) => {
|
||||||
|
if (!file.includes('/src/content')) return
|
||||||
|
for (const name of ['ssr', 'prerender'])
|
||||||
|
server.environments[name]?.hot.send('astro:content-changed', {})
|
||||||
|
server.environments.client.hot.send({
|
||||||
|
type: 'full-reload',
|
||||||
|
path: '*',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
30
src/layouts/ArticleLayout.astro
Normal file
30
src/layouts/ArticleLayout.astro
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||||
|
import Node from '@/components/markdoc/Node.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
|
||||||
|
}
|
||||||
|
|
||||||
|
const { article, rendered } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={article.title} description={article.summary.excerpt || undefined}>
|
||||||
|
<DevBanner
|
||||||
|
collection={article.collection}
|
||||||
|
id={article.id}
|
||||||
|
status={article.status}
|
||||||
|
diagnostics={rendered.diagnostics}
|
||||||
|
/>
|
||||||
|
<article class="article">
|
||||||
|
<h1>{article.title}</h1>
|
||||||
|
<div class="prose">
|
||||||
|
<Node node={rendered.tree} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</BaseLayout>
|
||||||
29
src/layouts/PageLayout.astro
Normal file
29
src/layouts/PageLayout.astro
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||||
|
import Node from '@/components/markdoc/Node.astro'
|
||||||
|
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||||
|
import type { Rendered } from '@/lib/markdoc/types'
|
||||||
|
import type { Page } from '@/lib/site/types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: Page
|
||||||
|
rendered: Rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page, rendered } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={page.title} description={page.summary.excerpt || undefined}>
|
||||||
|
<DevBanner
|
||||||
|
collection={page.collection}
|
||||||
|
id={page.id}
|
||||||
|
status={page.status}
|
||||||
|
diagnostics={rendered.diagnostics}
|
||||||
|
/>
|
||||||
|
<article class="page">
|
||||||
|
<h1>{page.title}</h1>
|
||||||
|
<div class="prose">
|
||||||
|
<Node node={rendered.tree} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</BaseLayout>
|
||||||
25
src/layouts/TagLayout.astro
Normal file
25
src/layouts/TagLayout.astro
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||||
|
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||||
|
import type { Tag } from '@/lib/site/types'
|
||||||
|
|
||||||
|
type Props = { tag: Tag }
|
||||||
|
|
||||||
|
const { tag } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={`Tagged: ${tag.title}`} description={tag.description || undefined}>
|
||||||
|
<DevBanner collection={tag.collection} id={tag.id} />
|
||||||
|
<section class="tag">
|
||||||
|
<h1>{tag.title}</h1>
|
||||||
|
{tag.description && <p>{tag.description}</p>}
|
||||||
|
<ul>
|
||||||
|
{tag.articles.map((article) => (
|
||||||
|
<li>
|
||||||
|
<a href={article.path}>{article.title}</a>
|
||||||
|
<p>{article.summary.excerpt}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
@@ -21,3 +21,6 @@ export function assertNoErrors(list: Diagnostic[]): void {
|
|||||||
const errors = list.filter((d) => d.level === 'error').length
|
const errors = list.filter((d) => d.level === 'error').length
|
||||||
if (errors) throw new Error(`Content has ${errors} error(s):\n${formatDiagnostics(list)}`)
|
if (errors) throw new Error(`Content has ${errors} error(s):\n${formatDiagnostics(list)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const forEntry = (list: Diagnostic[], collection: string, id: string) =>
|
||||||
|
list.filter((d) => d.collection === collection && d.id === id)
|
||||||
|
|||||||
33
src/lib/markdoc/index.ts
Normal file
33
src/lib/markdoc/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Markdoc from '@markdoc/markdoc'
|
||||||
|
|
||||||
|
import { createDiagnostics } from '../diagnostics'
|
||||||
|
import { createNodes } from './nodes'
|
||||||
|
import type { NodeState, Renderable, Rendered } from './types'
|
||||||
|
|
||||||
|
const LEVEL = {
|
||||||
|
critical: 'error',
|
||||||
|
error: 'error',
|
||||||
|
warning: 'warning',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export async function render({ collection, id, content }: Renderable): Promise<Rendered> {
|
||||||
|
const { node } = await content()
|
||||||
|
const state: NodeState = {
|
||||||
|
headings: [],
|
||||||
|
ids: new Set(),
|
||||||
|
}
|
||||||
|
const config = {
|
||||||
|
nodes: createNodes(state),
|
||||||
|
}
|
||||||
|
const problems = createDiagnostics()
|
||||||
|
|
||||||
|
for (const { lines, error } of Markdoc.validate(node, config)) {
|
||||||
|
const level = LEVEL[error.level as keyof typeof LEVEL]
|
||||||
|
if (level) problems[level](collection, id, `Line ${lines[0] + 1} : ${error.message}`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tree: Markdoc.transform(node, config),
|
||||||
|
headings: state.headings,
|
||||||
|
diagnostics: problems.list,
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/lib/markdoc/nodes.ts
Normal file
49
src/lib/markdoc/nodes.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import Markdoc, { type Node, type Schema } from '@markdoc/markdoc'
|
||||||
|
|
||||||
|
import { slugify } from '../content/slug'
|
||||||
|
import type { NodeState } from './types'
|
||||||
|
|
||||||
|
const { Tag } = Markdoc
|
||||||
|
|
||||||
|
const textOf = (node: Node): string =>
|
||||||
|
node.type === 'text' || node.type === 'code'
|
||||||
|
? String(node.attributes.content ?? '')
|
||||||
|
: node.children.map(textOf).join('')
|
||||||
|
|
||||||
|
function uniqueId(text: string, ids: Set<string>): string {
|
||||||
|
const base = slugify(text) || 'section'
|
||||||
|
let id = base
|
||||||
|
for (let n = 2; ids.has(id); n++) id = `${base}-${n}`
|
||||||
|
ids.add(id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createNodes(state: NodeState): Record<string, Schema> {
|
||||||
|
return {
|
||||||
|
document: {
|
||||||
|
...Markdoc.nodes.document,
|
||||||
|
transform: (node, config) => node.transformChildren(config),
|
||||||
|
},
|
||||||
|
heading: {
|
||||||
|
...Markdoc.nodes.heading,
|
||||||
|
transform(node, config) {
|
||||||
|
const level = Number(node.attributes.level)
|
||||||
|
const text = textOf(node)
|
||||||
|
const id = uniqueId(text, state.ids)
|
||||||
|
state.headings.push({ id, level, text })
|
||||||
|
return new Tag(`h${level}`, { id }, node.transformChildren(config))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
...Markdoc.nodes.table,
|
||||||
|
transform(node, config) {
|
||||||
|
const table = new Tag(
|
||||||
|
'table',
|
||||||
|
node.transformAttributes(config),
|
||||||
|
node.transformChildren(config),
|
||||||
|
)
|
||||||
|
return new Tag('div', { class: 'table-scroll', tabindex: 0 }, [table])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/lib/markdoc/types.ts
Normal file
26
src/lib/markdoc/types.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Node, RenderableTreeNode } from '@markdoc/markdoc'
|
||||||
|
|
||||||
|
import type { Diagnostic } from '../diagnostics/types'
|
||||||
|
|
||||||
|
export type Heading = {
|
||||||
|
id: string
|
||||||
|
level: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeState = {
|
||||||
|
headings: Heading[]
|
||||||
|
ids: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Rendered = {
|
||||||
|
tree: RenderableTreeNode
|
||||||
|
headings: Heading[]
|
||||||
|
diagnostics: Diagnostic[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Renderable = {
|
||||||
|
collection: string
|
||||||
|
id: string
|
||||||
|
content: () => Promise<{ node: Node }>
|
||||||
|
}
|
||||||
28
src/pages/[...path].astro
Normal file
28
src/pages/[...path].astro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import type { GetStaticPaths } from 'astro'
|
||||||
|
|
||||||
|
import ArticleLayout from '@/layouts/ArticleLayout.astro'
|
||||||
|
import PageLayout from '@/layouts/PageLayout.astro'
|
||||||
|
import { assertNoErrors } from '@/lib/diagnostics/report'
|
||||||
|
import { render } from '@/lib/markdoc'
|
||||||
|
import { getSite } from '@/lib/site'
|
||||||
|
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const site = await getSite()
|
||||||
|
if (import.meta.env.PROD) assertNoErrors
|
||||||
|
return [...site.byPath.keys()].map((path) => ({ params: { path: path.slice(1) } }))
|
||||||
|
}) satisfies GetStaticPaths
|
||||||
|
|
||||||
|
const site = await getSite()
|
||||||
|
const node = site.byPath.get(`/${Astro.params.path}`)
|
||||||
|
if (!node) return new Response(null, { status: 404 })
|
||||||
|
|
||||||
|
const rendered = await render(node)
|
||||||
|
if (import.meta.env.PROD) assertNoErrors(rendered.diagnostics)
|
||||||
|
---
|
||||||
|
|
||||||
|
{node.collection === 'articles' ? (
|
||||||
|
<ArticleLayout article={node} rendered={rendered} />
|
||||||
|
) : (
|
||||||
|
<PageLayout page={node} rendered={rendered} />
|
||||||
|
)}
|
||||||
17
src/pages/tags/[tag].astro
Normal file
17
src/pages/tags/[tag].astro
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import type { GetStaticPaths } from 'astro'
|
||||||
|
|
||||||
|
import TagLayout from '@/layouts/TagLayout.astro'
|
||||||
|
import { getSite } from '@/lib/site'
|
||||||
|
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const site = await getSite()
|
||||||
|
return [...site.tags.keys()].map((tag) => ({ params: { tag } }))
|
||||||
|
}) satisfies GetStaticPaths
|
||||||
|
|
||||||
|
const site = await getSite()
|
||||||
|
const tag = site.tags.get(Astro.params.tag)
|
||||||
|
if (!tag) return new Response(null, { status: 404 })
|
||||||
|
---
|
||||||
|
|
||||||
|
<TagLayout tag={tag} />
|
||||||
Reference in New Issue
Block a user