dappconnect-sdks/apps/website/contentlayer.config.ts

262 lines
7.0 KiB
TypeScript
Raw Normal View History

[website] Add documentation (#403) * setup md/mdx * add test page * setup mdx provider * Add breadcrumbs * links -> routes * add information box * add more remark plugins * text InformationBox * setup contentlayer * add testing docs folder * setup docs static generation * add ssg to blog too * add Shortcut component * add SearchButton * add hook for keyboard shortcuts * update main docs page * update main learn page content * side-bar -> sidebar-menu * rename docs pages * add heading anchors, use new components * add table of contents to docs * move styling to breadcrumbs * cleanup markdown examples * change slugify fn * update getting-started.md * add highlight matches component * update learn pages * rename website InformationBox to Admonition * ?enable user-select * add static helpers to epics * simplify components structure * update insights layout * add link tree utils * add search engine * update docs import * update contenlayer * add docs indexer * support code block highlighting * search index * fix types * remove supporting files * update tree builder * make build work * update docs * update label * update toc * add config.json * update menu * search index import * learn -> help * fix md file * update toc * fix code snippets * add help index route * remove testing files * add original docs * migrate getting started * migrate messaging-and-web3-browser docs * migrate network-nodes-and-statistics docs * handle empty content * add navigation config * wip * migrate status-wallet docs * migrate to new admonitions format * migrate your-profile-and-preferences docs * migrate part of status-communities docs * mv * rm log * index * fix lint * fix warn * fix title * fix docs layout * udpate next.config * use lowercase lang * generate contentlayer types before typechecking * update docs images * update help routes --------- Co-authored-by: Felicio Mununga <felicio@users.noreply.github.com>
2023-06-27 20:30:11 +00:00
import {
defineDocumentType,
defineNestedType,
makeSource,
} from '@contentlayer/source-files'
import remarkHeadings from '@vcarl/remark-headings'
import { slug as slugify } from 'github-slugger'
import { toString } from 'mdast-util-to-string'
import * as fs from 'node:fs/promises'
import path from 'node:path'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import { remark } from 'remark'
import remarkDirective from 'remark-directive'
import remarkGfm from 'remark-gfm'
// import remarkMessageControl from 'remark-message-control'
// import remarkAdmonitions from 'remark-github-beta-blockquote-admonitions'
// import remarkBreaks from 'remark-breaks'
import strip from 'strip-markdown'
import { visit } from 'unist-util-visit'
import type { Plugin } from 'unified'
import type { Node } from 'unist'
const CONTENT_DIR_PATH = 'docs'
export type DocHeading = {
level: 1 | 2
value: string
}
export type DocIndex = {
path: string
title: string
content: {
[key in string]: string[]
}
}
const HeroImage = defineNestedType(() => ({
name: 'HeroImage',
fields: {
src: { type: 'string', required: true },
alt: { type: 'string', required: true },
},
}))
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: '**/*.md{,x}',
contentType: 'mdx',
fields: {
id: { type: 'number', required: false },
revision: { type: 'string', required: false },
language: { type: 'string', required: false },
title: { type: 'string', required: true },
author: { type: 'string', required: false },
image: { type: 'nested', of: HeroImage, required: false },
},
computedFields: {
slug: {
// @ts-expect-error TODO
type: 'string[]',
resolve: doc => doc._raw.flattenedPath.split('/'),
// resolve: doc => {
// return getPathSegments(doc._raw.flattenedPath).map(
// ({ pathName }) => pathName
// )
// },
},
url: {
type: 'string',
resolve: doc => `/help/${doc._raw.flattenedPath}`,
// resolve: doc => {
// const slug = getPathSegments(doc._raw.flattenedPath).map(
// ({ pathName }) => pathName
// )
// return `/help/${slug.join('/')}`
// },
},
pathSegments: {
// @ts-expect-error TODO
type: '{ order: number; pathName: string }[]',
resolve: doc => getPathSegments(doc._raw.flattenedPath),
},
titleSlug: {
type: 'string',
resolve: doc => slugify(doc.title),
},
headings: {
// @ts-expect-error TODO
type: '{ level: 1 | 2; value: string, slug: string }[]',
resolve: async doc => {
const result = await remark().use(remarkHeadings).process(doc.body.raw)
return (result.data.headings as { depth: number; value: string }[])
.filter(({ depth }) => [1, 2].includes(depth))
.map<DocHeading>(({ depth, value }) => ({
level: depth as 1 | 2,
value,
slug: slugify(value),
}))
},
},
last_edited: {
type: 'date',
resolve: async (doc): Promise<Date> => {
const stats = await fs.stat(
path.join(CONTENT_DIR_PATH, doc._raw.sourceFilePath)
)
return stats.mtime
},
},
},
}))
function getPathSegments(filePath: string) {
return (
filePath
.split('/')
// .slice(1) // skip content dir path `/docs`
.map(fileName => {
const re = /^((\d+)-)?(.*)$/
const [, , orderStr, pathName] = fileName.match(re) ?? []
const order = orderStr ? parseInt(orderStr) : 0
return { order, pathName }
})
)
}
const remarkAdmonition: Plugin = () => {
return tree => {
visit(tree, node => {
if (
node.type === 'textDirective' ||
node.type === 'leafDirective' ||
node.type === 'containerDirective'
) {
// @ts-expect-error TODO
if (!['info', 'tip', 'warn'].includes(node.name)) {
return
}
// Store node.name before overwritten with "Alert".
// @ts-expect-error TODO
const type = node.name
// const data = node.data || (node.data = {})
// const tagName = node.type === 'textDirective' ? 'span' : 'div'
node.type = 'mdxJsxFlowElement'
// @ts-expect-error TODO
node.name = 'Admonition'
// @ts-expect-error TODO
node.attributes = [
{ type: 'mdxJsxAttribute', name: 'type', value: type },
// @ts-expect-error TODO
{ type: 'mdxJsxAttribute', name: 'title', value: node.label },
]
}
})
}
}
const remarkIndexer: Plugin = () => (root, file) => {
file.data.index = indexer(root)
}
function indexer(root: Node) {
const index: DocIndex['content'] = {}
let parentHeading = ''
visit(root, ['paragraph', 'heading'], node => {
if (node.type === 'heading') {
const text = toString(node, { includeImageAlt: false })
parentHeading = text
return
}
const text = toString(node, { includeImageAlt: false })
index[parentHeading] ??= []
index[parentHeading].push(text)
})
return index
}
export default makeSource({
contentDirPath: CONTENT_DIR_PATH,
documentTypes: [Doc],
mdx: {
remarkPlugins: [
remarkGfm,
remarkDirective,
remarkAdmonition,
// [remarkMessageControl, { name: 'hello' }],
],
rehypePlugins: [
rehypeSlug,
[
rehypePrettyCode,
{
// Use one of Shiki's packaged themes
theme: 'github-light',
// Keep the background or use a custom background color?
// keepBackground: true,
// Callback hooks to add custom logic to nodes when visiting
// them.
onVisitLine(node: any) {
// Prevent lines from collapsing in `display: grid` mode, and
// allow empty lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }]
}
},
onVisitHighlightedLine(node: any) {
// Each line node by default has `class="line"`.
node.properties.className.push('highlighted')
},
onVisitHighlightedWord(node: any) {
// Each word node has no className by default.
node.properties.className = ['word']
},
},
],
],
},
onSuccess: async importData => {
const { allDocs } = await importData()
const basePath = '/docs'
const index: DocIndex[] = []
for (const doc of allDocs) {
const result = await remark()
.use(strip, {
keep: ['heading'],
})
.use(remarkGfm)
.use(remarkIndexer, { path: basePath + '/' + doc._raw.flattenedPath })
.process(doc.body.raw)
index.push({
title: doc.title,
path: basePath + '/' + doc._raw.flattenedPath,
content: result.data.index as DocIndex['content'],
})
}
const filePath = path.resolve('./.contentlayer/en.json')
fs.writeFile(filePath, JSON.stringify(index))
},
})