diff --git a/app/(marketing)/blog/page.tsx b/app/(marketing)/blog/page.tsx new file mode 100644 index 0000000..c3bc67b --- /dev/null +++ b/app/(marketing)/blog/page.tsx @@ -0,0 +1,154 @@ +/** + * Blog Overview Page + * + * Landing page with series cards and recent posts + */ + +import Link from 'next/link' +import { getAllPosts, getSeriesWithCounts, type BlogSeries } from '@/lib/mdx/blog' + +export const metadata = { + title: 'Blog - AI Speedrun', + description: 'Artikelen over AI-gestuurde software ontwikkeling, healthcare IT en het bouwen van een EPD.', +} + +export default async function BlogPage() { + const series = await getSeriesWithCounts() + const posts = await getAllPosts() + + return ( +
+
+ {/* Header */} +
+

+ Blog +

+

+ Artikelen over AI-gestuurde ontwikkeling en de reis van het bouwen van een EPD. +

+
+ + {/* Series Section */} + {series.length > 0 && ( +
+

+ Series +

+
+ {series.map((serie) => ( + + ))} +
+
+ )} + + {/* Recent Posts Section */} +
+

+ Recente Posts +

+ {posts.length === 0 ? ( +

+ Nog geen blogposts gepubliceerd. +

+ ) : ( +
+ {posts.map((post) => ( + +
+ + {post.seriesId.replace('-', ' ')} + + + + + {post.readingTime} min leestijd +
+

+ {post.frontmatter.title} +

+

+ {post.frontmatter.description} +

+ {post.frontmatter.tags && post.frontmatter.tags.length > 0 && ( +
+ {post.frontmatter.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + + ))} +
+ )} +
+
+
+ ) +} + +function SeriesCard({ series }: { series: BlogSeries & { postCount: number } }) { + const colorStyles = { + teal: { + bg: 'bg-teal-50', + border: 'border-teal-200 hover:border-teal-400', + badge: 'bg-teal-100 text-teal-700', + dot: 'bg-teal-500', + }, + amber: { + bg: 'bg-amber-50', + border: 'border-amber-200 hover:border-amber-400', + badge: 'bg-amber-100 text-amber-700', + dot: 'bg-amber-500', + }, + slate: { + bg: 'bg-slate-50', + border: 'border-slate-200 hover:border-slate-400', + badge: 'bg-slate-100 text-slate-700', + dot: 'bg-slate-500', + }, + } + + const styles = colorStyles[series.color] || colorStyles.slate + const statusLabels = { + completed: 'Voltooid', + active: 'Actief', + planned: 'Gepland', + } + + return ( + +
+
+ + {statusLabels[series.status]} + +
+

{series.title}

+

{series.description}

+
+ {series.postCount} {series.postCount === 1 ? 'deel' : 'delen'} +
+ + ) +} + diff --git a/app/(marketing)/blog/serie/[serieId]/[slug]/page.tsx b/app/(marketing)/blog/serie/[serieId]/[slug]/page.tsx new file mode 100644 index 0000000..69e939e --- /dev/null +++ b/app/(marketing)/blog/serie/[serieId]/[slug]/page.tsx @@ -0,0 +1,214 @@ +/** + * Individual Blog Post Page + * + * Renders MDX content with series navigation + */ + +import { notFound } from 'next/navigation' +import Link from 'next/link' +import { MDXRemote } from 'next-mdx-remote/rsc' +import { getPost, getSeries, getPostsBySeries, getSeriesNavigation, getAllPosts } from '@/lib/mdx/blog' +import { mdxComponents } from '../../../../documentatie/components/mdx-components' + +interface PostPageProps { + params: Promise<{ serieId: string; slug: string }> +} + +export async function generateStaticParams() { + const posts = await getAllPosts() + return posts.map((post) => ({ + serieId: post.seriesId, + slug: post.slug, + })) +} + +export async function generateMetadata({ params }: PostPageProps) { + const { serieId, slug } = await params + const post = await getPost(serieId, slug) + const series = await getSeries(serieId) + + if (!post || !series) { + return { title: 'Post niet gevonden' } + } + + return { + title: `${post.frontmatter.title} - ${series.title}`, + description: post.frontmatter.description, + openGraph: { + title: post.frontmatter.title, + description: post.frontmatter.description, + type: 'article', + publishedTime: post.frontmatter.date, + }, + } +} + +export default async function PostPage({ params }: PostPageProps) { + const { serieId, slug } = await params + const post = await getPost(serieId, slug) + const series = await getSeries(serieId) + + if (!post || !series) { + notFound() + } + + const navigation = await getSeriesNavigation(serieId, slug) + const { frontmatter, content, readingTime } = post + + const colorStyles = { + teal: { + badge: 'bg-teal-100 text-teal-700 border-teal-200', + link: 'text-teal-600 hover:text-teal-700', + }, + amber: { + badge: 'bg-amber-100 text-amber-700 border-amber-200', + link: 'text-amber-600 hover:text-amber-700', + }, + slate: { + badge: 'bg-slate-100 text-slate-700 border-slate-200', + link: 'text-slate-600 hover:text-slate-700', + }, + } + + const styles = colorStyles[series.color] || colorStyles.slate + + return ( +
+
+ {/* Back link */} +
+ + + + + Terug naar {series.title} + +
+ + {/* Header */} +
+ {/* Series badge */} +
+ + {series.title} + + + Deel {navigation.current} van {navigation.total} + +
+ + {/* Title */} +

+ {frontmatter.title} +

+ + {/* Description */} +

+ {frontmatter.description} +

+ + {/* Meta */} +
+ + + {readingTime} min leestijd +
+ + {/* Tags */} + {frontmatter.tags && frontmatter.tags.length > 0 && ( +
+ {frontmatter.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ + {/* MDX Content */} +
+ +
+ + {/* Series Navigation */} + + + {/* Back to overview */} +
+ + ← Terug naar blog overzicht + +
+
+
+ ) +} + diff --git a/app/(marketing)/blog/serie/[serieId]/page.tsx b/app/(marketing)/blog/serie/[serieId]/page.tsx new file mode 100644 index 0000000..69ae491 --- /dev/null +++ b/app/(marketing)/blog/serie/[serieId]/page.tsx @@ -0,0 +1,160 @@ +/** + * Series Overview Page + * + * Shows all posts in a specific series + */ + +import { notFound } from 'next/navigation' +import Link from 'next/link' +import { getSeries, getPostsBySeries, getAllSeries } from '@/lib/mdx/blog' + +interface SeriesPageProps { + params: Promise<{ serieId: string }> +} + +export async function generateStaticParams() { + const series = await getAllSeries() + return series.map((serie) => ({ serieId: serie.id })) +} + +export async function generateMetadata({ params }: SeriesPageProps) { + const { serieId } = await params + const series = await getSeries(serieId) + + if (!series) { + return { title: 'Serie niet gevonden' } + } + + return { + title: `${series.title} - Blog`, + description: series.description, + } +} + +export default async function SeriesPage({ params }: SeriesPageProps) { + const { serieId } = await params + const series = await getSeries(serieId) + + if (!series) { + notFound() + } + + const posts = await getPostsBySeries(serieId) + + const colorStyles = { + teal: { + bg: 'bg-teal-50', + badge: 'bg-teal-100 text-teal-700 border-teal-200', + dot: 'bg-teal-500', + number: 'bg-teal-600 text-white', + }, + amber: { + bg: 'bg-amber-50', + badge: 'bg-amber-100 text-amber-700 border-amber-200', + dot: 'bg-amber-500', + number: 'bg-amber-600 text-white', + }, + slate: { + bg: 'bg-slate-50', + badge: 'bg-slate-100 text-slate-700 border-slate-200', + dot: 'bg-slate-500', + number: 'bg-slate-600 text-white', + }, + } + + const styles = colorStyles[series.color] || colorStyles.slate + const statusLabels = { + completed: 'Voltooid', + active: 'Actief', + planned: 'Gepland', + } + + return ( +
+
+ {/* Back link */} +
+ + + + + Terug naar blog + +
+ + {/* Series Header */} +
+
+
+ + {statusLabels[series.status]} + +
+

+ {series.title} +

+

+ {series.description} +

+
+ {posts.length} {posts.length === 1 ? 'deel' : 'delen'} +
+
+ + {/* Posts List */} + {posts.length === 0 ? ( +

+ Nog geen posts in deze serie. +

+ ) : ( +
+ {posts.map((post, index) => ( + +
+ {index + 1} +
+
+

+ {post.frontmatter.title} +

+

+ {post.frontmatter.description} +

+
+ + + {post.readingTime} min +
+
+ + + + + ))} +
+ )} +
+
+ ) +} + diff --git a/content/nl/blog/_series.json b/content/nl/blog/_series.json new file mode 100644 index 0000000..16ddfc7 --- /dev/null +++ b/content/nl/blog/_series.json @@ -0,0 +1,21 @@ +{ + "series": [ + { + "id": "ai-speedrun", + "title": "AI Speedrun", + "description": "4 weken, €200, 1 EPD. De reis van idee naar werkend prototype.", + "color": "teal", + "order": 1, + "status": "completed" + }, + { + "id": "cortex-intent", + "title": "Intent Driven Cortex", + "description": "De volgende stap: een AI-copilot die begrijpt wat je wilt.", + "color": "amber", + "order": 2, + "status": "active" + } + ] +} + diff --git a/content/nl/blog/ai-speedrun/week-1-kickoff.mdx b/content/nl/blog/ai-speedrun/week-1-kickoff.mdx new file mode 100644 index 0000000..fde5ca8 --- /dev/null +++ b/content/nl/blog/ai-speedrun/week-1-kickoff.mdx @@ -0,0 +1,18 @@ +--- +title: "Week 1: De Kickoff" +description: "Hoe bouw je een EPD in 4 weken? Het begin van de AI Speedrun reis." +date: "2024-11-15" +published: true +seriesOrder: 1 +tags: + - AI + - Development + - EPD +--- + +## De Start + +Placeholder voor je eerste AI Speedrun blogpost. + +Vervang deze content met je LinkedIn post. + diff --git a/content/nl/blog/cortex-intent/introductie.mdx b/content/nl/blog/cortex-intent/introductie.mdx new file mode 100644 index 0000000..47a69f3 --- /dev/null +++ b/content/nl/blog/cortex-intent/introductie.mdx @@ -0,0 +1,18 @@ +--- +title: "Introductie: Intent Driven Cortex" +description: "De volgende evolutie van het EPD: een AI-copilot die begrijpt wat je wilt bereiken." +date: "2025-01-01" +published: true +seriesOrder: 1 +tags: + - AI + - Cortex + - Intent +--- + +## Een Nieuwe Aanpak + +Placeholder voor je eerste Cortex Intent blogpost. + +Vervang deze content met je nieuwe serie over het intent-driven EPD. + diff --git a/content/nl/navigation.json b/content/nl/navigation.json index 759fbd6..66f04ee 100644 --- a/content/nl/navigation.json +++ b/content/nl/navigation.json @@ -1,6 +1,10 @@ { "logo": "AI SPEEDRUN", "links": [ + { + "label": "Blog", + "href": "/blog" + }, { "label": "Documentatie", "href": "/documentatie" diff --git a/docs/blogs/blog-setup-guide.md b/docs/blogs/blog-setup-guide.md new file mode 100644 index 0000000..4e18bb8 --- /dev/null +++ b/docs/blogs/blog-setup-guide.md @@ -0,0 +1,232 @@ +# Blog Setup Gids (ikbenlit.nl) + +Deze gids beschrijft hoe de blog-sectie van `ikbenlit.nl` is opgezet, zodat je dit kunt kopiëren naar een ander Next.js project. + +## 1. Architectuur Overzicht +De blog gebruikt een **Filesystem-based MDX** benadering: +- **Content**: Opgeslagen als `.mdx` bestanden in de folder `content/blog`. +- **Data Fetching**: Server-side functies lezen de bestanden en parsen de YAML frontmatter. +- **Rendering**: Gebruikt `next-mdx-remote` voor het renderen van MDX content met custom React componenten. +- **Styling**: Gebruikt Tailwind CSS met de `@tailwindcss/typography` plugin. + +## 2. Dependencies +Installeer de volgende pakketten in je nieuwe project: + +```bash +pnpm add next-mdx-remote gray-matter rehype-slug rehype-autolink-headings rehype-pretty-code remark-gfm +pnpm add -D @tailwindcss/typography @types/mdx +``` + +## 3. File Structure +Zorg voor de volgende directory structuur: + +```text +. +├── app/ +│ └── blog/ +│ ├── page.tsx # Blog overzichtspagina +│ └── [slug]/ +│ └── page.tsx # Individuele blogpost pagina +├── components/ +│ └── blog/ +│ ├── blog-list.tsx # Lijst weergave component +│ └── ... # Overige UI componenten +├── content/ +│ └── blog/ +│ └── mijn-post.mdx # Je blog artikelen +├── lib/ +│ ├── blog.ts # Data fetching logica +│ ├── mdx-components.tsx # MDX naar React mapping +│ └── types/ +│ └── blog.ts # TypeScript interfaces +└── tailwind.config.js # Tailwind configuratie (typography) +``` + +## 4. Kern Implementatie + +### A. Types (`lib/types/blog.ts`) +Definieer de structuur van een blogpost. + +```typescript +export interface Post { + slug: string + title: string + description: string + date: string + published: boolean + image?: string + tags?: string[] + content: string + url: string + readingTime: number +} + +export interface PostFrontmatter { + title: string + description: string + date: string + published?: boolean + image?: string + tags?: string[] +} +``` + +### B. Data Fetching (`lib/blog.ts`) +Deze functies lezen de bestanden van je harde schijf. + +```typescript +import fs from 'fs' +import path from 'path' +import matter from 'gray-matter' +import type { Post, PostFrontmatter } from './types/blog' + +const POSTS_DIRECTORY = path.join(process.cwd(), 'content/blog') + +export async function getAllPosts(): Promise { + const files = fs.readdirSync(POSTS_DIRECTORY).filter(f => f.endsWith('.mdx')) + + const posts = files.map((filename) => { + const slug = filename.replace('.mdx', '') + const fullPath = path.join(POSTS_DIRECTORY, filename) + const fileContents = fs.readFileSync(fullPath, 'utf8') + const { data, content } = matter(fileContents) + const frontmatter = data as PostFrontmatter + + return { + slug, + ...frontmatter, + content, + url: `/blog/${slug}`, + readingTime: Math.ceil(content.split(/\s+/).length / 200), + } as Post + }) + + return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) +} + +export async function getPostBySlug(slug: string): Promise { + const fullPath = path.join(POSTS_DIRECTORY, `${slug}.mdx`) + if (!fs.existsSync(fullPath)) return null + + const fileContents = fs.readFileSync(fullPath, 'utf8') + const { data, content } = matter(fileContents) + const frontmatter = data as PostFrontmatter + + return { + slug, + ...frontmatter, + content, + url: `/blog/${slug}`, + readingTime: Math.ceil(content.split(/\s+/).length / 200), + } +} +``` + +### C. MDX Componenten (`lib/mdx-components.tsx`) +Map HTML elementen naar je eigen gestylede componenten. + +```tsx +import type { MDXComponents } from 'mdx/types' +import Link from 'next/link' +import Image from 'next/image' + +export const mdxComponents: MDXComponents = { + h1: (props) =>

, + h2: (props) =>

, + p: (props) =>

, + img: (props) => {props.alt, + a: ({ href, ...props }) => , +} +``` + +## 5. Pagina Implementatie + +### Blog Overzicht (`app/blog/page.tsx`) +```tsx +import { getAllPosts } from '@/lib/blog' +import Link from 'next/link' + +export default async function BlogPage() { + const posts = await getAllPosts() + + return ( +

+

Blog

+
+ {posts.map(post => ( + +

{post.title}

+

{post.description}

+ + ))} +
+
+ ) +} +``` + +### Blog Post Pagina (`app/blog/[slug]/page.tsx`) +```tsx +import { getPostBySlug, getAllPosts } from '@/lib/blog' +import { MDXRemote } from 'next-mdx-remote/rsc' +import { mdxComponents } from '@/lib/mdx-components' +import { notFound } from 'next/navigation' +import remarkGfm from 'remark-gfm' +import rehypeSlug from 'rehype-slug' + +export default async function PostPage({ params }: { params: { slug: string } }) { + const post = await getPostBySlug(params.slug) + if (!post) notFound() + + return ( +
+
+

{post.title}

+

{post.description}

+
+ +
+ +
+
+ ) +} +``` + +## 6. Tailwind Configuratie (`tailwind.config.js`) +Vergeet niet de typography plugin toe te voegen: + +```javascript +module.exports = { + content: [ + './app/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './lib/**/*.{js,ts,jsx,tsx}', + ], + plugins: [ + require('@tailwindcss/typography'), + ], +} +``` + +## Tips voor Kopieren +1. **MDX Styling**: De `prose` class in de pagina zorgt for de basis styling. Pas de `mdxComponents` aan voor specifieke elementen. +2. **Afbeeldingen**: Zorg dat je een `public/images` folder hebt als je lokale afbeeldingen gebruikt in je MDX. +3. **Frontmatter**: Elk `.mdx` bestand moet beginnen met: + ```markdown + --- + title: Mijn Eerste Post + description: Een korte samenvatting + date: '2025-01-01' + published: true + --- + ``` diff --git a/docs/blogs/linkedin-series-cortex-v2.md b/docs/blogs/linkedin-series-cortex-v2.md new file mode 100644 index 0000000..2c92c4f --- /dev/null +++ b/docs/blogs/linkedin-series-cortex-v2.md @@ -0,0 +1,306 @@ +# 📱 LinkedIn Series: Cortex V2 - Het Intelligente EPD + +**Project:** AI Speedrun → Cortex V2 +**Totaal:** 11 posts + 4 optioneel +**Doelgroep:** Zorgprofessionals, ICT-managers, directie, psychiaters (70%) + Techies, leveranciers (30%) +**Toon:** Toegankelijk, concreet, herkenbaar - techniek waar nodig (duidelijk gelabeld) + +--- + +## Fase 0: De Brug (1 post) + +### Post 0: "We bewezen dat het kan. Nu de echte vraag." +**Doel:** Transitie van Speedrun naar nieuwe serie +**Doelgroep:** Iedereen die Speedrun volgde + nieuwe lezers + +**Inhoud:** +- Korte recap: 4 weken, 118k regels code, werkend EPD +- Maar: tijdens het bouwen realiseerde ik me iets +- Ik bouwde nog steeds een *traditioneel* EPD +- De vraag is niet "kan AI software bouwen?" maar "wat moeten we eigenlijk bouwen?" +- Teaser: intent-driven EPD concept +- Aankondiging: komende weken deel ik de volgende stap + +**Hook:** "118.000 regels code. En toch de verkeerde vraag beantwoord." + +--- + +## Fase 1: De Visie (3 posts) + +### Post 1: "Het probleem met EPD's is niet de techniek" +**Doel:** Herkenning creëren, probleem framen +**Doelgroep:** Verpleegkundigen, behandelaren, managers + +**Inhoud:** +- Scenario 1: Verpleegkundige loopt ronde, wond verzorgd → 3 losse acties nodig +- Scenario 2: Psychiater begint dienst → 15 dossiers openen om te checken wat er vannacht gebeurde +- Scenario 3: Psycholoog dicteert notitie over suïcidaliteit → systeem slaat alleen op, denkt niet mee +- Kern: EPD's zijn gebouwd rond data-opslag, niet rond zorgprocessen +- Behandelaren passen zich aan het systeem aan, niet andersom + +**Hook:** "Je wond is verzorgd. Nu nog 3 schermen, 2 menu's en een bevestigingsdialoog." + +--- + +### Post 2: "Wat als je EPD gewoon snapt wat je bedoelt?" +**Doel:** Intent-driven concept introduceren +**Doelgroep:** Breed - iedereen die met EPD werkt + +**Inhoud:** +- Voorbeeld: "Zeg Jan af en maak notitie dat hij griep heeft" +- Wat er nu gebeurt: systeem snapt het niet, of je moet 2 losse commando's geven +- Wat er zou moeten gebeuren: systeem herkent 2 acties, bevestigt, voert uit +- Dit is geen spraakherkenning - dit is *begrip* +- Technisch (kort): AI die intentie herkent, niet alleen woorden +- Vergelijking: het verschil tussen een rekenmachine en een collega + +**Hook:** "Eén zin. Twee acties. Nul extra kliks." + +--- + +### Post 3: "Een EPD dat meedenkt" +**Doel:** Proactiviteit concept introduceren +**Doelgroep:** Behandelaren, verpleegkundigen, regiebehandelaren + +**Inhoud:** +- Voorbeeld 1: Wondzorg genoteerd → "Wondcontrole inplannen over 3 dagen?" +- Voorbeeld 2: Nieuwe medicatie gestart → "Evaluatie inplannen over 2 weken?" +- Voorbeeld 3: Signalen van crisis → "Signaleringsplan bekijken?" +- Cruciale nuance: suggesties, geen verplichtingen +- Het systeem leert wat jij routinematig doet +- Grens: behulpzaam vs. Clippy ("Ik zie dat je een brief schrijft...") + +**Hook:** "Na 'wond verzorgd' zegt je EPD nu: niets. Wat als het meedacht?" + +--- + +## Fase 2: De Bouw (5 posts) + +### Post 4: "Snelheid waar het kan, intelligentie waar het moet" +**Doel:** Architectuur uitleggen (toegankelijk) +**Doelgroep:** ICT-managers, tech-geïnteresseerde behandelaren +**Label:** 🔧 *Iets technischer - maar relevant voor iedereen die snapt waarom systemen soms traag zijn* + +**Inhoud:** +- Probleem: AI is slim maar niet instant +- Oplossing: twee sporen + - Simpele commando's ("agenda vandaag") → direct, lokaal, <20ms + - Complexe vragen ("zeg af en noteer") → AI denkt mee, ~400ms +- Het systeem beslist zelf welk spoor +- Vergelijking: reflexen vs. nadenken (je trekt je hand terug van vuur zonder na te denken) +- Resultaat: snelheid waar het kan, intelligentie waar het moet + +**Hook:** "Soms moet je nadenken. Soms moet je gewoon snel zijn. Een goed systeem weet wanneer." + +--- + +### Post 5: "Waarom 'ik snap het niet' geen optie is" +**Doel:** Betrouwbaarheid en vertrouwen +**Doelgroep:** Iedereen die ooit "commando niet herkend" zag + +**Inhoud:** +- Frustratie: 2x "ik begrijp het niet" en je typt het wel met de hand +- Principe: het systeem doet *altijd* een poging +- Bij twijfel: vraag verduidelijking ("Bedoel je een notitie of een afspraak?") +- Bij meerdere opties: bied ze aan +- Nooit opgeven, altijd proberen te helpen +- Dit is hoe vertrouwen ontstaat: betrouwbaarheid + +**Hook:** "'Commando niet herkend.' Drie woorden die ervoor zorgen dat niemand spraak meer gebruikt." + +--- + +### Post 6: "Context is alles" +**Doel:** Context-awareness uitleggen +**Doelgroep:** Behandelaren, verpleegkundigen + +**Inhoud:** +- Voorbeeld: "Maak notitie voor hem" +- Probleem: wie is "hem"? +- Oplossing: het systeem weet: + - Welke patiënt je nu bekijkt (Marie) + - Welk scherm je open hebt (dossier) + - Wat je net deed (rapportage invoeren) +- "Hem" → Marie (want die staat open) +- "Morgen" → datum van morgen (niet "het woord morgen") +- Resultaat: natuurlijk praten, geen commando-syntax + +**Hook:** "'Maak notitie voor hem.' Jij weet wie je bedoelt. Eindelijk je EPD ook." + +--- + +### Post 7: "Van idee naar werkend systeem (met AI)" +**Doel:** Bouwproces delen +**Doelgroep:** ICT-managers, directie, leveranciers +**Label:** 🔧 *Voor de techneuten en beslissers* + +**Inhoud:** +- Hoe ik dit bouw: + 1. Denken: wat is het doel? (PRD) + 2. Ontwerpen: hoe ervaart de gebruiker dit? (FO) + 3. Bouwen: AI als partner, niet als vervanger (TO) +- Tools: Claude, Cursor, Supabase +- AI rol: code genereren, reviewen, debuggen +- Mijn rol: richting bepalen, beslissingen nemen, kwaliteit bewaken +- Niet magie - wel een nieuwe manier van werken + +**Hook:** "AI bouwt de code. Maar wie bepaalt wat er gebouwd wordt?" + +--- + +### Post 8: "De meedenkende collega: waar ligt de grens?" +**Doel:** Ethische overwegingen bespreken +**Doelgroep:** Behandelaren, managers, beleidsmakers + +**Inhoud:** +- Vragen die ik mezelf stel: + - Wanneer is een suggestie behulpzaam vs. opdringerig? + - Alert-moeheid: hoe voorkom je dat alles genegeerd wordt? + - Verantwoordelijkheid: wat als het systeem iets mist? + - Privacy: wat "weet" het systeem eigenlijk? +- Mijn aanpak: + - Start conservatief (alleen kritieke suggesties) + - Transparante redenering (waarom deze suggestie?) + - Gebruiker blijft in control +- Geen definitieve antwoorden - wel de juiste vragen + +**Hook:** "Een systeem dat meedenkt. Klinkt goed. Maar wanneer wordt het bemoeizucht?" + +--- + +## Fase 3: Demo & Reflectie (3 posts) + +### Post 9: "Het werkt. Kijk maar." +**Doel:** Bewijs leveren +**Doelgroep:** Iedereen + +**Inhoud:** +- Video/GIF van werkende demo +- Flow: gebruiker spreekt → systeem detecteert 2 acties → voert uit → bevestigt → suggereert vervolg +- Eerlijk over wat nog niet perfect is +- Uitnodiging: feedback welkom + +**Hook:** "Genoeg gepraat. Hier is het bewijs." + +**Bijlage:** Demo video (30-60 sec) + +--- + +### Post 10: "Waarom duurt alles 18 maanden?" +**Doel:** Status quo uitdagen +**Doelgroep:** Managers, directie, leveranciers + +**Inhoud:** +- Als dit solo te bouwen is, waarom duren features bij leveranciers jaren? +- Eerlijk: wat mist er nog? + - NEN7510 compliance + - Integraties met bestaande systemen + - Edge cases + - Support en onderhoud +- Maar: de kern werkt. Nu. +- Vraag: waarom experimenteren we niet meer? +- Geen leverancier-bashing - wel een uitnodiging tot dialoog + +**Hook:** "'Staat op de roadmap.' De vier woorden die innovatie in de zorg vertragen." + +--- + +### Post 11: "Wat ik leerde over de toekomst van EPD's" +**Doel:** Serie afsluiten, vooruitkijken +**Doelgroep:** Iedereen + +**Inhoud:** +- Learnings: + - Intent-driven werkt + - Context is cruciaal + - Proactiviteit is krachtig maar delicaat + - Het denken is het probleem, niet de techniek +- Wat nodig is voor productie: + - Compliance + - Integraties + - Gebruikerstesten +- Grotere visie (teaser): + - Geen schermen meer + - Agents die het werk doen + - Focus op de patiënt, niet het systeem +- Call to action: samenwerken, testen, of zelf beginnen met experimenteren + +**Hook:** "De toekomst van EPD's is niet een beter scherm. Het is geen scherm." + +--- + +## Optionele Posts (4) + +### Optioneel A: "De bug die me een avond kostte" +**Wanneer:** Als er een goede technische struggle-story is +**Doelgroep:** 🔧 Techies, builders + +**Inhoud:** +- Specifieke bug of uitdaging +- Hoe AI hielp (of juist niet) +- Wat ik leerde +- Relatable voor iedereen die ooit code debugde + +--- + +### Optioneel B: "Privacy en AI in de zorg" +**Wanneer:** Als het onderwerp opkomt in reacties +**Doelgroep:** Managers, directie, beleidsmakers + +**Inhoud:** +- Wat "weet" een AI-systeem? +- Waar gaat data naartoe? +- Hoe bouw je privacy-by-design? +- Geen juridisch advies - wel transparantie + +--- + +### Optioneel C: "Geen schermen, maar agents" +**Wanneer:** Als grotere visie resoneert +**Doelgroep:** Visionairs, early adopters + +**Inhoud:** +- Ephemeral UI: interfaces die ontstaan wanneer nodig +- Agents die taken uitvoeren +- De behandelaar focust op de patiënt +- Sci-fi? Nee, technisch nu al mogelijk + +--- + +### Optioneel D: "Wat kost dit eigenlijk?" +**Wanneer:** Als kosten-vraag vaak komt +**Doelgroep:** Managers, directie + +**Inhoud:** +- Breakdown: €200 budget, waar ging het heen? +- AI-kosten per interactie +- Vergelijking met traditionele development +- ROI-denken: wat kost tijd van behandelaren? + +--- + +## Planning & Ritme + +| Week | Posts | Focus | +|------|-------|-------| +| 1 | Post 0, 1, 2 | De brug + probleem + oplossing | +| 2 | Post 3, 4, 5 | Proactiviteit + architectuur + betrouwbaarheid | +| 3 | Post 6, 7, 8 | Context + bouwproces + ethiek | +| 4 | Post 9, 10, 11 | Demo + uitdaging + conclusie | + +**Frequentie:** 3x per week (ma-wo-vr of di-do-za) +**Optionele posts:** Inspelen op reacties en momentum + +--- + +## Technische Referenties (voor eigen gebruik) + +De volgende documenten bevatten de technische details: +- `architecture-cortex-v2.md` - Volledige architectuur +- `fo-cortex-intent-system-v2.md` - Functioneel ontwerp +- `to-cortex-v2.md` - Technisch ontwerp +- `prd-cortex-v2.md` - Product requirements +- `review-cortex-v2.md` - Review rapport + +**Let op:** Technische details alleen gebruiken waar relevant voor de post. Vertaal altijd naar concrete voorbeelden die behandelaren herkennen. + diff --git a/lib/mdx/blog.ts b/lib/mdx/blog.ts new file mode 100644 index 0000000..4e74300 --- /dev/null +++ b/lib/mdx/blog.ts @@ -0,0 +1,258 @@ +/** + * 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[] +} + +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 +} +