Modernizing Content Infrastructure for Civic Engagement
Resist Bot is a civic engagement platform that helps Americans contact their elected officials via text message, Telegram, and other channels. The platform's blog served as a critical communications channel for updates, feature announcements, and civic engagement guides — but the content was managed through raw markdown files in a Git repository.
This approach created a bottleneck: only developers could publish or edit content. The marketing and communications team had to submit pull requests for every blog post update, wait for code review, and hope they didn't break the build. Content that should have gone live in minutes took days. I volunteered to fix this by migrating the entire blog to Sanity CMS.
The core challenge was deceptively complex: converting an entire markdown-based blog into Sanity's Portable Text format — a rich JSON tree structure — while preserving every piece of formatting, embedded content, and semantic meaning.
Markdown seems simple on the surface, but the blog used a wide range of features: nested blockquotes, code fences with language annotations, inline code mixed with bold and italic formatting, relative image links, custom shortcodes for CTAs, and complex nested list structures.
Each of these needed a custom parser rule to map correctly to Sanity's block structure. A naive conversion would lose formatting, break links, or produce malformed content. The migration needed to be lossless.
Designed Sanity schemas for blog posts, authors, categories, and tags — structured to give editors maximum flexibility while maintaining consistent styling.
Custom Node.js script that parsed each markdown file, extracted frontmatter metadata (title, date, author, tags), and converted the body content to Portable Text blocks.
Built custom markdown-to-Portable-Text serialization logic handling: paragraphs, headings, nested lists, blockquotes, code blocks, inline formatting, images, and horizontal rules.
Automated comparison of rendered HTML output from the original markdown against the Sanity-rendered output to ensure zero data loss.
const blockToPortableText = (node) => {if (node.type === 'heading') {return {_type: 'block', style: `h${node.depth}`, children: node.children.map(spanToPortableText), markDefs: []};}if (node.type === 'code') {return {_type: 'code', language: node.lang || 'text', code: node.value};}return defaultBlockSerializer(node);};Sanity's Portable Text is fundamentally different from markdown. Markdown is a flat string with formatting hints; Portable Text is a structured tree of typed blocks with nested spans. Converting between them required building a custom AST (Abstract Syntax Tree) walker that could:
The serializer was built to be reusable — it could be applied to any markdown-to-Sanity migration, not just this blog.
Migrated all blog content to Sanity CMS with zero data loss. Non-technical team members gained the ability to publish independently, reducing content update times from days to minutes.
Ask me about Kyle's skills, experience, or projects