SEO for SaaS
Start
Components

React components

Render every block type with components you own, restyle any of them, and never break on a new one.

Articles arrive as typed blocks, so each part of an article is a component you control: restyle the callout, swap the call to action for your signup form, hand images to your framework's image component. The set below renders every block type with semantic HTML and one stylesheet.

Example article · every block type

How Google decides what to show for a search

Quick answer: Google finds pages by crawling, stores what it understands in an index, and ranks the indexed pages for each query by how well they answer it. Most of what you can influence happens in the last step.

Crawling, indexing, ranking

Google describes the process in three stages in its own guide to how Search works. A page that is never crawled cannot be indexed, and a page that is not indexed cannot rank.

  1. Crawling: discovering URLs and downloading pages.
  2. Indexing: reading a page and storing what it is about.
  3. Serving: choosing and ordering results for one query.
A team building a rocket together, a metaphor for launching a page
Every page starts at step one: being found.

What a crawler needs

  • A link to the page from somewhere it already knows.
  • A sitemap.xml listing the URL.
  • A server that answers with 200.

Signals that decide the order

SignalWhat it measuresWhat you control
RelevanceDoes the page answer this query?Topic and structure
QualityIs it trustworthy and original?Sources and depth
UsabilityDoes it load and read well?Your templates
Our goal is to organize the world’s information and make it universally accessible and useful.
Google’s mission statement
<meta name="robots" content="noindex">
How Google Search Works (in 5 minutes)

FAQ

How long does it take for a new page to appear in Google?

Anywhere from a few days to a few weeks. On our own sites the median is three days to the first impression.

Does submitting a sitemap guarantee indexing?

No. It helps Google find the URL; whether it is indexed depends on the page.

Install

npx shadcn@latest add https://seoforsaas.dev/r/article.json

You get components/seoforsaas/ and lib/seoforsaas/types.ts, as source files in your repository: no package to keep updated, no dependency beyond React (tested with React 19). The components import the types relatively, so keep the two folders side by side. Customize through components and CSS variables instead of editing the files, so reinstalling a newer version stays a clean overwrite.

Render

tsx
import { Article } from "@/components/seoforsaas/article";import { JsonLd } from "@/components/seoforsaas/json-ld";import "@/components/seoforsaas/article.css";<h1>{article.title}</h1><Article blocks={article.blocks} /><JsonLd data={article.seo.jsonLd} />

The title isn't a block: render it as the page's h1. Only video.tsx is a client component (click to play); everything else renders on the server, and without JavaScript the video's poster links to YouTube.

Replace a block

components merges over the defaults: you replace only what you pass, and each replacement receives its block, typed.

tsx
import Image from "next/image";import { Article } from "@/components/seoforsaas/article";import { safeHref } from "@/components/seoforsaas/rich-text";import type { CtaBlock, ImageBlock } from "@/lib/seoforsaas/types";function SignupCta({ block }: { block: CtaBlock }) {  const href = safeHref(block.buttonHref); // the same link check the defaults use  return (    <section className="signup-cta">      <h2>{block.heading}</h2>      {block.body && <p>{block.body}</p>}      {href && <a href={href}>{block.buttonLabel}</a>}    </section>  );}function ArticleImage({ block }: { block: ImageBlock }) {  // Set the size your layout needs.  return <Image src={block.url} alt={block.alt} width={1536} height={1024} />;}<Article blocks={article.blocks} components={{ cta: SignupCta, image: ArticleImage }} />

With next/image, allow the SEO for SaaS CDN in next.config: images: { remotePatterns: [new URL("https://media.seoforsaas.dev/**")] }.

Block types added later

A type the components don't know renders through components.unknown, which renders nothing. Your pages never break when we add one. To log unknown types in development:

tsx
import { Article } from "@/components/seoforsaas/article";<Article  blocks={article.blocks}  components={{    unknown: ({ block }) => {      if (process.env.NODE_ENV === "development") console.warn(`Unrendered block type: ${block.type}`);      return null;    },  }}/>

Theming

Everything is scoped under .sfs-article and driven by CSS variables. The defaults derive from currentColor, so the article reads well on light and dark pages as installed, and the stylesheet restores list markers and table borders a CSS reset removes.

your global CSS
.sfs-article {  --sfs-font: "Inter", sans-serif;  --sfs-heading-font: "Bricolage Grotesque", sans-serif;  --sfs-accent: #7c3aed;          /* links, quote rule, CTA */  --sfs-accent-contrast: #ffffff; /* text on the CTA */  --sfs-radius: 16px;  --sfs-space: 1.25em;            /* rhythm between blocks */}
VariableDefaultUsed by
--sfs-font, --sfs-heading-font, --sfs-mono-fontinherit, inherit, system monotext, headings, code
--sfs-text, --sfs-mutedcurrentColor, 65% of itbody, captions
--sfs-border, --sfs-surface14% and 5% of currentColortables, callouts, code
--sfs-accent, --sfs-accent-contrastteal, whitelinks, CTA
--sfs-radius, --sfs-space12px, 1.25emcorners, spacing

Every block

Each block has a page with a live render, its JSON and how to replace it: heading, paragraph, list, quote, code, image, callout, call to action, FAQ, table, video.