nyxcore-systems
5 min read

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!

TypeScriptNext.jstRPCPrismaGitHub APIAIContent GenerationMarkdownUI/UXFull-Stack

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.

typescript
// 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-markdown with remark-gfm in src/components/markdown-renderer.tsx to 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 our tailwind.config.ts.
  • Navigation & Accessibility:

    • We updated src/components/layout/sidebar.tsx and src/components/layout/mobile-nav.tsx to prominently feature "Projects," ensuring easy navigation across all screen sizes.

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 our GenerateSheet component.
  • The Symptom: TypeScript struggled to resolve properties like .data?.length and .data?.map(), inferring data as {} instead of the expected array of unblogged entries.
  • The Workaround: The solution was to define an explicit interface for UnbloggedEntry and then type the prop directly as { isLoading: boolean; data?: UnbloggedEntry[] }.
typescript
// 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!