Documents

Rebuilding My Personal Blog with Next.js and FastAPI

10 min readFeb 21, 2026Feb 22, 2026

hr_blog 2.0

페이지 메인
페이지 메인

Overview

This is the story of building hrletsgo.me — the very blog you're reading right now. It took exactly one day: February 21, 2026. Everyone says AI makes things easy these days, but it's genuinely astonishing progress. I still remember pulling all-nighters for a week just to get a single component working four years ago...

Why build it from scratch?

  • My team lead showed me the blog he'd built — link — and, whether he meant it as a challenge or not, I took it as one and built my own.
  • The Force Graph visualization for document connections looked particularly interesting.
  • I wanted something I could use freely as both a portfolio and a technical blog.

Architecture

                    ┌─────────┐
                    │     Nginx     │ (리버스 프록시 + SSL)
                    └────┬────┘
                    ┌────┴────┐
              ┌───┴────┐  ┌──┴───┐
              │   Next.js   │  │ FastAPI  │
              │   Frontend  │  │ Backend  │
              └────────┘  └───┬──┘
                           ┌──────┴──┐
                     ┌───┴────┐  ┌──┴─────┐
                     │   Postgres  │  │    MinIO     │
                     │     (DB)    │  │   (Images)   │
                     └────────┘  └────────┘

Notes

Backend + Database

I designed a data model on top of PostgreSQL + SQLAlchemy and implemented a FastAPI REST API.

# Post 모델
class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    slug = Column(String, unique=True, index=True)
    title = Column(String, nullable=False)
    content = Column(Text, nullable=False)
    category = Column(String, nullable=False)
    tags = Column(ARRAY(String), default=[])
    read_time = Column(Integer)
    published = Column(Boolean, default=True)
    connections = Column(ARRAY(String), default=[])
    created_at = Column(DateTime, default=func.now())

I also implemented safe migrations using advisory locking:

# Advisory Lock으로 동시 마이그레이션 방지
async def safe_migrate(engine):
    async with engine.begin() as conn:
        await conn.execute(text("SELECT pg_advisory_lock(12345)"))
        try:
            await run_migrations(conn)
        finally:
            await conn.execute(text("SELECT pg_advisory_unlock(12345)"))

MinIO is integrated as image storage, so images embedded in Markdown are saved to an S3-compatible backend.
This combination comes naturally when building agent flows.

A database is always necessary, and defining it as an abstract class makes it easy for agents to consume. Agents can figure things out on their own, of course, but richer context never hurts.

Image handling is another area that always requires careful thought — there's a lot more to it than simply storing files.

Frontend

The frontend is built on Next.js 15 (App Router).

Key components:

  • DocsProvider: document data context management
  • DocsSidebar: per-category document list sidebar
  • TableOfContents: auto-generated table of contents from Markdown headings
  • MarkdownRenderer: syntax highlighting + image optimization

Force Graph Visualization

페이지 그래프
페이지 그래프

This was the most enjoyable part — visualizing inter-document relationships as a Force Graph.

// ForceGraph 컴포넌트
const ForceGraph = ({ posts }: { posts: Post[] }) => {
  const nodes = posts.map(p => ({ id: p.slug, label: p.title, category: p.category }));
  const links = posts.flatMap(p => 
    p.connections.map(c => ({ source: p.slug, target: c }))
  );
  
  // D3 force simulation
  const simulation = d3.forceSimulation(nodes)
    .force("link", d3.forceLink(links).id(d => d.id))
    .force("charge", d3.forceManyBody().strength(-100))
    .force("center", d3.forceCenter(width / 2, height / 2));
};

Infrastructure + Deployment

  • Nginx reverse proxy configuration (SSL, gzip, caching)
  • Docker Compose multi-service orchestration
  • PostgreSQL healthcheck + automatic restart
  • Admin login (simple password-based auth)

Bonus: Tistory Migration

I also wrote a script to automatically migrate posts from my old Tistory blog using an LLM. The script — batch_import_tistory_llm.py — uses an LLM to handle HTML → Markdown conversion and metadata extraction.

Tech Stack

CategoryTechnology
FrontendNext.js 15, React 19, TypeScript
BackendFastAPI, SQLAlchemy, Pydantic
DatabasePostgreSQL
StorageMinIO (S3-compatible)
ProxyNginx (SSL, reverse proxy)
DeployDocker Compose
GraphD3.js Force Layout
Tags
BlogNext.jsFastAPIPostgreSQLMinIOForceGraphDocker