A minimalist personal blog. Reflections on technology and artificial intelligence as tools in service of people.
Built with Bun, React 19, Vite 8, TypeScript, and Tailwind CSS 4.
- Bun 1.x — package manager and runtime
- Vite 8 — build tool and dev server
- React 19 — UI library with StrictMode enabled
- TypeScript 5.9 — strict mode
- Tailwind CSS 4 — utility-first styling
- react-helmet-async — per-page SEO meta tags
- react-markdown + remark-gfm + rehype-highlight — Markdown rendering with syntax highlighting
- Vitest 4 — unit testing
- Cloudflare Pages — deployment and hosting
bun install
bun run devThe dev server starts at http://localhost:8080.
| Command | Description |
|---|---|
bun run dev |
Start development server |
bun run build |
Generate RSS/sitemap, then production build |
bun run preview |
Preview production build locally |
bun run lint |
Run ESLint |
bun run test |
Run tests |
bun run test:watch |
Run tests in watch mode |
posts/
└── YYYY/
└── month/
└── post-name_DD_MM.md # Markdown posts (content in Spanish)
src/
├── components/ # Reusable components (BlogLayout, CodeBlock)
├── lib/ # Utilities (posts, formatters, theme, utils)
├── pages/ # Route pages (Index, PostPage, About, NotFound)
├── test/ # Test files
├── App.tsx # Router setup
├── main.tsx # Entry point
└── index.css # Global styles and CSS variables
scripts/
└── generate-feeds.ts # Build-time RSS and sitemap generation
public/
└── favicon.svg # Adaptive SVG favicon (dark/light)
Posts are individual Markdown files organized by year and month:
posts/2026/marzo/ia_colega_silencioso_14_03.md
Each file uses YAML frontmatter:
---
title: La IA como colega silencioso
slug: ia-colega-silencioso
date: 2026-03-14
time: 03:25:00 # optional, recommended when publishing multiple posts on the same day
---
Content in Markdown...Frontmatter notes:
titledoes not need quotes, even when it contains colons (:). The parser splits on the first:only.- Posts are ordered by
dateand, when present,time(most recent first).
To publish a new post, create the .md file in the appropriate folder and run a build. The system picks it up automatically.
Posts support Markdown images.
Store local images under:
public/images/posts/Reference them from within the post:
Remote images are also supported:
- Prefer local images for blog-owned content.
- Use simple, lowercase, hyphenated filenames.
- If a post uses multiple images, group them with prefixes or subdirectories inside
public/images/posts/.
The favicon is a minimalist SVG with a stylized "V" that adapts automatically to the OS theme using @media (prefers-color-scheme: dark) embedded in the SVG:
- Light mode:
#222stroke - Dark mode:
#cccstroke
index.html references favicon.svg as primary and favicon.ico as fallback.
Each page sets document.title via useEffect as the primary mechanism, since react-helmet-async does not reliably override the static <title> in index.html. The Helmet tag is kept for SEO/OG meta tags.
Format: {Post title} — vmhq for posts, vmhq for the index.
The project deploys to Cloudflare Pages:
- Build command:
bun run build - Output directory:
dist - SPA routing:
public/_redirects(/* /index.html 200) - Set
SITE_URLin the CF Pages dashboard to the production domain (e.g.https://vmhq.blog)
- Syntax highlighting — code blocks with per-language colors (light/dark), using
rehype-highlightwith tokens integrated into the blog's CSS variables - Copy button — overlay on hover over any code block, copies to clipboard with visual feedback
- Prev/next navigation — at the end of each post, links to the previous (older) and next (newer) post
The prebuild script runs before every build and generates:
public/rss.xml— RSS 2.0 feed (from.mdfiles)public/sitemap.xml— XML sitemap
SITE_URL is resolved from SITE_URL (set in CF Pages dashboard) or CF_PAGES_URL (auto), with no hardcoded domain. Falls back to localhost:8080 for local development.
The RSS feed includes an <image> block pointing to the site's SVG favicon.
MIT