Blog #1
@@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^11.0.0",
|
||||
"@astrojs/react": "^6.0.0",
|
||||
"@keystar/ui": "^0.10.0",
|
||||
"@keystatic/astro": "^6.0.0",
|
||||
"@keystatic/core": "^0.6.7",
|
||||
"@markdoc/markdoc": "^0.4.0",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ importers:
|
||||
'@astrojs/react':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(esbuild@0.28.2)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(sugarss@5.0.1(postcss@8.5.28))(yaml@2.9.1)
|
||||
'@keystar/ui':
|
||||
specifier: ^0.10.0
|
||||
version: 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)
|
||||
'@keystatic/astro':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@keystatic/core@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))(astro@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))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
|
||||
@@ -6,20 +6,27 @@ import Markdoc, { type RenderableTreeNode } from '@markdoc/markdoc'
|
||||
import ContentImage from '@/components/media/ContentImage.astro'
|
||||
|
||||
import Figure from '../media/Figure.astro'
|
||||
import Callout from './blocks/general/Callout.astro'
|
||||
import Quote from './blocks/general/Quote.astro'
|
||||
import TableWrapper from './blocks/general/TableWrapper.astro'
|
||||
|
||||
type Props = { node: RenderableTreeNode }
|
||||
type Props = { node: RenderableTreeNode | RenderableTreeNode[] }
|
||||
|
||||
const COMPONENTS: Record<string, AstroComponentFactory> = {
|
||||
Callout,
|
||||
Figure,
|
||||
TableWrapper,
|
||||
Quote,
|
||||
Image: ContentImage,
|
||||
}
|
||||
const { node } = Astro.props
|
||||
const Element = Markdoc.Tag.isTag(node) ? node.name : null
|
||||
const isTag = Markdoc.Tag.isTag(node)
|
||||
const Element = isTag ? (COMPONENTS[node.name] ?? node.name) : null
|
||||
---
|
||||
|
||||
{Array.isArray(node) ? (
|
||||
node.map((child) => <Astro.self node={child} />)
|
||||
) : Markdoc.Tag.isTag(node) && Element ? (
|
||||
) : isTag && Element ? (
|
||||
<Element {...node.attributes}>
|
||||
{node.children.map((child) => (
|
||||
<Astro.self node={child} />
|
||||
|
||||
17
src/components/markdoc/blocks/general/Callout.astro
Normal file
17
src/components/markdoc/blocks/general/Callout.astro
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import type { CalloutType } from '@/lib/constants/blocks/notes'
|
||||
|
||||
type Props = {
|
||||
type?: CalloutType
|
||||
title?: string
|
||||
}
|
||||
|
||||
const { type = 'default', title } = Astro.props
|
||||
---
|
||||
|
||||
<aside class:list={['callout', `callout--${type}`]}>
|
||||
{title && <p class="callout__title">{title}</p>}
|
||||
<div class="callout__body">
|
||||
<slot />
|
||||
</div>
|
||||
</aside>
|
||||
23
src/components/markdoc/blocks/general/FootnoteList.astro
Normal file
23
src/components/markdoc/blocks/general/FootnoteList.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import Node from '@/components/markdoc/Node.astro'
|
||||
import type { Footnote } from '@/lib/markdoc/types'
|
||||
|
||||
type Props = { footnotes: Footnote[] }
|
||||
const { footnotes } = Astro.props
|
||||
---
|
||||
|
||||
{footnotes.length > 0 && (
|
||||
<section class="footnotes">
|
||||
<h2 class="footnotes__title">Notes</h2>
|
||||
<ol>
|
||||
{footnotes.map((footnote) => (
|
||||
<li id={`fn-${footnote.n}`}>
|
||||
<Node node={footnote.children} />{' '}
|
||||
<a href={`#fnref-${footnote.n}`} class="footnotes__back" aria-label="Back to text">
|
||||
↩
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
22
src/components/markdoc/blocks/general/Quote.astro
Normal file
22
src/components/markdoc/blocks/general/Quote.astro
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
type Props = {
|
||||
cite?: string
|
||||
source?: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
const { cite, source, href } = Astro.props
|
||||
const label = source || href
|
||||
---
|
||||
|
||||
<figure class="quote">
|
||||
<blockquote cite={href}>
|
||||
<slot />
|
||||
</blockquote>
|
||||
{(cite || label) && (
|
||||
<figcaption class="quote__citation">
|
||||
{cite && <span class="quote__who">{cite}</span>}
|
||||
{label && <cite class="quote__source">{href ? <a href={href}>{label}</a> : label}</cite>}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
24
src/components/markdoc/blocks/general/SidenoteList.astro
Normal file
24
src/components/markdoc/blocks/general/SidenoteList.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import Node from '@/components/markdoc/Node.astro'
|
||||
import type { Sidenote } from '@/lib/markdoc/types'
|
||||
|
||||
type Props = { sidenotes: Sidenote[] }
|
||||
|
||||
const { sidenotes } = Astro.props
|
||||
---
|
||||
|
||||
{sidenotes.map((sidenote) => (
|
||||
<aside
|
||||
class:list={['sidenote', `sidenote--${sidenote.type}`]}
|
||||
id={`sn-${sidenote.n}`}
|
||||
style={`position-anchor: --sn-${sidenote.n}`}
|
||||
role="note"
|
||||
>
|
||||
<a class="sidenote__marker" href={`#snref-${sidenote.n}`} aria-label="Back to text">
|
||||
{sidenote.marker}
|
||||
</a>
|
||||
<div class="sidenote__note">
|
||||
<Node node={sidenote.children} />
|
||||
</div>
|
||||
</aside>
|
||||
))}
|
||||
15
src/components/markdoc/blocks/general/TableWrapper.astro
Normal file
15
src/components/markdoc/blocks/general/TableWrapper.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import type { TableVariant } from '@/lib/constants/blocks/table'
|
||||
|
||||
type Props = {
|
||||
variant?: TableVariant
|
||||
caption?: string
|
||||
}
|
||||
|
||||
const { variant = 'default', caption } = Astro.props
|
||||
---
|
||||
|
||||
<figure class:list={['table-wrapper', `table-wrapper--${variant}`]}>
|
||||
<slot />
|
||||
{caption && <figcaption>{caption}</figcaption>}
|
||||
</figure>
|
||||
19
src/keystatic/blocks/general/callout.ts
Normal file
19
src/keystatic/blocks/general/callout.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
import { NOTE_TYPES } from '@/lib/constants/blocks/notes'
|
||||
|
||||
export const callout = wrapper({
|
||||
label: 'Callout',
|
||||
description: 'An aside around any content',
|
||||
schema: {
|
||||
type: fields.select({
|
||||
label: 'Type',
|
||||
options: NOTE_TYPES,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
title: fields.text({
|
||||
label: 'Title',
|
||||
}),
|
||||
},
|
||||
})
|
||||
16
src/keystatic/blocks/general/footnote.ts
Normal file
16
src/keystatic/blocks/general/footnote.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { inline } from '@keystatic/core/content-components'
|
||||
|
||||
export const footnote = inline({
|
||||
label: 'Footnote',
|
||||
description: 'Numbered automatically; the note is collected at the end of the article',
|
||||
schema: {
|
||||
note: fields.text({
|
||||
label: 'Note',
|
||||
multiline: true,
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,17 @@
|
||||
import { callout } from './callout'
|
||||
import { figure } from './figure'
|
||||
import { footnote } from './footnote'
|
||||
import { quote } from './quote'
|
||||
import { sidenote } from './sidenote'
|
||||
import { tableWrapper } from './table-wrapper'
|
||||
import { xref } from './xref'
|
||||
|
||||
export const generalBlocks = (collection: string) => ({
|
||||
figure: figure(collection),
|
||||
callout,
|
||||
footnote,
|
||||
sidenote,
|
||||
tableWrapper,
|
||||
quote,
|
||||
xref: xref(),
|
||||
})
|
||||
|
||||
17
src/keystatic/blocks/general/quote.ts
Normal file
17
src/keystatic/blocks/general/quote.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
export const quote = wrapper({
|
||||
label: 'Quote',
|
||||
schema: {
|
||||
cite: fields.text({
|
||||
label: 'Cite',
|
||||
}),
|
||||
source: fields.text({
|
||||
label: 'Source',
|
||||
}),
|
||||
href: fields.url({
|
||||
label: 'Source Link',
|
||||
}),
|
||||
},
|
||||
})
|
||||
33
src/keystatic/blocks/general/sidenote.ts
Normal file
33
src/keystatic/blocks/general/sidenote.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { inline } from '@keystatic/core/content-components'
|
||||
|
||||
import { NOTE_TYPES } from '@/lib/constants/blocks/notes'
|
||||
|
||||
export const sidenote = inline({
|
||||
label: 'Sidenote',
|
||||
schema: {
|
||||
type: fields.select({
|
||||
label: 'Type',
|
||||
options: NOTE_TYPES,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
marker: fields.text({
|
||||
label: 'Marker',
|
||||
description: 'One or two characters, e.g. * † ‡ a.',
|
||||
defaultValue: '▾',
|
||||
validation: {
|
||||
isRequired: true,
|
||||
length: {
|
||||
max: 3,
|
||||
},
|
||||
},
|
||||
}),
|
||||
note: fields.text({
|
||||
label: 'Note',
|
||||
multiline: true,
|
||||
validation: {
|
||||
isRequired: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
19
src/keystatic/blocks/general/table-wrapper.ts
Normal file
19
src/keystatic/blocks/general/table-wrapper.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { wrapper } from '@keystatic/core/content-components'
|
||||
|
||||
import { TABLE_VARIANTS } from '@/lib/constants/blocks/table'
|
||||
|
||||
export const tableWrapper = wrapper({
|
||||
label: 'Table Wrapper',
|
||||
description: 'Styling and caption for one table',
|
||||
schema: {
|
||||
variant: fields.select({
|
||||
label: 'Variant',
|
||||
options: TABLE_VARIANTS,
|
||||
defaultValue: 'default',
|
||||
}),
|
||||
caption: fields.text({
|
||||
label: 'Caption',
|
||||
}),
|
||||
},
|
||||
})
|
||||
23
src/keystatic/blocks/general/xref.ts
Normal file
23
src/keystatic/blocks/general/xref.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
import { mark } from '@keystatic/core/content-components'
|
||||
|
||||
import { linkIcon } from '@keystar/ui/icon/icons/linkIcon'
|
||||
|
||||
import { ARTICLES } from '@/lib/constants/collections'
|
||||
|
||||
export const xref = (collection: string = ARTICLES) =>
|
||||
mark({
|
||||
label: 'Internal link',
|
||||
icon: linkIcon,
|
||||
tag: 'a',
|
||||
schema: {
|
||||
reference: fields.relationship({
|
||||
label: 'Reference',
|
||||
collection,
|
||||
validation: { isRequired: true },
|
||||
}),
|
||||
anchor: fields.text({
|
||||
label: 'Heading id',
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
---
|
||||
import DevBanner from '@/components/dev/DevBanner.astro'
|
||||
import FootnoteList from '@/components/markdoc/blocks/general/FootnoteList.astro'
|
||||
import SidenoteList from '@/components/markdoc/blocks/general/SidenoteList.astro'
|
||||
import Node from '@/components/markdoc/Node.astro'
|
||||
import Figure from '@/components/media/Figure.astro'
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
@@ -35,5 +37,7 @@ const { article, rendered } = Astro.props
|
||||
<div class="prose">
|
||||
<Node node={rendered.tree} />
|
||||
</div>
|
||||
<SidenoteList sidenotes={rendered.sidenotes} />
|
||||
<FootnoteList footnotes={rendered.footnotes} />
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
12
src/lib/constants/blocks/notes.ts
Normal file
12
src/lib/constants/blocks/notes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const NOTE_TYPES = [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Example',
|
||||
value: 'example',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type CalloutType = (typeof NOTE_TYPES)[number]['value']
|
||||
16
src/lib/constants/blocks/table.ts
Normal file
16
src/lib/constants/blocks/table.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const TABLE_VARIANTS = [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Random',
|
||||
value: 'random',
|
||||
},
|
||||
{
|
||||
label: 'Icon',
|
||||
value: 'icon',
|
||||
},
|
||||
]
|
||||
|
||||
export type TableVariant = (typeof TABLE_VARIANTS)[number]['value']
|
||||
@@ -2,8 +2,8 @@ import Markdoc from '@markdoc/markdoc'
|
||||
|
||||
import { createDiagnostics } from '../diagnostics'
|
||||
import { createNodes } from './nodes'
|
||||
import { tags } from './tags'
|
||||
import type { NodeState, Renderable, Rendered } from './types'
|
||||
import { createTags } from './tags'
|
||||
import type { NodeState, Renderable, Rendered, ResolveLink } from './types'
|
||||
|
||||
const LEVEL = {
|
||||
critical: 'error',
|
||||
@@ -11,25 +11,37 @@ const LEVEL = {
|
||||
warning: 'warning',
|
||||
} as const
|
||||
|
||||
export async function render({ collection, id, content }: Renderable): Promise<Rendered> {
|
||||
export async function render(
|
||||
{ collection, id, content }: Renderable,
|
||||
resolveLink: ResolveLink,
|
||||
): Promise<Rendered> {
|
||||
const { node } = await content()
|
||||
const state: NodeState = {
|
||||
headings: [],
|
||||
ids: new Set(),
|
||||
footnotes: [],
|
||||
sidenotes: [],
|
||||
markers: new Set(),
|
||||
problems: [],
|
||||
}
|
||||
const config = {
|
||||
nodes: createNodes(state),
|
||||
tags,
|
||||
tags: createTags(state, resolveLink),
|
||||
}
|
||||
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}`)
|
||||
if (level) problems[level](collection, id, `Text line ${lines[0] + 1}: ${error.message}`)
|
||||
}
|
||||
const tree = Markdoc.transform(node, config)
|
||||
for (const { level, message } of state.problems) problems[level](collection, id, message)
|
||||
|
||||
return {
|
||||
tree: Markdoc.transform(node, config),
|
||||
tree,
|
||||
headings: state.headings,
|
||||
footnotes: state.footnotes,
|
||||
sidenotes: state.sidenotes,
|
||||
diagnostics: problems.list,
|
||||
}
|
||||
}
|
||||
|
||||
17
src/lib/markdoc/notes.ts
Normal file
17
src/lib/markdoc/notes.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Markdoc, { type RenderableTreeNode, type Schema } from '@markdoc/markdoc'
|
||||
|
||||
const unwrap: Schema = { transform: (node, config) => node.transformChildren(config) }
|
||||
|
||||
const INLINE = {
|
||||
nodes: {
|
||||
...Markdoc.nodes,
|
||||
document: unwrap,
|
||||
paragraph: unwrap,
|
||||
},
|
||||
tags: {},
|
||||
}
|
||||
|
||||
export function inlineOf(text: string): RenderableTreeNode[] {
|
||||
const rendered = Markdoc.transform(Markdoc.parse(text), INLINE)
|
||||
return Array.isArray(rendered) ? rendered : [rendered]
|
||||
}
|
||||
@@ -1,12 +1,113 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import Markdoc, { type Node, type Schema } from '@markdoc/markdoc'
|
||||
|
||||
import { generalBlocks } from '@/keystatic/blocks/general'
|
||||
|
||||
export const tags = fields.markdoc.createMarkdocConfig({
|
||||
components: generalBlocks('articles'),
|
||||
render: {
|
||||
tags: {
|
||||
figure: 'Figure',
|
||||
import { ARTICLES } from '../constants/collections'
|
||||
import { inlineOf } from './notes'
|
||||
import type { NodeState, ResolveLink } from './types'
|
||||
|
||||
const { Tag } = Markdoc
|
||||
|
||||
const generated =
|
||||
fields.markdoc.createMarkdocConfig({
|
||||
components: generalBlocks(ARTICLES),
|
||||
render: {
|
||||
tags: {
|
||||
callout: 'Callout',
|
||||
figure: 'Figure',
|
||||
tableWrapper: 'TableWrapper',
|
||||
quote: 'Quote',
|
||||
},
|
||||
},
|
||||
},
|
||||
}).tags
|
||||
}).tags ?? {}
|
||||
|
||||
const text = (value: unknown) => String(value ?? '')
|
||||
|
||||
export function createTags(state: NodeState, resolveLink: ResolveLink): Record<string, Schema> {
|
||||
return {
|
||||
...generated,
|
||||
footnote: {
|
||||
...generated.footnote,
|
||||
transform(node) {
|
||||
const n = state.footnotes.length + 1
|
||||
state.footnotes.push({ n, children: inlineOf(text(node.attributes.note)) })
|
||||
return new Tag('sup', { class: 'footnote-ref' }, [
|
||||
new Tag('a', { href: `#fn-${n}`, id: `fnref-${n}` }, [String(n)]),
|
||||
])
|
||||
},
|
||||
},
|
||||
sidenote: {
|
||||
...generated.sidenote,
|
||||
transform(node, config) {
|
||||
const { marker, type } = node.transformAttributes(config) as Record<string, string>
|
||||
const noteType = type || 'default'
|
||||
if (state.markers.has(marker))
|
||||
state.problems.push({
|
||||
level: 'warning',
|
||||
message: `Two sidenotes use the marker "${marker}".`,
|
||||
})
|
||||
state.markers.add(marker)
|
||||
const n = state.sidenotes.length + 1
|
||||
state.sidenotes.push({
|
||||
n,
|
||||
marker,
|
||||
type: noteType,
|
||||
children: inlineOf(text(node.attributes.note)),
|
||||
})
|
||||
return new Tag(
|
||||
'a',
|
||||
{
|
||||
class: 'sidenote-ref',
|
||||
id: `snref-${n}`,
|
||||
href: `#sn-${n}`,
|
||||
style: `anchor-name: --sn-${n}`,
|
||||
},
|
||||
[marker],
|
||||
)
|
||||
},
|
||||
},
|
||||
xref: {
|
||||
...generated.xref,
|
||||
transform(node: Node, config) {
|
||||
const id = text(node.attributes.reference)
|
||||
const anchor = text(node.attributes.anchor)
|
||||
const children = node.transformChildren(config)
|
||||
const target = resolveLink(id)
|
||||
|
||||
if (!target) {
|
||||
state.problems.push({ level: 'error', message: `Link to "${id}": no such article.` })
|
||||
return children
|
||||
}
|
||||
if (!target.path) {
|
||||
state.problems.push({
|
||||
level: 'warning',
|
||||
message: `Link to "${id}": that article has no page yet, so the link is plain text.`,
|
||||
})
|
||||
return children
|
||||
}
|
||||
return new Tag(
|
||||
'a',
|
||||
{ href: anchor ? `${target.path}#${anchor}` : target.path },
|
||||
children,
|
||||
)
|
||||
},
|
||||
},
|
||||
tableWrapper: {
|
||||
...generated.tableWrapper,
|
||||
validate(node) {
|
||||
const tables = node.children.filter((child) => child.tag === 'table').length
|
||||
return tables === 1
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: 'table-wrapper-children',
|
||||
level: 'warning' as const,
|
||||
message: `A table wrapper should hold exactly one table, this one holds ${tables}.`,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Node, RenderableTreeNode } from '@markdoc/markdoc'
|
||||
|
||||
import type { Diagnostic } from '../diagnostics/types'
|
||||
import type { Diagnostic, Level } from '../diagnostics/types'
|
||||
|
||||
export type Heading = {
|
||||
id: string
|
||||
@@ -8,14 +8,41 @@ export type Heading = {
|
||||
text: string
|
||||
}
|
||||
|
||||
export type Footnote = {
|
||||
n: number
|
||||
children: RenderableTreeNode[]
|
||||
}
|
||||
|
||||
export type Sidenote = {
|
||||
n: number
|
||||
marker: string
|
||||
type: string
|
||||
children: RenderableTreeNode[]
|
||||
}
|
||||
|
||||
export type Problem = {
|
||||
level: Level
|
||||
message: string
|
||||
}
|
||||
|
||||
export type NodeState = {
|
||||
headings: Heading[]
|
||||
ids: Set<string>
|
||||
footnotes: Footnote[]
|
||||
sidenotes: Sidenote[]
|
||||
markers: Set<string>
|
||||
problems: Problem[]
|
||||
}
|
||||
|
||||
export type LinkTarget = { path: string | null } | undefined
|
||||
|
||||
export type ResolveLink = (id: string) => LinkTarget
|
||||
|
||||
export type Rendered = {
|
||||
tree: RenderableTreeNode
|
||||
headings: Heading[]
|
||||
footnotes: Footnote[]
|
||||
sidenotes: Sidenote[]
|
||||
diagnostics: Diagnostic[]
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export function buildSite(content: Content, visibility: Visibility): Site {
|
||||
pages,
|
||||
tags,
|
||||
byPath,
|
||||
known: new Set(content.articles.keys()),
|
||||
roots,
|
||||
diagnostics: [...content.diagnostics, ...problems.list],
|
||||
}
|
||||
|
||||
10
src/lib/site/links.ts
Normal file
10
src/lib/site/links.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { LinkTarget } from '../markdoc/types'
|
||||
import type { Site } from './types'
|
||||
|
||||
export const linkTo =
|
||||
(site: Site) =>
|
||||
(id: string): LinkTarget => {
|
||||
const article = site.articles.get(id)
|
||||
if (article) return { path: article.path }
|
||||
return site.known.has(id) ? { path: null } : undefined
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export type Site = {
|
||||
pages: Map<string, Page>
|
||||
tags: Map<string, Tag>
|
||||
byPath: Map<string, Routed>
|
||||
known: Set<string>
|
||||
roots: Article[]
|
||||
diagnostics: Diagnostic[]
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import PageLayout from '@/layouts/general/PageLayout.astro'
|
||||
import { assertNoErrors } from '@/lib/diagnostics/report'
|
||||
import { render } from '@/lib/markdoc'
|
||||
import { getSite } from '@/lib/site'
|
||||
import { linkTo } from '@/lib/site/links'
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
const site = await getSite()
|
||||
if (import.meta.env.PROD) assertNoErrors
|
||||
if (import.meta.env.PROD) assertNoErrors(site.diagnostics)
|
||||
return [...site.byPath.keys()].map((path) => ({ params: { path: path.slice(1) } }))
|
||||
}) satisfies GetStaticPaths
|
||||
|
||||
@@ -17,8 +18,8 @@ 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(site.diagnostics)
|
||||
const rendered = await render(node, linkTo(site))
|
||||
if (import.meta.env.PROD) assertNoErrors(rendered.diagnostics)
|
||||
---
|
||||
|
||||
{node.collection === 'articles' ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import type { GetStaticPaths } from 'astro'
|
||||
|
||||
import TagLayout from '@/layouts/TagLayout.astro'
|
||||
import TagLayout from '@/layouts/general/TagLayout.astro'
|
||||
import { getSite } from '@/lib/site'
|
||||
|
||||
export const getStaticPaths = (async () => {
|
||||
|
||||
Reference in New Issue
Block a user