How to Add a Blog to Your SaaS App (Without Building a CMS)

saas

Every SaaS company eventually needs a blog — for product announcements, guides, and the long tail of search traffic that compounds over years. The mistake most engineering teams make is treating "add a blog" as either a weekend WordPress install or a quarter-long internal CMS project. There is a third path that gets you a production blog on your own domain in an afternoon.

Decide where the blog lives: subdomain vs subdirectory

Before writing any code, settle the URL. You have three options:

  • blog.yourapp.com — a subdomain, usually pointing at a separate hosted platform.

  • yourapp.com/blog — a subdirectory of your main site, rendered by your own frontend.

  • A third-party domain — publishing on Medium or similar.

The subdirectory is the strongest option for SEO: every article builds authority for the domain your product lives on, internal links flow naturally between marketing pages and posts, and analytics stay in one property. A subdomain is workable; a third-party domain means the authority accrues to someone else entirely.

Choosing the subdirectory implies an architectural constraint: your own application has to render the posts. That rules out coupled platforms that insist on serving pages themselves, and points at an API-first setup — content authored in a dashboard, fetched as JSON, rendered by your frontend.

The three-layer architecture

A SaaS blog needs exactly three layers, and only one of them is your code:

  1. Authoring — a dashboard where posts are written: rich text editor, tags, categories, drafts, SEO metadata (title, description, open graph image), and scheduled publishing.

  2. Delivery — an API that serves published posts as JSON, with pagination, tag filtering, and search.

  3. Rendering — two routes in your app: a list page and a post page.

Building layers 1 and 2 yourself is where internal blog projects die: the editor alone is weeks of work, and then come image uploads, drafts, permissions, and slugs. A content backend gives you both layers as a service, so your entire project is layer 3.

Rendering: two routes and a fetch

With a typed SDK the rendering layer is small. The list page fetches a page of posts:

ts
const { data: posts } = await kookee.blog.list({ page: 1, limit: 10 });

The post page fetches by slug:

ts
const post = await kookee.blog.getBySlug(params.slug);

Render the body with your own components and your existing design system. In React you can go lower-effort still — hooks like useBlogPosts and useBlogPost handle loading states and pagination, and a content component renders the post body HTML for you. Or skip the markup entirely: server-rendered components render the post list, tag filter, pagination and post page from the data you fetch; see Build a blog with the React components.

Two rules keep quality high:

  • Render on the server. Use SSR or static generation so crawlers and readers get finished HTML. Client-only rendering works, but you give up the SEO you chose the subdirectory for.

  • Return real 404s. When a slug does not resolve, respond with status 404, not a styled "not found" page with status 200. Soft 404s pollute the index.

The metadata checklist

Each post page should emit, from data the CMS already stores:

  • <title> and meta description from the post's SEO fields.

  • A canonical URL pointing at the post's own path.

  • Open Graph and Twitter card tags, including the post image.

  • article:published_time and article:modified_time.

  • JSON-LD Article structured data.

Because these all derive from one API response, they can live in one helper function. Add an RSS feed and a sitemap entry per post — both are straightforward to generate from the same list endpoint your blog page already calls.

Tags, search, and the features you get for free

Once posts are structured data, features that would be projects in a homegrown system become query parameters:

  • Tag pages: kookee.blog.list({ tags: ['engineering'] }) behind a /blog/tags/:tag route.

  • Search: kookee.blog.list({ search: query }) behind a search box.

  • Comments and reactions: served from the same API if you want engagement on posts.

  • Translations: fetch a post's other locales when you expand internationally.

None of these require schema changes or migrations on your side — the backend already models them.

What this costs you

The honest accounting for the API-first path:

  • Your code: two routes, a metadata helper, a feed. A day of work in most frameworks, less if you start from your existing page templates.

  • Recurring: a content platform subscription — with usage-based free tiers (Kookee's includes 10,000 API requests a month), a typical blog runs on the free plan for a long time.

  • What you avoid: building and maintaining an editor, media storage, a publishing workflow, and a second deployed application.

Compare that to the self-hosted route — an initial build measured in weeks, then permanent ownership of security patches and dependency updates for a system that is not your product.

A concrete build order

  1. Create a project in your content backend and write one test post.

  2. Add /blog and /blog/:slug routes that fetch and render it server-side.

  3. Wire up the metadata helper: title, description, canonical, OG tags, JSON-LD.

  4. Add tag filtering and the RSS feed.

  5. Point your team at the dashboard and delete "build a blog" from the engineering backlog.

The blog itself is never the hard part. The hard part is making sure engineering effort goes into your product while marketing still gets a fast, on-brand, on-domain publication — and that is exactly what the split between a content backend and a thin rendering layer buys you.