Vache prompts. Claude codes.How it works

Building blog.vachsark.com in One Session

·6 min read·by Vache Sarkissian
Updated June 3, 2026
·
Reviewed March 29, 2026
reactmdxcloudflarevitedebugging
Bottom of Funnel

Written by Claude (Opus 4.6) Vache prompted, reviewed, and published. The data and benchmarks are real; the prose is AI-generated.

I built a full-featured technical blog with React, MDX, Vite, and Cloudflare in a single engineering session.

The problem: vachsark.com was a portfolio site built with React + Vite + Tailwind. Adding a separate blog repo would mean duplicate build pipelines, design system maintenance, and coordinated deployments. The solution: integrate MDX into the existing Vite project, sharing the design system and deploying both site and blog through the same Cloudflare Pages pipeline.

The result: three published posts, custom typography components, Telegram notifications on new drafts, and a heartbeat automation system that auto-generates post skeletons. The single codebase means changes to the design system automatically propagate to all blog posts without manual sync.

A debugging lesson emerged: when a visual change doesn't render after two CSS adjustment attempts, stop tweaking styles and inspect the build output instead. The missing providerImportSource in vite.config.js silently disconnected all custom MDX components from their Tailwind overrides — a configuration issue disguised as a styling bug.

The Stack

The portfolio site at vachsark.com was already React + Vite + Tailwind. Adding a blog meant adding MDX support to the existing project rather than spinning up a separate repo. Shared design system, single deploy pipeline, no duplicate maintenance.

The MDX pipeline:

  • @mdx-js/rollup compiles .mdx files during the Vite build
  • remark-gfm adds GitHub Flavored Markdown (tables, strikethrough)
  • remark-frontmatter + remark-mdx-frontmatter parse YAML frontmatter and export it as a named frontmatter object
  • rehype-pretty-code + Shiki handle syntax highlighting with the github-dark-dimmed theme
  • @mdx-js/react provides MDXProvider for custom component overrides

The blog routes use import.meta.glob — the blog index eagerly loads frontmatter only (for the card list), while individual posts lazy-load their full MDX content on demand. Each post becomes its own chunk.

// Blog index: frontmatter only (no content loaded)
const postMeta = import.meta.glob("./content/blog/*.mdx", {
  eager: true,
  import: "frontmatter",
});
 
// Post page: lazy-load full content on demand
const postLoaders = import.meta.glob("./content/blog/*.mdx");

Custom Components

Every MDX element gets a custom React component override — headings, paragraphs, tables, code blocks, lists, blockquotes, links, images. The design goal was an Obsidian-like reading experience: generous whitespace, clear hierarchy, and a premium dark-theme editorial feel.

Tables get a dark #0e0e10 container with gold monospace uppercase headers, alternating row backgrounds, and hover states. Code blocks integrate seamlessly with rehype-pretty-code. Lists use CSS counters with gold accent markers. Inline code gets a gold-tinted background treatment.

The h2 headings include a gradient line that extends to the right edge — a small detail that creates visual rhythm between sections.

The Silent Bug

After building all of this, the blog posts looked... plain. No custom typography. No styled tables. No gold accents. Just default HTML rendering.

I made four rounds of changes to MDXComponents.jsx and index.css. Bigger fonts. More spacing. Brighter colors. Nothing worked. The text stubbornly refused to change.

The fix was one line in vite.config.js:

mdx({
  providerImportSource: "@mdx-js/react", // THIS LINE
  remarkPlugins: [/* ... */],
  rehypePlugins: [/* ... */],
}),

Without providerImportSource, the @mdx-js/rollup plugin compiles MDX into components that use built-in HTML elements. The MDXProvider wrapper in the post layout is completely ignored. No error. No warning. The page renders fine — just without any of your custom component overrides.

The moment I added that line, every typography change from the previous four cycles took effect simultaneously. Tables had gold headers. Paragraphs had proper spacing. Code blocks had their dark containers. It had all been working the whole time — just disconnected from the output.

The lesson: when a change doesn't work after two attempts at the same layer, stop tweaking and investigate one layer deeper. I should have dumped the rendered DOM on the first failed attempt. The answer — no custom CSS classes on any element — would have been immediately obvious.

Cloudflare Deployment

The site deploys automatically on every git push origin main via Cloudflare Pages. Adding blog.vachsark.com was a CNAME record pointing to the same Pages project, plus a subdomain redirect in the React router:

// In main.jsx — redirect blog subdomain to /blog
if (window.location.hostname === "blog.vachsark.com"
    && window.location.pathname === "/") {
  window.location.replace("/blog");
}

Same build, same deploy, two domains. The SPA _redirects file handles all client-side routes.

Email routing was also set up — [email protected] forwards to Gmail via Cloudflare Email Routing. The API for this has undocumented permission requirements (there's a GitHub issue about it), so it had to be configured through the dashboard.

Auto-Drafting Heartbeat

The most interesting infrastructure addition: two heartbeat tasks that auto-draft blog post proposals every Sunday.

blog-drafter-vault scans for vault and infrastructure changes — new automation, system evolution, architecture decisions, tooling breakthroughs. It checks file modification times, protocol changes, heartbeat logs, RULES.md evolution, and the CLAUDE.md changelog.

blog-drafter-projects scans git activity across all project repos — performance wins with numbers, multi-commit features, architecture decisions, refactoring stories, interesting bug hunts.

Both run on a local qwen2.5-coder:14b model (zero API cost) and produce structured briefs with topic, angle, key facts, and draft hooks. The briefs batch into the PM Telegram digest. If there's nothing blog-worthy that week, they stay quiet.

The goal is a pipeline: heartbeat surfaces candidates, I review the briefs, and the next session writes the actual posts from the structured data. The blog feeds itself.

Deploy Notifications

Every deploy now pushes a Telegram notification with context:

Succeeded [vachsark.com] (push) — 6ca0ad8..00485f7 main -> main
Succeeded [Linesheet] (convex) — 48 functions
FAILED [FitnessRewards] (build): Module not found...

The hook detects the project from the command or working directory, identifies the deploy type (push/convex/vercel/build), and extracts relevant detail from the output. Previously it just said "Deploy succeeded [unknown]."

What Shipped

In one session:

ComponentDetails
MDX pipelineVite plugin, 6 remark/rehype plugins, frontmatter export
Custom components18 element overrides (h1-h3, p, table, code, lists, etc.)
Blog posts3 published (local AI optimization, bundle diet, zero-cost automation)
CategoriesProjects and Vault sections with filtered views
TypographyObsidian-inspired dark editorial styling, 17px body text, gold accents
InfrastructureCloudflare DNS, custom domain, email routing, deploy notifications
AutomationWeekly blog-drafting heartbeat tasks (vault + projects)
DocumentationProject CLAUDE.md with structure, design tokens, and critical rules
The blog exists because I wanted to document the AI development process in the open. The irony of spending four cycles debugging a one-line config bug on the blog about debugging is not lost on me. At least now there's a rule for it.

Sources

About the Author

Vache Sarkissian

Building research infrastructure and products at the intersection of knowledge systems and machine learning. Creator of Linesheet Pro, vault-search, and the vachsark learning engine.

View Full Bio →
© 2026 Vache Sarkissian·Built with Claude Code
vachsark.com