From Code to Content: Unveiling Our AI-Powered Blog Generation Pipeline
We've just shipped a major update, bringing a seamless GitHub-to-blog post generation pipeline to life, complete with a beautiful, mobile-first UI and AI-driven content creation. Dive into how we built it!
It's been a whirlwind of a development session, but I'm thrilled to announce that we've reached a significant milestone! We've successfully implemented the complete GitHub → Memory Import → Blog Generation pipeline, wrapped in a sleek, project-based user interface that's fully responsive and ready for action.
Our goal was ambitious: to create a system where users could connect their GitHub repositories, select "memory files" (think documentation, notes, or structured data), and then, with a little AI magic, transform that raw information into polished blog posts. All within a cohesive, intuitive UI. And as of commit 49e98f3 on main, it's feature-complete!
The Vision: Bridging Raw Data and Published Content
The core idea behind this pipeline is to empower developers and teams to effortlessly turn their internal knowledge, project documentation, or even code comments into public-facing blog content. This means less manual copying, more consistent formatting, and a significant boost in content velocity.
To achieve this, we broke the challenge down into several key components:
1. The Data Foundation: Prisma & Database Schema
First, we extended our data model to support the new concepts of Project and BlogPost.
// prisma/schema.prisma
model Project {
id String @id @default(uuid())
name String
repoUrl String
// ... other project details
blogPosts BlogPost[]
user User @relation(fields: [userId], references: [id])
userId String
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String
}
model BlogPost {
id String @id @default(uuid())
title String
content String @db.Text
isPublished Boolean @default(false)
// ... other blog post details
project Project @relation(fields: [projectId], references: [id])
projectId String
}
After defining these, a quick prisma db push brought our database up to speed, ready to store all the new project and blog post data.
2. The GitHub Brain: Connecting to Your Repos
The heart of our data ingestion lies in the github-connector.ts service. We replaced a previous stub with a full-fledged BYOK (Bring Your Own Key) implementation. This service is now responsible for:
- Resolving GitHub tokens securely.
- Fetching a user's repositories.
- Checking for designated "memory paths" within those repos.
- Listing and fetching the content of specific memory files.
- Finally, syncing all this valuable information into our database, associating files with their respective projects.
This robust connector ensures that pulling content from GitHub is seamless and secure.
3. The AI Wordsmith: blog-generator.ts
This is where the magic happens! We ported our existing Python blog generation logic into a new TypeScript service: src/server/services/blog-generator.ts. Leveraging our existing AnthropicProvider, this service takes the raw content from your memory files and, guided by a sophisticated prompt, generates a blog post. It also includes parseFrontmatter() to structure the output with title, date, and other metadata, making it immediately ready for publication.
4. The API Layer: trpc/routers/projects.ts
To expose all this functionality to our frontend, we built a comprehensive tRPC router for projects. This router handles everything from project CRUD operations to the intricate GitHub integration points (github.repos, check, files, import) and all blog post-related actions (blogPosts.list, get, generate, generateBatch, update, delete, unblogged). It's the central nervous system connecting our frontend to the powerful backend services.
5. Bringing it to Life: The User Interface
A powerful backend is nothing without a great frontend. We focused heavily on creating an intuitive, mobile-first experience:
-
Project Management:
src/app/(dashboard)/dashboard/projects/page.tsx: A dashboard for listing projects with cards, counts, and badges, including a friendly empty state for new users.src/app/(dashboard)/dashboard/projects/new/page.tsx: A multi-step, scrollable form for creating new projects, featuring a GitHub repo selector, memory file checkboxes, and a toggle for immediate blog generation.src/app/(dashboard)/dashboard/projects/[id]/page.tsx: A detailed project page with tabbed navigation (Blog Posts / Sources / Settings), collapsible blog cards, and an inline markdown preview.
-
Blog Post Experience:
src/app/(dashboard)/dashboard/projects/[id]/blog/[postId]/page.tsx: The full blog post viewer and editor. It features a beautiful markdown rendering, an edit mode, and actions for publish/unpublish, regenerate, and delete, all anchored by a sticky bottom action bar for easy access.
-
Markdown Rendering:
- We integrated
react-markdownwithremark-gfminsrc/components/markdown-renderer.tsxto ensure our generated blog posts look fantastic. To give it a consistent and professional feel, we've applied nyx theme prose overrides via@tailwindcss/typography, which was added to ourtailwind.config.ts.
- We integrated
-
Navigation & Accessibility:
- We updated
src/components/layout/sidebar.tsxandsrc/components/layout/mobile-nav.tsxto prominently feature "Projects," ensuring easy navigation across all screen sizes.
- We updated
Lessons Learned: Navigating the tRPC Waters
No major feature goes live without a few interesting challenges. One particular hurdle involved tRPC's type inference when passing query results as component props.
- The Problem: We tried to use
ReturnType<typeof trpc.projects.blogPosts.unblogged.useQuery>as a prop type for ourGenerateSheetcomponent. - The Symptom: TypeScript struggled to resolve properties like
.data?.lengthand.data?.map(), inferringdataas{}instead of the expected array of unblogged entries. - The Workaround: The solution was to define an explicit interface for
UnbloggedEntryand then type the prop directly as{ isLoading: boolean; data?: UnbloggedEntry[] }.
// Define the expected shape of an unblogged entry
interface UnbloggedEntry {
id: string;
title: string;
// ... other relevant properties
}
// In the component props
interface GenerateSheetProps {
isLoading: boolean;
data?: UnbloggedEntry[]; // Explicitly type the data prop
// ... other props
}
Lesson: While ReturnType is incredibly powerful, for nested tRPC router queries passed directly as component props, explicit interface definitions can often provide more reliable and clearer type inference, saving you from TypeScript's more conservative assumptions.
What's Next?
With the core pipeline now fully operational, our immediate next steps involve rigorous testing and refinement:
- End-to-end testing of the entire flow, from project creation to blog post publication.
- Thorough mobile layout verification to ensure a flawless experience on small screens.
- Implementing robust error handling for edge cases like expired tokens or API rate limits.
- Adding loading skeletons to enhance the user experience during data fetches.
This achievement marks a significant step forward in our mission to streamline content creation. We're incredibly excited about the possibilities this new pipeline unlocks for turning raw knowledge into engaging narratives!