chore(strip): fase 0 — verwijder marketing, leads, archive en sitemap
Prototype-ballast verwijderd als eerste stap van de ECD-rebuild: - app/(marketing) incl. blog, contact, documentatie + lib/mdx, lib/content, content/ - /api/leads en publieke marketing-routes uit middleware - app/epd/_archive (backup van clients-module) - sitemap.ts (verwees alleen naar blog), globals.css.backup - root / redirect naar /login; robots.txt op disallow-all (afgeschermd systeem) FHIR-routes blijven bewust staan: /api/fhir/Patient is de facto de patienten-API voor dossier, agenda en Cortex — vervangen volgt in fase 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Content Loader Utility
|
||||
*
|
||||
* Loads JSON content files from the content directory structure.
|
||||
* Supports server-side loading with TypeScript type safety.
|
||||
* Also supports loading and parsing markdown files.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const manifesto = await getContent<ManifestoContent>('nl', 'manifesto')
|
||||
* const markdownSections = await getMarkdownContent('docs/manifesto.md')
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { markdownToSections } from './markdown-parser'
|
||||
|
||||
export async function getContent<T>(
|
||||
locale: string = 'nl',
|
||||
file: string
|
||||
): Promise<T> {
|
||||
try {
|
||||
const content = await import(`@/content/${locale}/${file}.json`)
|
||||
return content.default as T
|
||||
} catch (error) {
|
||||
console.error(`Failed to load content: ${locale}/${file}.json`, error)
|
||||
throw new Error(`Content not found: ${locale}/${file}.json`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse markdown file to ManifestoSection format
|
||||
* Useful for converting manifesto.md to React components
|
||||
*/
|
||||
export async function getMarkdownContent(
|
||||
filePath: string
|
||||
): Promise<Array<{
|
||||
id: string
|
||||
type: 'paragraph'
|
||||
content: string
|
||||
}>> {
|
||||
try {
|
||||
const fullPath = join(process.cwd(), filePath)
|
||||
const markdown = await readFile(fullPath, 'utf-8')
|
||||
return markdownToSections(markdown)
|
||||
} catch (error) {
|
||||
console.error(`Failed to load markdown: ${filePath}`, error)
|
||||
throw new Error(`Markdown file not found: ${filePath}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe content loader with fallback
|
||||
*
|
||||
* Returns default content if file is not found (useful for development)
|
||||
*/
|
||||
export async function getContentWithFallback<T>(
|
||||
locale: string = 'nl',
|
||||
file: string,
|
||||
fallback: T
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await getContent<T>(locale, file)
|
||||
} catch (error) {
|
||||
console.warn(`Using fallback content for: ${locale}/${file}.json`)
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Markdown Parser Utility
|
||||
*
|
||||
* Simple markdown parser for converting manifesto.md content
|
||||
* to React components with proper typography.
|
||||
*
|
||||
* This is a lightweight parser for basic markdown features:
|
||||
* - Paragraphs
|
||||
* - Line breaks
|
||||
* - Basic formatting (bold, italic)
|
||||
*/
|
||||
|
||||
export interface ParsedMarkdown {
|
||||
paragraphs: string[]
|
||||
metadata?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse markdown content into structured format
|
||||
* Splits content by double line breaks into paragraphs
|
||||
*/
|
||||
export function parseMarkdown(markdown: string): ParsedMarkdown {
|
||||
// Split by double line breaks or single line breaks followed by empty line
|
||||
const paragraphs = markdown
|
||||
.split(/\n\s*\n/)
|
||||
.map((para) => para.trim())
|
||||
.filter((para) => para.length > 0)
|
||||
|
||||
return {
|
||||
paragraphs,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown paragraphs to ManifestoSection format
|
||||
* This allows markdown content to be used with existing components
|
||||
*/
|
||||
export function markdownToSections(markdown: string): Array<{
|
||||
id: string
|
||||
type: 'paragraph'
|
||||
content: string
|
||||
}> {
|
||||
const parsed = parseMarkdown(markdown)
|
||||
|
||||
return parsed.paragraphs.map((content, index) => ({
|
||||
id: `paragraph-${index + 1}`,
|
||||
type: 'paragraph' as const,
|
||||
content: content.trim(),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple markdown renderer for inline formatting
|
||||
* Converts **bold** and *italic* to HTML
|
||||
*/
|
||||
export function renderMarkdownInline(text: string): string {
|
||||
return text
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>')
|
||||
}
|
||||
|
||||
259
lib/mdx/blog.ts
259
lib/mdx/blog.ts
@@ -1,259 +0,0 @@
|
||||
/**
|
||||
* Blog MDX Utilities
|
||||
*
|
||||
* Functions for loading and parsing blog MDX files with series support
|
||||
* Based on lib/mdx/documentatie.ts
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const BLOG_DIR = path.join(process.cwd(), 'content/nl/blog')
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface BlogSeries {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
color: 'teal' | 'amber' | 'slate'
|
||||
order: number
|
||||
status: 'active' | 'completed' | 'planned'
|
||||
}
|
||||
|
||||
export interface BlogFrontmatter {
|
||||
title: string
|
||||
description: string
|
||||
date: string
|
||||
published?: boolean
|
||||
seriesOrder: number
|
||||
tags?: string[]
|
||||
image?: string // Optional OG image path (relative to /public, e.g. "/images/blog/my-post.png")
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
slug: string
|
||||
seriesId: string
|
||||
frontmatter: BlogFrontmatter
|
||||
content: string
|
||||
readingTime: number
|
||||
}
|
||||
|
||||
export interface SeriesNavigation {
|
||||
previous: { slug: string; title: string } | null
|
||||
next: { slug: string; title: string } | null
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function calculateReadingTime(content: string): number {
|
||||
const wordsPerMinute = 200
|
||||
const wordCount = content.split(/\s+/).length
|
||||
return Math.ceil(wordCount / wordsPerMinute)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Series Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all series metadata
|
||||
*/
|
||||
export async function getAllSeries(): Promise<BlogSeries[]> {
|
||||
try {
|
||||
const seriesPath = path.join(BLOG_DIR, '_series.json')
|
||||
|
||||
if (!fs.existsSync(seriesPath)) {
|
||||
console.warn('_series.json not found')
|
||||
return []
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(seriesPath, 'utf-8')
|
||||
const data = JSON.parse(content)
|
||||
|
||||
return (data.series || []).sort(
|
||||
(a: BlogSeries, b: BlogSeries) => a.order - b.order
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error loading series:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single series by ID
|
||||
*/
|
||||
export async function getSeries(seriesId: string): Promise<BlogSeries | null> {
|
||||
const allSeries = await getAllSeries()
|
||||
return allSeries.find((s) => s.id === seriesId) || null
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Post Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all blog posts from all series
|
||||
*/
|
||||
export async function getAllPosts(): Promise<BlogPost[]> {
|
||||
if (!fs.existsSync(BLOG_DIR)) return []
|
||||
|
||||
const series = await getAllSeries()
|
||||
const allPosts: BlogPost[] = []
|
||||
|
||||
for (const serie of series) {
|
||||
const serieDir = path.join(BLOG_DIR, serie.id)
|
||||
|
||||
if (!fs.existsSync(serieDir)) continue
|
||||
|
||||
const files = fs.readdirSync(serieDir).filter((f) => f.endsWith('.mdx'))
|
||||
|
||||
for (const file of files) {
|
||||
const slug = file.replace('.mdx', '')
|
||||
const filePath = path.join(serieDir, file)
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
const frontmatter = data as BlogFrontmatter
|
||||
|
||||
// Skip unpublished in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'development' &&
|
||||
frontmatter.published === false
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
allPosts.push({
|
||||
slug,
|
||||
seriesId: serie.id,
|
||||
frontmatter,
|
||||
content,
|
||||
readingTime: calculateReadingTime(content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date, newest first
|
||||
return allPosts.sort(
|
||||
(a, b) =>
|
||||
new Date(b.frontmatter.date).getTime() -
|
||||
new Date(a.frontmatter.date).getTime()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all posts for a specific series
|
||||
*/
|
||||
export async function getPostsBySeries(seriesId: string): Promise<BlogPost[]> {
|
||||
const serieDir = path.join(BLOG_DIR, seriesId)
|
||||
|
||||
if (!fs.existsSync(serieDir)) return []
|
||||
|
||||
const files = fs.readdirSync(serieDir).filter((f) => f.endsWith('.mdx'))
|
||||
const posts: BlogPost[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const slug = file.replace('.mdx', '')
|
||||
const filePath = path.join(serieDir, file)
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
const frontmatter = data as BlogFrontmatter
|
||||
|
||||
// Skip unpublished in production
|
||||
if (
|
||||
process.env.NODE_ENV !== 'development' &&
|
||||
frontmatter.published === false
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
posts.push({
|
||||
slug,
|
||||
seriesId,
|
||||
frontmatter,
|
||||
content,
|
||||
readingTime: calculateReadingTime(content),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by seriesOrder
|
||||
return posts.sort(
|
||||
(a, b) => a.frontmatter.seriesOrder - b.frontmatter.seriesOrder
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single post
|
||||
*/
|
||||
export async function getPost(
|
||||
seriesId: string,
|
||||
slug: string
|
||||
): Promise<BlogPost | null> {
|
||||
const filePath = path.join(BLOG_DIR, seriesId, `${slug}.mdx`)
|
||||
|
||||
if (!fs.existsSync(filePath)) return null
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
const frontmatter = data as BlogFrontmatter
|
||||
|
||||
return {
|
||||
slug,
|
||||
seriesId,
|
||||
frontmatter,
|
||||
content,
|
||||
readingTime: calculateReadingTime(content),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get navigation for a post within its series
|
||||
*/
|
||||
export async function getSeriesNavigation(
|
||||
seriesId: string,
|
||||
slug: string
|
||||
): Promise<SeriesNavigation> {
|
||||
const posts = await getPostsBySeries(seriesId)
|
||||
const currentIndex = posts.findIndex((p) => p.slug === slug)
|
||||
|
||||
return {
|
||||
previous:
|
||||
currentIndex > 0
|
||||
? { slug: posts[currentIndex - 1].slug, title: posts[currentIndex - 1].frontmatter.title }
|
||||
: null,
|
||||
next:
|
||||
currentIndex < posts.length - 1
|
||||
? { slug: posts[currentIndex + 1].slug, title: posts[currentIndex + 1].frontmatter.title }
|
||||
: null,
|
||||
current: currentIndex + 1,
|
||||
total: posts.length,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get series with post counts
|
||||
*/
|
||||
export async function getSeriesWithCounts(): Promise<
|
||||
(BlogSeries & { postCount: number })[]
|
||||
> {
|
||||
const series = await getAllSeries()
|
||||
const result = []
|
||||
|
||||
for (const serie of series) {
|
||||
const posts = await getPostsBySeries(serie.id)
|
||||
result.push({
|
||||
...serie,
|
||||
postCount: posts.length,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* Release Notes MDX Utilities
|
||||
*
|
||||
* Functions for loading and parsing release note MDX files
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const RELEASES_DIR = path.join(process.cwd(), 'content/nl/documentatie')
|
||||
|
||||
export interface ReleaseFrontmatter {
|
||||
title: string
|
||||
category: string
|
||||
group: 'foundation' | 'features' | 'infrastructure' | 'bugs'
|
||||
version: string
|
||||
releaseDate: string
|
||||
status: 'completed' | 'in_progress' | 'planned'
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ReleaseNote {
|
||||
slug: string
|
||||
frontmatter: ReleaseFrontmatter
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all release note files
|
||||
*/
|
||||
export async function getAllReleases(): Promise<ReleaseNote[]> {
|
||||
const files = fs.readdirSync(RELEASES_DIR)
|
||||
|
||||
const releases = files
|
||||
.filter(file => file.endsWith('.mdx') && !file.startsWith('_'))
|
||||
.map(file => {
|
||||
const slug = file.replace('.mdx', '')
|
||||
const filePath = path.join(RELEASES_DIR, file)
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
|
||||
return {
|
||||
slug,
|
||||
frontmatter: data as ReleaseFrontmatter,
|
||||
content,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by group order, then by status (completed first), then alphabetically
|
||||
return releases.sort((a, b) => {
|
||||
const groupOrder = { foundation: 1, features: 2, infrastructure: 3, bugs: 4 }
|
||||
const statusOrder = { completed: 1, in_progress: 2, planned: 3 }
|
||||
|
||||
// 1. Sort by group
|
||||
const aGroupOrder = groupOrder[a.frontmatter.group]
|
||||
const bGroupOrder = groupOrder[b.frontmatter.group]
|
||||
if (aGroupOrder !== bGroupOrder) return aGroupOrder - bGroupOrder
|
||||
|
||||
// 2. Sort by status (completed first, then in_progress, then planned)
|
||||
const aStatusOrder = statusOrder[a.frontmatter.status] ?? 4
|
||||
const bStatusOrder = statusOrder[b.frontmatter.status] ?? 4
|
||||
if (aStatusOrder !== bStatusOrder) return aStatusOrder - bStatusOrder
|
||||
|
||||
// 3. Sort alphabetically within same status
|
||||
return a.frontmatter.category.localeCompare(b.frontmatter.category)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single release by slug
|
||||
*/
|
||||
export async function getRelease(slug: string): Promise<ReleaseNote | null> {
|
||||
const filePath = path.join(RELEASES_DIR, `${slug}.mdx`)
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
const { data, content } = matter(fileContent)
|
||||
|
||||
return {
|
||||
slug,
|
||||
frontmatter: data as ReleaseFrontmatter,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get releases grouped by their group (foundation, features, infrastructure, bugs)
|
||||
*/
|
||||
export async function getReleasesGrouped() {
|
||||
const releases = await getAllReleases()
|
||||
|
||||
return {
|
||||
foundation: releases.filter(r => r.frontmatter.group === 'foundation'),
|
||||
features: releases.filter(r => r.frontmatter.group === 'features'),
|
||||
infrastructure: releases.filter(r => r.frontmatter.group === 'infrastructure'),
|
||||
bugs: releases.filter(r => r.frontmatter.group === 'bugs'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category metadata from index file
|
||||
*/
|
||||
export interface CategoryMetadata {
|
||||
slug: string
|
||||
title: string
|
||||
group: string
|
||||
description: string
|
||||
order: number
|
||||
}
|
||||
|
||||
export interface GroupMetadata {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
order: number
|
||||
}
|
||||
|
||||
interface IndexData {
|
||||
groups: GroupMetadata[]
|
||||
categories: CategoryMetadata[]
|
||||
}
|
||||
|
||||
export async function getCategoryMetadata(): Promise<IndexData> {
|
||||
try {
|
||||
const indexPath = path.join(RELEASES_DIR, '_index.json')
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.warn('_index.json not found, returning empty metadata')
|
||||
return {
|
||||
groups: [],
|
||||
categories: []
|
||||
}
|
||||
}
|
||||
|
||||
const indexContent = fs.readFileSync(indexPath, 'utf-8')
|
||||
return JSON.parse(indexContent)
|
||||
} catch (error) {
|
||||
console.error('Error loading category metadata:', error)
|
||||
return {
|
||||
groups: [],
|
||||
categories: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate slug from heading text (must match the slugify function in mdx-components.tsx)
|
||||
*/
|
||||
function slugify(text: string): string {
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Table of Contents item
|
||||
*/
|
||||
export interface TocItem {
|
||||
id: string
|
||||
text: string
|
||||
level: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract headings from MDX content for Table of Contents
|
||||
* Extracts h1, h2, and h3 headings, filtered for main sections
|
||||
*/
|
||||
export function extractHeadings(content: string): TocItem[] {
|
||||
const headingRegex = /^(#{1,3})\s+(.+)$/gm
|
||||
const headings: TocItem[] = []
|
||||
let match
|
||||
|
||||
// Main sections to include in TOC (h2 level)
|
||||
const includedH2Sections = [
|
||||
'overview',
|
||||
'standaard-compliance',
|
||||
'geimplementeerde-resources',
|
||||
'relaties-tussen-resources',
|
||||
'privacy-beveiliging',
|
||||
'roadmap',
|
||||
'patient-api',
|
||||
'practitioner-api',
|
||||
'encounter-api',
|
||||
'condition-api',
|
||||
'observation-api',
|
||||
'careplan-api',
|
||||
'authenticatie-autorisatie',
|
||||
'error-handling',
|
||||
]
|
||||
|
||||
// Resource subsections to include (h3 level)
|
||||
const includedH3Sections = [
|
||||
'1-practitioners-behandelaren',
|
||||
'2-organizations-instellingen',
|
||||
'3-patients-patientenclienten',
|
||||
'4-encounters-contactmomenten',
|
||||
'5-conditions-diagnoses',
|
||||
'6-observations-metingen-en-observaties',
|
||||
'7-careplans-behandelplannen',
|
||||
]
|
||||
|
||||
while ((match = headingRegex.exec(content)) !== null) {
|
||||
const level = match[1].length
|
||||
const text = match[2]
|
||||
.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1') // Remove markdown links
|
||||
.replace(/`([^`]+)`/g, '$1') // Remove inline code
|
||||
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold
|
||||
.replace(/\*([^*]+)\*/g, '$1') // Remove italic
|
||||
.trim()
|
||||
|
||||
const id = slugify(text)
|
||||
|
||||
// Include h2 headings from the main sections list
|
||||
if (level === 2 && includedH2Sections.includes(id)) {
|
||||
headings.push({
|
||||
id,
|
||||
text,
|
||||
level,
|
||||
})
|
||||
}
|
||||
// Include h3 headings from the resource sections list
|
||||
else if (level === 3 && includedH3Sections.includes(id)) {
|
||||
headings.push({
|
||||
id,
|
||||
text,
|
||||
level,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return headings
|
||||
}
|
||||
Reference in New Issue
Block a user