Added layouts + routes
This commit is contained in:
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
|
||||
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