147 lines
5.2 KiB
JavaScript
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
|
|
}
|
|
}
|
|
}
|
|
}
|