1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-28 16:48:00 +02:00

add table of contents

This commit is contained in:
Matthias Kretschmann 2023-09-04 11:22:16 +01:00
parent 006447355b
commit 735a48c1c1
Signed by: m
GPG Key ID: 606EEEF3C479A91F
13 changed files with 1174 additions and 861 deletions

View File

@ -1,18 +1,19 @@
import { defineConfig } from 'astro/config'
import { remarkLeadParagraph } from './src/lib/remark-lead-paragraph.mjs'
import remarkLeadParagraph from './src/lib/remark-lead-paragraph.mjs'
import remarkToc from './src/lib/remark-toc.mjs'
import react from '@astrojs/react'
// https://astro.build/config
export default defineConfig({
site: 'https://kremalicious.com',
markdown: {
remarkPlugins: [remarkLeadParagraph, remarkToc],
shikiConfig: {
// https://github.com/shikijs/shiki/blob/main/docs/themes.md
theme: 'github-dark-dimmed',
langs: [],
wrap: true
},
remarkPlugins: [remarkLeadParagraph]
}
},
integrations: [react()]
})

1897
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -64,9 +64,12 @@
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-testing-library": "^6.0.1",
"hast-util-to-html": "^9.0.0",
"jest": "^29.6.4",
"jest-environment-jsdom": "^29.6.4",
"markdownlint-cli": "^0.35.0",
"mdast-util-to-hast": "^13.0.2",
"mdast-util-toc": "^7.0.0",
"node-iptc": "^1.0.5",
"npm-run-all": "^4.1.5",
"ora": "^7.0.1",

View File

@ -0,0 +1,17 @@
---
import styles from './toc.module.css'
type Props = {
tableOfContents: string
}
const { tableOfContents } = Astro.props
---
<style></style>
<nav
aria-label="Table of Contents"
class={styles.toc}
set:html={tableOfContents}
/>

View File

@ -5,7 +5,8 @@
margin-bottom: calc(var(--spacer) * var(--line-height));
}
.toc ul {
.toc ul,
.toc p {
margin: 0;
}

View File

@ -6,13 +6,15 @@ import Actions from './Actions.astro'
import styles from './index.module.css'
import Meta from './Meta.astro'
import Picture from '@components/core/Picture.astro'
import Toc from '@components/Toc/Toc.astro'
type Props = CollectionEntry<'articles' | 'links' | 'photos'> & {
lead?: string // comes in through remark plugin as html
tableOfContents?: string // comes in through remark plugin as html
}
const { data, collection, lead } = Astro.props
const { title, date, updated, image, linkurl } = data
const { data, collection, lead, tableOfContents } = Astro.props
const { title, date, updated, image, linkurl, toc } = data
---
<LayoutBase title={title}>
@ -39,6 +41,8 @@ const { title, date, updated, image, linkurl } = data
)
}
{toc && tableOfContents && <Toc tableOfContents={tableOfContents} />}
<slot />
<Meta post={Astro.props} />

View File

@ -22,7 +22,8 @@ const schemaShared = {
redirect_from: z.array(z.string()).optional(),
author: z.string().optional(),
featured: z.boolean().optional(),
style: z.string().optional()
style: z.string().optional(),
toc: z.boolean().optional()
}
export const schemaArticles = (image: ImageFunction) =>
@ -35,7 +36,6 @@ export const schemaArticles = (image: ImageFunction) =>
// })
.optional(),
download: z.string().optional(),
toc: z.boolean().optional(),
changelog: z.string().optional()
})
.strict()

View File

@ -1,18 +0,0 @@
import React, { ReactElement } from 'react'
import styles from './Toc.module.css'
const PostToc = ({
tableOfContents
}: {
tableOfContents: string
}): ReactElement => {
return (
<nav
aria-label="Table of Contents"
className={styles.toc}
dangerouslySetInnerHTML={{ __html: tableOfContents }}
/>
)
}
export default PostToc

View File

@ -1,4 +1,4 @@
export function remarkLeadParagraph() {
export default function remarkLeadParagraph() {
return function (tree, file) {
// run only on articles
if (!file.history[0].includes('articles')) return

28
src/lib/remark-toc.mjs Normal file
View File

@ -0,0 +1,28 @@
//
// Extract headings from markdown and add them as HTML to the frontmatter
// Similiar to https://github.com/remarkjs/remark-toc
//
import { toc } from 'mdast-util-toc'
import { toHast } from 'mdast-util-to-hast'
import { toHtml } from 'hast-util-to-html'
export default function remarkToc() {
return (tree, file) => {
const result = toc(tree, { maxDepth: 3 })
if (
result.endIndex === null ||
result.index === null ||
result.index === -1 ||
!result.map
) {
return
}
const hast = toHast(result.map)
const html = toHtml(hast)
// Add toc to frontmatter
file.data.astro.frontmatter.tableOfContents = html.toString()
}
}

View File

@ -24,6 +24,10 @@ const { entry } = Astro.props
const { Content, remarkPluginFrontmatter } = await entry.render()
---
<LayoutPost {...entry} lead={remarkPluginFrontmatter.lead}>
<LayoutPost
{...entry}
lead={remarkPluginFrontmatter.lead}
tableOfContents={remarkPluginFrontmatter.tableOfContents}
>
<Content />
</LayoutPost>

View File

@ -1,14 +1,20 @@
import { getCollection } from 'astro:content'
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts'
import { loadAndFormatCollection } from '@lib/astro'
import config from '@config/blog.config.mjs'
const { siteTitle, siteDescription } = config
export async function get(context) {
const posts = await getCollection('blog')
const articles = await loadAndFormatCollection('articles')
const links = await loadAndFormatCollection('links')
const photos = await loadAndFormatCollection('photos')
const allPosts = [...articles, ...links, ...photos]
return {
body: JSON.stringify({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
title: siteTitle,
description: siteDescription,
site: context.site,
items: posts.map((post) => ({
items: allPosts.map((post) => ({
...post.data,
link: `/blog/${post.slug}/`
}))

View File

@ -1,16 +1,24 @@
import rss from '@astrojs/rss'
import { getCollection } from 'astro:content'
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts'
import config from '@config/blog.config.mjs'
import { loadAndFormatCollection } from '@lib/astro'
const { siteTitle, siteDescription } = config
export async function get(context) {
const posts = await getCollection('blog')
const articles = await loadAndFormatCollection('articles')
const links = await loadAndFormatCollection('links')
const photos = await loadAndFormatCollection('photos')
const allPosts = [...articles, ...links, ...photos]
return rss({
title: SITE_TITLE,
description: SITE_DESCRIPTION,
title: siteTitle,
description: siteDescription,
site: context.site,
items: posts.map((post) => ({
...post.data,
link: `/blog/${post.slug}/`
items: allPosts.map((post) => ({
title: post.data.title,
pubDate: post.data.date as Date,
link: `/blog/${post.slug}/`,
content: post.body
}))
})
}