Files
Orchestrator/privat/CV/andreknie.de/plugins/vite-plugin-content.js

147 lines
5.2 KiB
JavaScript

import { readFileSync, readdirSync, existsSync } from 'fs'
import { join, resolve, extname, basename } from 'path'
import matter from 'gray-matter'
import { marked } from 'marked'
import * as yaml from 'js-yaml'
const CONTENT_DIR = resolve(process.cwd(), 'content')
const VIRTUAL_MODULE_ID = 'virtual:content'
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID
const DETAIL_LOADERS_MODULE_ID = 'virtual:content-detail-loaders'
const RESOLVED_DETAIL_LOADERS_MODULE_ID = '\0' + DETAIL_LOADERS_MODULE_ID
const DETAIL_MODULE_PREFIX = 'virtual:content-detail/'
const RESOLVED_DETAIL_MODULE_PREFIX = '\0' + DETAIL_MODULE_PREFIX
const DETAIL_TYPES = ['posts', 'kniepunkt']
function readContentDir(subdir, includeBodies = false) {
const dir = join(CONTENT_DIR, subdir)
if (!existsSync(dir)) return []
return readdirSync(dir)
.filter(f => !f.startsWith('.'))
.map(filename => {
const filepath = join(dir, filename)
const ext = extname(filename)
const slug = basename(filename, ext)
if (ext === '.md') {
const raw = readFileSync(filepath, 'utf-8')
const { data, content } = matter(raw)
const body = renderMarkdownContent(content)
const searchText = body.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 320)
return {
...data,
slug,
...(includeBodies ? { body } : { searchText }),
_source: `${subdir}/${filename}`
}
}
if (ext === '.yaml' || ext === '.yml') {
const raw = readFileSync(filepath, 'utf-8')
const data = yaml.load(raw)
return { ...data, slug, _source: `${subdir}/${filename}` }
}
return null
})
.filter(Boolean)
}
export function renderMarkdownContent(content) {
const tokens = marked.lexer(content)
const firstContentToken = tokens.findIndex(token => token.type !== 'space')
const firstToken = tokens[firstContentToken]
if (firstToken?.type === 'heading' && firstToken.depth === 1) {
tokens.splice(firstContentToken, 1)
}
return marked.parser(tokens)
}
function readMetaFile(filename) {
const filepath = join(CONTENT_DIR, 'meta', filename)
if (!existsSync(filepath)) return null
const raw = readFileSync(filepath, 'utf-8')
return yaml.load(raw)
}
function loadAllContent(includeBodies = false) {
return {
posts: readContentDir('posts', includeBodies),
kniepunkt: readContentDir('kniepunkt', includeBodies),
podcast: readContentDir('podcast', includeBodies),
talks: readContentDir('talks', includeBodies),
consulting: readContentDir('consulting', includeBodies),
resources: readContentDir('resources', includeBodies),
events: readContentDir('events', includeBodies),
meta: {
profile: readMetaFile('profile.yaml'),
stats: readMetaFile('stats.yaml'),
testimonials: readMetaFile('testimonials.yaml'),
logos: readMetaFile('logos.yaml'),
services: readMetaFile('services.yaml'),
about: readMetaFile('about.yaml'),
speaking: readMetaFile('speaking.yaml'),
newsletter: readMetaFile('newsletter.yaml'),
}
}
}
function createDetailLoadersModule() {
const typeEntries = DETAIL_TYPES.map(type => {
const loaders = readContentDir(type)
.map(item => `${JSON.stringify(item.slug)}: () => import(${JSON.stringify(`${DETAIL_MODULE_PREFIX}${type}/${item.slug}`)})`)
.join(',\n')
return `${JSON.stringify(type)}: {\n${loaders}\n}`
})
return `export default {\n${typeEntries.join(',\n')}\n}`
}
function loadDetailModule(id) {
const relativeId = id.slice(RESOLVED_DETAIL_MODULE_PREFIX.length)
const separator = relativeId.indexOf('/')
if (separator < 1) return null
const type = relativeId.slice(0, separator)
const slug = relativeId.slice(separator + 1)
if (!DETAIL_TYPES.includes(type)) return null
const item = readContentDir(type, true).find(candidate => candidate.slug === slug)
return item ? `export default ${JSON.stringify(item)}` : null
}
export default function contentPlugin() {
return {
name: 'vite-plugin-content',
resolveId(id) {
if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID
if (id === DETAIL_LOADERS_MODULE_ID) return RESOLVED_DETAIL_LOADERS_MODULE_ID
if (id.startsWith(DETAIL_MODULE_PREFIX)) return `\0${id}`
},
load(id) {
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
const content = loadAllContent()
return `export default ${JSON.stringify(content)}`
}
if (id === RESOLVED_DETAIL_LOADERS_MODULE_ID) return createDetailLoadersModule()
if (id.startsWith(RESOLVED_DETAIL_MODULE_PREFIX)) return loadDetailModule(id)
},
handleHotUpdate({ file, server }) {
if (file.includes('content')) {
const modules = [
server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID),
server.moduleGraph.getModuleById(RESOLVED_DETAIL_LOADERS_MODULE_ID),
...Array.from(server.moduleGraph.idToModuleMap.entries())
.filter(([id]) => id.startsWith(RESOLVED_DETAIL_MODULE_PREFIX))
.map(([, module]) => module),
].filter(Boolean)
modules.forEach(module => server.moduleGraph.invalidateModule(module))
return modules
}
}
}
}