blog.patriciomarroquin.dev โ Personal Developer Blog
A modern, fast developer blog built with Astro, React, TypeScript, Tailwind 4, and an AI-powered content pipeline.
-
๐ Dark / Light mode
- Respects system preference (
prefers-color-scheme) - Manual toggle powered by a
data-themeattribute
- Respects system preference (
-
๐ฐ MDX-powered blog
- Articles live in
src/content/articles/*.mdx - Custom
BlogPost.astrolayout with consistent typography and spacing - Reading time, publication meta, tags, and category badges
- Articles live in
-
๐ Reading experience upgrades
- Reading progress bar (top of the article)
- Estimated reading time
- Text-to-speech mode: โListen / Stop listeningโ button that reads:
- Title
- Description
- Full article body
-
๐งโ๐ป Beautiful code blocks
- Shiki-based syntax highlighting via Astroโs markdown pipeline
- Custom
CodeBlock.astrowrapper for MDX - โTraffic lightโ header + language label + Copy button
- Optional
filename="yourfile.tsx"meta support in code fences
-
๐ Content calendar
src/pages/calendar.astro+ContentCalendarPage.tsx- Reads monthly plans from
content-plans/*.json - Shows:
- Planned vs Published articles
- Category color dots (๐ฅ Trending, ๐ Tutorial, ๐ฌ Deep Dive)
- Upcoming articles list
- Published entries link directly to
/blog/[slug]
-
๐ค AI-powered content pipeline
- Monthly content plan generator (JSON under
content-plans/) - Daily article generator (MDX under
src/content/articles/) - Uses OpenAIโs Responses API
- GitHub Actions open PRs so content can be reviewed before publishing
- Monthly content plan generator (JSON under
-
๐ Full-text search (Pagefind)
- Static search index generated after each build
- Client-side search modal (
SearchModal.tsx) with:- Debounced queries
- Keyboard navigation (โ / โ / โต / ESC)
- Recent searches stored in
localStorage
- Results show title, excerpt, category badge, and tags
-
๐ Production ready
- Astro static site generation
- Deployed on Vercel
- Vercel Web Analytics enabled
- SEO-friendly structure + sitemap
This project uses pnpm.
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production (static output)
pnpm build
# Preview the production build
pnpm previewpnpm lintTwo scripts handle content planning and generation.
Creates/updates a JSON file in content-plans/YYYY-MM.json with a schedule of articles for the month (category, title, slug, angle, outline, etc.).
pnpm generate:plan- Looks at the current month (e.g.
"2025-12") - Keeps existing months; doesnโt overwrite unrelated data
- Used by the Content Calendar page
Reads the monthโs plan, finds todayโs entry, and generates a full MDX article file (with frontmatter + body):
pnpm generate:article-
Output:
src/content/articles/YYYY-MM-DD-slug-from-plan.mdx -
Uses:
- Your outlined sections
- Code ideas
- Media ideas (images/diagrams)
-
Enforces:
-
Real, useful content (not fluff)
-
Multiple code snippets
-
Optional
filename="..."in code fences for better code block headers -
Final signature:
Until next time, happy coding ๐จโ๐ป โ Juan Patricio ๐
-
โ ๏ธ These scripts requireOPENAI_API_KEYin your.env.
(File names are suggestions; adjust if you named them differently.)
- Runs: monthly, on the 1st (e.g.
0 9 1 * *) - Does:
- Checks out the repo
- Runs
pnpm generate:plan - Commits the updated
content-plans/YYYY-MM.json - Opens a PR with a conventional commit-style message, e.g.:
feat(content): add content plan for 2025-12
- Runs: daily around midday (UTC / chosen TZ)
- Does:
- Checks out the repo
- Runs
pnpm generate:article - If a new article is generated:
- Commits the MDX file under
src/content/articles/ - Opens a PR, e.g.:
feat(article): add post for 2025-12-06
- Commits the MDX file under
This keeps you in the loop: AI proposes, you review & merge.
Search is powered by Pagefind and runs entirely on the client.
-
After
astro build, a post-build script runs:pnpm build # -> astro build # -> node scripts/build-search-index.mjs
-
scripts/build-search-index.mjs:-
Detects the correct static output directory:
- Uses
.vercel/output/staticwhen deployed on Vercel (Astro + Vercel adapter) - Falls back to
distwhen running locally
- Uses
-
Runs Pagefind against that directory:
npx pagefind --site "<output-dir>" -
Generates the search bundle and index under
/pagefind(includingpagefind.js), alongside other static assets.
-
-
The search modal dynamically loads the Pagefind client at runtime:
const pagefind = await import(/* @vite-ignore */ "/pagefind/pagefind.js");
-
It performs
pagefind.search(query)and maps each result to:urltitleexcerpt(HTML stripped to plain text)meta.categorymeta.tagsmeta.publish_date
-
UX features:
- Debounced search requests
- Keyboard navigation (โ / โ / โต / ESC)
- Loading and โno resultsโ states
- Recent searches stored in
localStoragewith a โClearโ option - Result cards showing title, excerpt, category badge, and tags
All searching happens client-side on top of the static HTML generated by Astro, so the site remains fast, statically hosted, and independent of external search services.
High-level overview of the Astro + React + MDX setup.
src/
โโโ components/
โ โโโ ui/ # Reusable UI primitives
โ โ โโโ Button.tsx
โ โ โโโ Tag.tsx
โ โ โโโ CodeContainer.tsx # React version (for non-MDX usage)
โ โ โโโ ThemeToggle.tsx
โ โโโ articles/
โ โ โโโ ArticleCard.tsx
โ โ โโโ ArticleCategoryBadge.tsx
โ โโโ layout/
โ โโโ Navbar.tsx
โ โโโ MobileNav.tsx
โ โโโ Footer.tsx
โโโ content/
โ โโโ articles/ # MDX blog posts
โ โโโ 2025-12-06-react-19-....mdx
โ โโโ ...
โโโ layouts/
โ โโโ BaseLayout.astro # Shared shell for pages
โ โโโ BlogPost.astro # Article layout (reading UX, TTS, etc.)
โโโ pages/
โ โโโ index.astro # Home / landing
โ โโโ blog/
โ โ โโโ index.astro # Blog index
โ โ โโโ [slug].astro # Article detail route
โ โโโ calendar.astro # Content calendar page
โโโ views/
โ โโโ HomePage.tsx # React "view" components
โ โโโ BlogIndexPage.tsx
โ โโโ ArticleListPage.tsx
โ โโโ ContentCalendarPage.tsx
โโโ hooks/
โ โโโ useReadingProgress.ts # Scroll โ progress bar value
โ โโโ useTheme.tsx # ThemeProvider + useTheme
โโโ utils/
โ โโโ index.ts # cn(), formatDate(), etc.
โ โโโ calendar.ts # getMonthMatrix(), helpers for calendar
โโโ styles/
โ โโโ global.css # Tailwind base + custom prose styles
โโโ types/
โ โโโ index.d.ts # Article & calendar types
โโโ env.d.ts # Astro env typingThereโs also:
content-plans/
โโโ YYYY-MM.json # AI-generated content plan per month
scripts/
โโโ generate-content-plan.mjs # pnpm generate:plan
โโโ generate-article-from-plan.mjs # pnpm generate:article| Technology | Purpose |
|---|---|
| Astro | Static site generator / HTML-first framework |
| React 19 | Interactive islands and UI components |
| TypeScript | Type safety |
| Tailwind CSS 4 | Utility-first styling (via @tailwindcss/vite) |
| Shiki | Syntax highlighting for MDX code blocks |
| Lucide React | Icon set for UI + calendar legend |
| Vercel | Hosting + analytics |
| OpenAI Responses API | Content planning & article generation |
| Pagefind | Static full-text search over generated HTML |
- Primary: Sky (
sky-500) - Neutrals: Zinc palette
- Categories:
- ๐ฅ Trending: Orange
- ๐ Tutorial: Sky Blue
- ๐ฌ Deep Dive: Purple
- Status:
- โ Published: Emerald badge
- โณ Coming soon: Neutral gray badge
- Sans:
Plus Jakarta Sans - Mono:
JetBrains Mono
Code blocks in MDX are rendered by CodeBlock.astro and Shiki.
Example with filename:
```ts filename="Profile.server.tsx"
// Profile.server.tsx
import { fetchUserProfile } from "../lib/api";
export default async function Profile() {
const user = await fetchUserProfile();
return (
<div>
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
```
- `language` is picked from the fence (`ts`, `tsx`, etc.)
- `filename` is parsed from the meta string and displayed in the header
- The copy button uses a delegated click handler + `navigator.clipboard`
---
## ๐ Content Types
```ts
export type ArticleCategory = "trending" | "tutorial" | "deep-dive";
export interface Article {
slug: string; // e.g. "react-19-biggest-features-i-am-excited-about"
title: string;
description: string;
category: ArticleCategory;
tags: string[];
date: string; // ISO date, e.g. "2025-12-06"
}
export interface CalendarArticle extends Article {
status: "planned" | "published";
}
On the calendar page:
- Published articles:
- Green โPublishedโ badge
- Clickable card โ navigates to
/blog/[slug]
- Planned articles:
- Gray โComing soonโ badge
- Non-clickable card
Dark mode is handled via data-theme on <html> plus a small React provider:
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}Flow:
- Check
localStoragefor a saved theme. - Otherwise, read
window.matchMedia("(prefers-color-scheme: dark)"). - Apply
data-theme="light" | "dark"on<html>. - Persist user choice when toggled.
- MDX support for article content
- Migrate to Astro for SSG and SEO
- Content calendar view
- AI-generated monthly plan + daily article script
- RSS feed generation
- Full-text search (Pagefind)
- Comments (Giscus)
- Related articles recommendations
- View counter per article
- Search filters (by category / tag)
This project is licensed under the MIT License. See the LICENSE file for details.
MIT ยฉ Juan Patricio Marroquรญn
Built with โ and ๐ from Lima, Peru.