status-web/apps/website/contentlayer.config.ts
Pavel b88f7e73da
[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 21:30:11 +01:00

262 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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))
},
})