import { defineDocumentType, ComputedFields, makeSource } from 'contentlayer2/source-files' import { writeFileSync } from 'fs' import readingTime from 'reading-time' import { slug } from 'github-slugger' import path from 'path' import { fromHtmlIsomorphic } from 'hast-util-from-html-isomorphic' // Remark packages import remarkGfm from 'remark-gfm' import remarkMath from 'remark-math' import { remarkAlert } from 'remark-github-blockquote-alert' import { remarkExtractFrontmatter, remarkCodeTitles, remarkImgToJsx, extractTocHeadings, } from 'pliny/mdx-plugins/index.js' // Rehype packages import rehypeSlug from 'rehype-slug' import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypeKatex from 'rehype-katex' import rehypeCitation from 'rehype-citation' import rehypePrismPlus from 'rehype-prism-plus' import rehypePresetMinify from 'rehype-preset-minify' import siteMetadata from './data/siteMetadata' import { allCoreContent, sortPosts } from 'pliny/utils/contentlayer.js' const root = process.cwd() const isProduction = process.env.NODE_ENV === 'production' // heroicon mini link const icon = fromHtmlIsomorphic( ` `, { fragment: true } ) const computedFields: ComputedFields = { readingTime: { type: 'json', resolve: (doc) => readingTime(doc.body.raw) }, slug: { type: 'string', resolve: (doc) => doc._raw.flattenedPath.replace(/^.+?(\/)/, ''), }, path: { type: 'string', resolve: (doc) => doc._raw.flattenedPath, }, filePath: { type: 'string', resolve: (doc) => doc._raw.sourceFilePath, }, toc: { type: 'json', resolve: (doc) => extractTocHeadings(doc.body.raw) }, } /** * Count the occurrences of all tags across blog posts and write to json file */ function createTagCount(allBlogs) { const tagCount: Record = {} allBlogs.forEach((file) => { if (file.tags && (!isProduction || file.draft !== true)) { file.tags.forEach((tag) => { const formattedTag = slug(tag) if (formattedTag in tagCount) { tagCount[formattedTag] += 1 } else { tagCount[formattedTag] = 1 } }) } }) writeFileSync('./app/tag-data.json', JSON.stringify(tagCount)) } function createSearchIndex(allBlogs) { if ( siteMetadata?.search?.provider === 'kbar' && siteMetadata.search.kbarConfig.searchDocumentsPath ) { writeFileSync( `public/${path.basename(siteMetadata.search.kbarConfig.searchDocumentsPath)}`, JSON.stringify(allCoreContent(sortPosts(allBlogs))) ) console.log('Local search index generated...') } } export const Blog = defineDocumentType(() => ({ name: 'Blog', filePathPattern: 'blog/**/*.mdx', contentType: 'mdx', fields: { title: { type: 'string', required: true }, date: { type: 'date', required: true }, tags: { type: 'list', of: { type: 'string' }, default: [] }, lastmod: { type: 'date' }, draft: { type: 'boolean' }, summary: { type: 'string' }, images: { type: 'json' }, authors: { type: 'list', of: { type: 'string' } }, layout: { type: 'string' }, bibliography: { type: 'string' }, canonicalUrl: { type: 'string' }, }, computedFields: { ...computedFields, structuredData: { type: 'json', resolve: (doc) => ({ '@context': 'https://schema.org', '@type': 'BlogPosting', headline: doc.title, datePublished: doc.date, dateModified: doc.lastmod || doc.date, description: doc.summary, image: doc.images ? doc.images[0] : siteMetadata.socialBanner, url: `${siteMetadata.siteUrl}/${doc._raw.flattenedPath}`, }), }, }, })) export const Authors = defineDocumentType(() => ({ name: 'Authors', filePathPattern: 'authors/**/*.mdx', contentType: 'mdx', fields: { name: { type: 'string', required: true }, avatar: { type: 'string' }, occupation: { type: 'string' }, company: { type: 'string' }, email: { type: 'string' }, twitter: { type: 'string' }, linkedin: { type: 'string' }, github: { type: 'string' }, layout: { type: 'string' }, }, computedFields, })) export default makeSource({ contentDirPath: 'data', documentTypes: [Blog, Authors], mdx: { cwd: process.cwd(), remarkPlugins: [ remarkExtractFrontmatter, remarkGfm, remarkCodeTitles, remarkMath, remarkImgToJsx, remarkAlert, ], rehypePlugins: [ rehypeSlug, [ rehypeAutolinkHeadings, { behavior: 'prepend', headingProperties: { className: ['content-header'], }, content: icon, }, ], rehypeKatex, [rehypeCitation, { path: path.join(root, 'data') }], [rehypePrismPlus, { defaultLanguage: 'js', ignoreMissing: true }], rehypePresetMinify, ], }, onSuccess: async (importData) => { const { allBlogs } = await importData() createTagCount(allBlogs) createSearchIndex(allBlogs) }, })