2023-04-25 11:01:25 +00:00
|
|
|
import fs from 'fs'
|
|
|
|
import matter from 'gray-matter'
|
|
|
|
import path from 'path'
|
|
|
|
import authors from '../content/authors.json'
|
|
|
|
import sections from '../content/sections.json'
|
|
|
|
import {
|
|
|
|
Article,
|
|
|
|
Articles,
|
|
|
|
Category,
|
|
|
|
GeneratedContent,
|
|
|
|
Group,
|
|
|
|
MarkdownContent,
|
|
|
|
Section,
|
|
|
|
Status,
|
|
|
|
} from '../types/content-types'
|
|
|
|
|
2023-06-01 18:01:49 +00:00
|
|
|
const { log: nicelog } = console
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
type InputCategory = {
|
|
|
|
id: string
|
|
|
|
title: string
|
|
|
|
description: string
|
|
|
|
groups: Group[]
|
|
|
|
}
|
|
|
|
|
|
|
|
type InputSection = {
|
|
|
|
id: string
|
|
|
|
title: string
|
|
|
|
description: string
|
|
|
|
categories: InputCategory[]
|
|
|
|
}
|
|
|
|
|
|
|
|
function generateSection(
|
|
|
|
section: InputSection,
|
|
|
|
content: MarkdownContent,
|
|
|
|
articles: Articles
|
|
|
|
): Section {
|
|
|
|
// A temporary table of categories
|
|
|
|
const _categories: Record<string, Category> = {}
|
|
|
|
|
|
|
|
// Uncategorized articles
|
|
|
|
const _ucg: Article[] = []
|
|
|
|
|
|
|
|
// A temporary table of articles mapped to categories
|
|
|
|
const _categoryArticles: Record<string, Article[]> = Object.fromEntries(
|
|
|
|
section.categories.map((category) => [category.id, []])
|
|
|
|
)
|
|
|
|
|
|
|
|
// The file directory for this section
|
|
|
|
const dir = path.join(process.cwd(), 'content', section.id)
|
|
|
|
|
|
|
|
fs.readdirSync(dir, { withFileTypes: false }).forEach((result: string | Buffer) => {
|
|
|
|
try {
|
|
|
|
const filename = result.toString()
|
|
|
|
|
|
|
|
const fileContent = fs.readFileSync(path.join(dir, filename)).toString()
|
|
|
|
|
|
|
|
const extension = path.extname(filename)
|
|
|
|
|
|
|
|
const articleId = filename.replace(extension, '')
|
|
|
|
|
|
|
|
const parsed = matter({ content: fileContent }, {})
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'development' && parsed.data.status !== 'published') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If a category was provided but that category was not found in the section, throw an error
|
|
|
|
const category =
|
|
|
|
parsed.data.category && section.categories.find((c) => c.id === parsed.data.category)
|
|
|
|
if (parsed.data.category && !category) {
|
|
|
|
throw Error(
|
|
|
|
`Could not find a category for section ${section.id} with id ${parsed.data.category}.`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (parsed.data.author && !authors[parsed.data.author as keyof typeof authors]) {
|
|
|
|
throw Error(`Could not find an author with id ${parsed.data.author}.`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// By default, the category is ucg (uncategorized)
|
|
|
|
const { category: categoryId = 'ucg' } = parsed.data
|
|
|
|
|
|
|
|
const article: Article = {
|
|
|
|
id: articleId,
|
|
|
|
sectionIndex: 0,
|
|
|
|
groupIndex: -1,
|
|
|
|
groupId: parsed.data.group ?? null,
|
|
|
|
categoryIndex: parsed.data.order ?? -1,
|
|
|
|
sectionId: section.id,
|
|
|
|
categoryId: parsed.data.category ?? 'ucg',
|
|
|
|
status: parsed.data.status ?? Status.Draft,
|
|
|
|
title: parsed.data.title ?? 'Article',
|
|
|
|
description: parsed.data.description ?? 'An article for the docs site.',
|
|
|
|
hero: parsed.data.hero ?? null,
|
|
|
|
date: parsed.data.date ? new Date(parsed.data.date).toISOString() : null,
|
|
|
|
keywords: parsed.data.keywords ?? [],
|
|
|
|
next: null,
|
|
|
|
prev: null,
|
|
|
|
author: parsed.data.author
|
|
|
|
? authors[parsed.data.author as keyof typeof authors] ?? null
|
|
|
|
: null,
|
2023-05-12 16:02:32 +00:00
|
|
|
sourceUrl: `https://github.com/tldraw/tldraw/tree/main/apps/docs/content/${section.id}/${articleId}${extension}`,
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (article.id === section.id) {
|
|
|
|
article.categoryIndex = -1
|
|
|
|
article.sectionIndex = -1
|
|
|
|
articles[section.id + '_index'] = article
|
|
|
|
content[section.id + '_index'] = parsed.content
|
|
|
|
} else {
|
|
|
|
if (category) {
|
|
|
|
if (article.id === category.id) {
|
|
|
|
article.categoryIndex = -1
|
|
|
|
article.sectionIndex = -1
|
|
|
|
articles[category.id + '_index'] = article
|
|
|
|
content[category.id + '_index'] = parsed.content
|
|
|
|
} else {
|
|
|
|
_categoryArticles[categoryId].push(article)
|
|
|
|
content[articleId] = parsed.content
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_ucg.push(article)
|
|
|
|
content[articleId] = parsed.content
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const sortArticles = (articleA: Article, articleB: Article) => {
|
|
|
|
const { categoryIndex: categoryIndexA, date: dateA = '01/01/1970' } = articleA
|
|
|
|
const { categoryIndex: categoryIndexB, date: dateB = '01/01/1970' } = articleB
|
|
|
|
|
|
|
|
return categoryIndexA === categoryIndexB
|
|
|
|
? new Date(dateB!).getTime() > new Date(dateA!).getTime()
|
|
|
|
? 1
|
|
|
|
: -1
|
|
|
|
: categoryIndexA < categoryIndexB
|
|
|
|
? -1
|
|
|
|
: 1
|
|
|
|
}
|
|
|
|
|
|
|
|
let sectionIndex = 0
|
|
|
|
|
|
|
|
// Sort ucg articles by date and add them to the articles table
|
|
|
|
_ucg.sort(sortArticles).forEach((article, i) => {
|
|
|
|
article.categoryIndex = i
|
|
|
|
article.sectionIndex = sectionIndex++
|
|
|
|
article.prev = _ucg[i - 1]?.id ?? null
|
|
|
|
article.next = _ucg[i + 1]?.id ?? null
|
|
|
|
articles[article.id] = article
|
|
|
|
})
|
|
|
|
|
|
|
|
// Sort categorized articles by date and add them to the articles table
|
|
|
|
section.categories.forEach((category) => {
|
|
|
|
const categoryArticles = _categoryArticles[category.id]
|
|
|
|
|
|
|
|
categoryArticles.sort(sortArticles).forEach((article, i) => {
|
|
|
|
article.categoryIndex = i
|
|
|
|
article.sectionIndex = sectionIndex++
|
|
|
|
article.prev = categoryArticles[i - 1]?.id ?? null
|
|
|
|
article.next = categoryArticles[i + 1]?.id ?? null
|
|
|
|
articles[article.id] = article
|
|
|
|
})
|
|
|
|
|
|
|
|
_categories[category.id] = {
|
|
|
|
...category,
|
|
|
|
articleIds: categoryArticles.map((article) => article.id),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
...section,
|
|
|
|
categories: [
|
|
|
|
{
|
|
|
|
id: 'ucg',
|
|
|
|
title: 'Uncategorized',
|
|
|
|
description: 'Articles that do not belong to a category.',
|
|
|
|
groups: [],
|
|
|
|
articleIds: _ucg
|
|
|
|
.sort((a, b) => a.sectionIndex - b.sectionIndex)
|
|
|
|
.map((article) => article.id),
|
|
|
|
},
|
|
|
|
...section.categories.map(({ id }) => _categories[id]).filter((c) => c.articleIds.length > 0),
|
|
|
|
],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function generateContent(): Promise<GeneratedContent> {
|
|
|
|
const content: MarkdownContent = {}
|
|
|
|
const articles: Articles = {}
|
|
|
|
|
2023-06-01 18:01:49 +00:00
|
|
|
nicelog('• Generating site content (content.json)')
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
try {
|
2023-06-17 09:46:46 +00:00
|
|
|
const outputSections: Section[] = [...(sections as InputSection[])]
|
2023-04-25 11:01:25 +00:00
|
|
|
.map((section) => generateSection(section, content, articles))
|
|
|
|
.filter((section) => section.categories.some((c) => c.articleIds.length > 0))
|
|
|
|
|
2023-06-01 18:01:49 +00:00
|
|
|
nicelog('✔ Generated site content.')
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
// Write to disk
|
|
|
|
|
2023-06-17 09:46:46 +00:00
|
|
|
const generatedApiContent = (await import(
|
|
|
|
path.join(process.cwd(), 'api-content.json')
|
|
|
|
)) as GeneratedContent
|
|
|
|
|
|
|
|
const contentComplete: GeneratedContent = {
|
|
|
|
sections: generatedApiContent
|
|
|
|
? [...outputSections, ...generatedApiContent.sections]
|
|
|
|
: outputSections,
|
|
|
|
content: generatedApiContent ? { ...content, ...generatedApiContent.content } : content,
|
|
|
|
articles: generatedApiContent ? { ...articles, ...generatedApiContent.articles } : articles,
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
path.join(process.cwd(), 'content.json'),
|
|
|
|
JSON.stringify(contentComplete, null, 2)
|
|
|
|
)
|
|
|
|
|
|
|
|
return contentComplete
|
|
|
|
} catch (error) {
|
2023-06-01 18:01:49 +00:00
|
|
|
nicelog(`x Could not generate site content.`)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
}
|