Blog / Knowledge Base
Content publishing system for security thought leadership, threat research, and platform update announcements with in-memory post storage and client-side Markdown rendering.
Overview
The Blog module provides a content publishing system for ThreatOps marketing and knowledge-base articles. As a SOCaaS platform competing in the enterprise security market, ThreatOps needs a vehicle for thought leadership: publishing research on SOC automation, compliance frameworks, SIEM integration strategies, and platform announcements. This content builds brand authority, drives organic discovery, and gives prospects substantive reasons to engage with the platform before becoming customers.
The module is deliberately lightweight. Posts are stored in-memory as JSON, seeded with seven substantive cybersecurity articles at startup, and served through a REST API that the Next.js frontend consumes via two client-side rendered pages: a grid index and an individual post detail view.
Design Decision: In-Memory Storage
In-memory storage was chosen for the initial build to avoid schema migrations and keep the module self-contained. The module comment explicitly notes that a future iteration can persist to Postgres without changing the API contract. All state lives in a module-level dict populated from SEED_POSTS at import time.
What Was Proposed
- A content publishing system for security thought leadership, threat research, and platform update announcements
- Category-filtered browsing across topics such as SOC Operations, Threat Intelligence, Compliance, and Platform Updates
- Individual post detail views rendering full article content from Markdown-like text
- Author attribution, read-time estimates, publish dates, and tag metadata on every post
- Admin API capability to create and update posts programmatically
- SEO-friendly URL slugs as the primary post identifier for both API and frontend routing
- Pre-seeded launch content covering the core ThreatOps value propositions
What's Built Complete
- In-memory blog post store seeded with 7 full-length cybersecurity articles (600-900 words each) at startup
- List posts endpoint with category filter (case-insensitive) and skip/limit pagination
- Get single post by URL slug endpoint with published-only filtering
- Create post endpoint (POST) returning 201 with auto-generated UUID and timestamps
- PATCH update endpoint with partial field updates via
model_dump(exclude_unset=True)and auto-refreshedupdated_at - BlogPostCreate / BlogPostUpdate / BlogPostResponse Pydantic schemas
- Frontend blog index page with dynamic category filter pills and responsive post card grid (1/2/3 columns)
- Frontend post detail page with client-side Markdown rendering via
renderMarkdown()anddangerouslySetInnerHTML - Loading spinners (
Loader2), error banners, and empty-state messages - Category color-coding across index cards (7 categories with distinct color schemes)
- Post cards with 2-line clamped title, 3-line clamped excerpt, author, date, and read-time footer
- Tag chips rendered as grey badges on the detail page
- Published-only filtering via
_published_posts()helper -- draft posts never leak
Architecture
Self-Contained Module
The Blog module is entirely self-contained with no database dependencies. The backend router stores all state in a module-level _posts dictionary populated from SEED_POSTS at import time. The frontend consists of two Next.js App Router pages -- an index grid and a slug-based detail page -- both using "use client" with useState, useEffect, and useCallback. Data fetching uses the shared api client from @/lib/api-client.
Backend (FastAPI) Frontend (Next.js)
+-------------------------------+ +-------------------------------+
| blog.py | | /blog (page.tsx) |
| | | Fetch all posts |
| SEED_POSTS -> _posts dict | | Derive category list |
| _published_posts() helper | | Category filter pills |
| | | Post card grid (1/2/3 col) |
| GET /posts -> list |<---| Loading / Error / Empty |
| GET /posts/{slug} -> detail |<---| |
| POST /posts -> create | | /blog/[slug] (page.tsx) |
| PATCH /posts/{id} -> update | | Fetch single post by slug |
+-------------------------------+ | renderMarkdown() -> HTML |
| dangerouslySetInnerHTML |
| Tag chips, author, date |
+-------------------------------+
Routing
/blog/[slug]
Prerequisites
- FastAPI with
APIRouter,HTTPException,Query(already in requirements.txt) - Pydantic v2 for
BaseModel,Field, andmodel_dump(exclude_unset=True) - Python stdlib only:
uuid,datetime,typing-- no third-party packages - No database required -- in-memory store needs no PostgreSQL, SQLAlchemy, or Alembic
- Frontend requires
@/lib/api-client(sharedapi.get()wrapper), Next.js App Router (v13+) - Lucide React icons:
BookOpen,Clock,User,Tag,ArrowLeft,Loader2,ArrowRight - Router registration in
main.py:from app.routers import blog; app.include_router(blog.router)
Data Model
Blog Post (In-Memory Dict / Pydantic Schema)
| Field | Type | Description |
|---|---|---|
| id | str (UUID4) | Auto-generated on create. Used as PATCH route parameter. |
| title | str | Post title. Displayed in cards (2-line clamp) and as H1 on detail page. |
| slug | str | URL-safe identifier. Used as Next.js dynamic route segment and GET lookup key. |
| excerpt | str | 2-3 sentence summary shown on index cards (3-line clamp). |
| content | str | Full article body with Markdown syntax (## headings, **bold**, `code`, - lists). |
| category | str | Free-form string. Known values: SOC Operations, Compliance, Threat Intelligence, Platform Updates. |
| author | str | Default: "ThreatOps Team". Shown with User icon on cards and detail header. |
| read_time | str | Human-readable estimate (e.g., "6 min read"). Displayed in blue on detail page. |
| cover_image | Optional[str] | URL to cover image. All seed posts are null. Wired for future image hosting. |
| tags | list[str] | Keyword tags. Rendered as grey badge chips on the detail page. |
| published | bool | Controls visibility. Only true entries returned by list/get endpoints. Toggleable via PATCH. |
| published_at | str (ISO 8601) | Determines sort order on list endpoint (descending, newest first). |
| updated_at | str (ISO 8601) | Auto-refreshed to current UTC on every PATCH call. |
Pydantic Schemas
| Schema | Direction | Notes |
|---|---|---|
| BlogPostCreate | POST request body | Required: title, slug, excerpt, content, category. Defaults: author="ThreatOps Team", read_time="5 min read", tags=[] |
| BlogPostUpdate | PATCH request body | All fields optional. Includes published boolean for draft/publish toggle. |
| BlogPostResponse | All responses | Full post data including auto-generated id, published flag, published_at, updated_at timestamps. |
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /posts | List all published posts (newest first). Query params: category (case-insensitive filter), skip (int, default 0), limit (int, default 20). |
| GET | /posts/{slug} | Get single published post by URL slug. Returns 404 if not found or unpublished. |
| POST | /posts | Create new post. Auto-generates UUID, sets published=true, timestamps published_at and updated_at. Returns 201. |
| PATCH | /posts/{post_id} | Partially update post by UUID (not slug). Only supplied fields changed. Auto-refreshes updated_at. Returns 404 if unknown. |
Seed Content
Seven full-length cybersecurity articles (600-900 words each) are seeded at module load from SEED_POSTS in blog.py. All have published=true with fixed published_at dates spanning September 2025 through January 2026:
| Category | Title | Read Time | Date |
|---|---|---|---|
| SOC Operations | The Evolution of SOC Operations: Why Automation is No Longer Optional | 6 min | Dec 2025 |
| Compliance | Achieving FedRAMP High Compliance in Your Security Operations | 7 min | Nov 2025 |
| Threat Intelligence | Multi-SIEM Strategy: Integrating Sentinel, Splunk, and Beyond | 6 min | Nov 2025 |
| SOC Operations | AI-Powered Alert Triage: Reducing Alert Fatigue by 80% | 6 min | Oct 2025 |
| Compliance | Building a Compliance-First Security Operations Center | 7 min | Oct 2025 |
| Threat Intelligence | UEBA: Detecting Insider Threats Before They Strike | 6 min | Sep 2025 |
| Platform Updates | ThreatOps Platform Update: Real-Time Alert Streaming & SOAR Playbooks | 5 min | Jan 2026 |
UI Description
Blog Index Page (/blog)
Header with BookOpen icon (blue) and "ThreatOps Blog" H1 title, subtitle "Security insights, threat research, and platform updates" in slate-500. Error banner (red border card) conditionally rendered when the API call throws. Category filter bar with "All" pill plus dynamically-derived category pills from fetched posts. Active pill uses blue fill; inactive pills are white with slate border. Responsive post card grid (1/2/3 columns) where each card is a Next.js Link to /blog/{slug}. Card structure: color-coded category badge, title (2-line clamp, hover turns blue), excerpt (3-line clamp), footer divider with author, publish date, and read time. Loading state shows centered Loader2 spinner. Empty state shows "No posts found" centered text.
Post Detail Page (/blog/[slug])
Constrained to max-w-3xl for comfortable reading width. "Back to Blog" link with ArrowLeft icon routes to /blog. Post header block: category badge (blue), H1 title (text-3xl bold), meta row with User icon + author, Clock icon + formatted long date, read time in blue. Tag chips rendered as grey badges with Tag icon. Horizontal rule divider. Article body rendered via dangerouslySetInnerHTML from renderMarkdown(post.content) which applies regex replacements for H1/H2/H3 headings, bold, italic, inline code, list items, and paragraph breaks. Error/not-found state shows back link plus red error card.
Category Color Mapping
| Category | Style |
|---|---|
| Threat Intelligence | Red background, red text, red border |
| Detection Engineering | Blue background, blue text, blue border |
| Incident Response | Orange background, orange text, orange border |
| Cloud Security | Blue background, blue text, blue border |
| Compliance | Emerald background, emerald text, emerald border |
| Vulnerability Management | Yellow background, yellow text, yellow border |
| SOC Operations | Purple background, purple text, purple border |
| Default (others) | Slate background, slate text, slate border |
Source Files
| Component | Path |
|---|---|
| API Router | platform/api/app/routers/blog.py |
| Blog Index Page | platform/frontend/src/app/blog/page.tsx |
| Post Detail Page | platform/frontend/src/app/blog/[slug]/page.tsx |