SEO for Solos

Spec 001Technical SEO implementation

Audited from 2,100 production URLs

The SEO playbookfor Solo SaaS founders

SEO for Solos is the complete technical SEO playbook for a company of one: 112 checks, 43 schema types, and the TypeScript that implements them.

  • One-time payment
  • Lifetime updates
  • 30 day refund, no questions

/checklist · sorted by return on effort

112 checks

  • Every page has a self-referencing absolute canonicalmeta-canonical-absolute · impact 5/5 · effort SFree
  • Apply the strip-the-entity-name test before publishing a tierpseo-strip-the-name-test · impact 5/5 · effort SFree
  • Ship the foundation group before anything elseseq-foundation-first · impact 5/5 · effort SFree
  • One canonical host, with a permanent redirect from the otherfound-canonical-host · impact 4/5 · effort SFree
108 more, across 12 groups
meta-canonical-absolute Every page has a self-referencing absolute canonical ▪ pseo-strip-the-name-test Apply the strip-the-entity-name test before publishing a tier ▪ seq-foundation-first Ship the foundation group before anything else ▪ found-canonical-host One canonical host, with a permanent redirect from the other ▪ found-404-returns-404 A missing page returns a real 404 status, not a 200 with an error message ▪ meta-title-template A title template with a keyword-led default, not a brand-led one ▪ meta-canonical-normalised Canonical generation normalises slashes, case and query parameters ▪ crawl-private-routes-disallowed API, auth and account routes are disallowed for every allowed agent ▪ measure-indexation-vs-ranking You can distinguish an indexation problem from a ranking problem ▪ seq-two-week-plan With two weeks, do foundation, metadata, one schema graph and robots. Nothing else. ▪ seq-content-before-programmatic Do not build a programmatic tier before you have data worth templating ▪ fail-faq-schema-spam Watch for FAQ schema being treated as spam ▪ found-single-site-url One constant holds the site origin, and nothing else hardcodes it ▪ found-https-everywhere HTTPS everywhere, with HSTS and no mixed content

00Provenance

Where the numbers come from

SEO for Solos was produced by auditing a production codebase of roughly 2,100 indexable URLs using 42 schema.org types, with a 1,689 page programmatic tier. These are counts from that audit, not estimates.

2,100
Indexable URLs
audited
42
Schema types
in production
1,689
Tier-3 pages
programmatic
112
Checks
in the registry
82
Glossary terms
8
Frameworks

This site also scores itself against its own checklist and publishes the result, including the checks it fails. See the current score.

01The problem

You are the engineer, the marketer and the support desk. SEO gets whatever hours are left over, which is why it never gets finished.

The posts you have read are written for teams with an SEO hire. The mechanisms that matter go unwritten: bot economics, llms.txt, differentiated lastModified, entity gating.

“Add BreadcrumbList schema” is a sentence. A tested factory that composes into a sitewide graph with stable identifiers is a day of work a solo founder does not have.

02The artefacts

What you get

01

The playbook

20 chapters, 4 of them free in full. Every subsection is a principle, an implementation and a worked example with real numbers.

Read a free chapter

02

The checklist

112 checks across 12 groups, each with a command you can run. Free, no account, and genuinely useful empty.

Open it

03

The Code Kit

12 modules of tested TypeScript, from the schema graph composer to the programmatic page template. Every module has a test that fails if it regresses.

See the modules

04

The PDF

The same content, print-formatted, watermarked with your licence at download time. Generated from the same source as the reader, so the two cannot diverge.

See pricing

03AI search

AI search, covered properly

The part competitors do not have

Answer engines synthesise an answer and cite sources. Extraction replaces ranking, which means being unquotable is worth nothing regardless of where you rank. Three chapters cover it, and this site implements every one of them on itself.

The bot economics split

Not AI versus search. Whether a crawler can return a visit or a citation for the bandwidth it consumes. OpenAI, Google, Anthropic and Apple all use separate agents for training and for search, which is what makes a selective policy possible.

  • AllowOAI-SearchBot, PerplexityBot, ClaudeBot
  • DenyGPTBot, Google-Extended, CCBot
Read the free chapter →

llms.txt, written properly

Entity definition, key facts, explicit negative statements, a canonical answer per likely question, and citation guidance telling an assistant how to qualify a price that changes. Nobody else is writing the last part down.

04The Code Kit

What is inside the Code Kit

Framework-agnostic where feasible, Next.js first, fully typed, every module tested. This site consumes it for its own SEO: if the kit were not good enough for us, it would not be good enough to sell.

Code Kit modules
ModuleImplements
seo/constants.tsCanonical normalisation, Open Graph, Twitter cards
seo/schema/43 factories plus the @graph composer with @id anchors
seo/robots.tsBot-economics policy generator, search versus training split
seo/sitemap.tsRoute-group composer, differentiated lastModified, integrity report
seo/indexnow.tsBatch submission, timing-safe cron guard
seo/feeds.tsRSS from the sitemap, XML escaping, CDATA safety
seo/llms.tsllms.txt and llms-full.txt builders from typed config
pseo/gating.tsEntity gating against an authoritative list
pseo/facts.tsPer-entity figures, computed FAQs, honest fallbacks, sibling selection
pseo/generate.tsSnapshot generation with a shrink guard
config/headers.tsHSTS, nosniff, frame-deny, referrer, permissions, a real CSP
tests/Vitest suites for every module above

The schema graph composer, in full (136 lines)

This is the file, not an excerpt. It is the difference between 43 disconnected script tags and one entity asserted from every page.

kit/seo/schema/graph.ts
/**
 * Code Kit: the sitewide @graph composer.
 * Implements checks: schema-graph-anchors, schema-single-script, schema-no-duplicates
 *
 * One <script type="application/ld+json"> per page holding one @graph array.
 * Nodes cross-reference each other by @id instead of duplicating themselves,
 * which is what stops a site from shipping the same Organization object 2,000
 * times and lets a crawler resolve one entity across every page.
 */

import { compact } from "../constants";
import { organization, website, type OrganizationOptions, type WebSiteOptions } from "./organization";
import { product, type ProductOptions } from "./commerce";
import { type JsonLdObject, SCHEMA_CONTEXT, nodeId, ref } from "./types";

export type SiteGraphOptions = {
  siteUrl: string;
  organization: Omit<OrganizationOptions, "siteUrl">;
  website: Omit<WebSiteOptions, "siteUrl" | "publisher">;
  /** Omit on sites that do not sell a product. */
  product?: Omit<ProductOptions, "siteUrl">;
  /** Page-level nodes: WebPage, BreadcrumbList, Article, FAQPage, and so on. */
  additionalNodes?: Array<JsonLdObject | undefined>;
};

/**
 * Compose the sitewide graph.
 *
 * The three anchors are stable and cross-referenced:
 *   #organization  is the publisher of #website and the brand of #product
 *   #website       is the isPartOf target for every page node
 *   #product       is the thing being sold
 */
export function siteGraph(opts: SiteGraphOptions): JsonLdObject {
  const orgId = nodeId(opts.siteUrl, "#organization");

  const nodes: Array<JsonLdObject | undefined> = [
    organization({ siteUrl: opts.siteUrl, ...opts.organization }),
    website({ siteUrl: opts.siteUrl, publisher: ref(orgId), ...opts.website }),
    opts.product
      ? product({
          siteUrl: opts.siteUrl,
          ...opts.product,
          brandName: opts.product.brandName,
        })
      : undefined,
    ...(opts.additionalNodes ?? []),
  ];

  return {
    "@context": SCHEMA_CONTEXT,
    "@graph": dedupeById(nodes.filter(isPresent)) as unknown as JsonLdObject[],
  } as unknown as JsonLdObject;
}

/**
 * Build a page-level graph that references the sitewide anchors without
 * repeating them. Use on every page that is not the home page.
 */
export function pageGraph(opts: {
  siteUrl: string;
  nodes: Array<JsonLdObject | undefined>;
}): JsonLdObject {
  return {
    "@context": SCHEMA_CONTEXT,
    "@graph": dedupeById(opts.nodes.filter(isPresent)) as unknown as JsonLdObject[],
  } as unknown as JsonLdObject;
}

function isPresent<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}

/** Two nodes with the same @id in one graph is a validation error. Last write wins. */
function dedupeById(nodes: JsonLdObject[]): JsonLdObject[] {
  const seen = new Map<string, JsonLdObject>();
  const anonymous: JsonLdObject[] = [];
  for (const node of nodes) {
    const id = typeof node["@id"] === "string" ? node["@id"] : undefined;
    if (id) seen.set(id, node);
    else anonymous.push(node);
  }
  return [...seen.values(), ...anonymous];
}

/**
 * Collect every @id declared in a graph and every @id referenced by it, so a
 * test can assert that no reference dangles.
 *
 * The distinction that matters: an object carrying `@id` plus other properties
 * DECLARES a node, at any nesting depth. An object carrying nothing but `@id`
 * REFERENCES one. A nested ImageObject with its own `@id` is a declaration, and
 * treating it as a dangling reference would be a false positive.
 */
export function graphIdIntegrity(graph: JsonLdObject): {
  declared: Set<string>;
  referenced: Set<string>;
  dangling: string[];
} {
  const declared = new Set<string>();
  const referenced = new Set<string>();
  const nodes = (graph["@graph"] as unknown as JsonLdObject[]) ?? [];

  const walk = (value: unknown): void => {
    if (Array.isArray(value)) {
      for (const item of value) walk(item);
      return;
    }
    if (!value || typeof value !== "object") return;

    const obj = value as Record<string, unknown>;
    const keys = Object.keys(obj);
    const id = typeof obj["@id"] === "string" ? obj["@id"] : undefined;

    if (id) {
      if (keys.length === 1) referenced.add(id);
      else declared.add(id);
    }

    for (const [key, val] of Object.entries(obj)) {
      if (key === "@id") continue;
      walk(val);
    }
  };

  for (const node of nodes) walk(node);

  return {
    declared,
    referenced,
    dangling: [...referenced].filter((id) => !declared.has(id)),
  };
}

export { compact };

The bot economics policy generator (293 lines)

The crawler registry, the class split and the generator, with the reasoning as comments because people read robots.txt when evaluating a site.

kit/seo/robots.ts
/**
 * Code Kit: bot-economics robots policy.
 * Implements checks: crawl-robots-generated, crawl-bot-economics,
 *                    crawl-private-routes-disallowed, crawl-sitemap-declared
 *
 * The decision nobody makes deliberately: which crawlers get your content.
 * The useful split is not "AI bots" versus "search bots". It is whether a
 * crawler can send you traffic or a citation in return for the bandwidth.
 *
 *   Search crawlers      index you and send clicks.            Allow.
 *   Citation crawlers    fetch a page to answer a live query
 *                        and name the source.                  Allow.
 *   Training crawlers    build a model. No link, no click,
 *                        no attribution.                       Your call.
 *
 * Blocking a training crawler does not remove you from that company's answer
 * engine if its citation crawler is a separate user agent, which for OpenAI,
 * Anthropic, Perplexity and Google it currently is. That asymmetry is the whole
 * mechanism.
 */

export type CrawlerClass = "search" | "citation" | "training" | "seo-tool" | "archive";

export type CrawlerProfile = {
  userAgent: string;
  operator: string;
  class: CrawlerClass;
  /** Does allowing this crawler produce visits or citations? */
  sendsReferralTraffic: boolean;
  purpose: string;
  documentationUrl?: string;
};

/**
 * Crawler registry.
 *
 * Verified against each operator's published documentation. Recheck quarterly:
 * user agent names change, and new ones appear faster than anyone updates
 * robots.txt.
 */
export const CRAWLERS: CrawlerProfile[] = [
  {
    userAgent: "Googlebot",
    operator: "Google",
    class: "search",
    sendsReferralTraffic: true,
    purpose: "Google Search index. Also feeds AI Overviews.",
    documentationUrl: "https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers",
  },
  {
    userAgent: "Bingbot",
    operator: "Microsoft",
    class: "search",
    sendsReferralTraffic: true,
    purpose: "Bing index. Also the retrieval layer behind Copilot.",
    documentationUrl: "https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0",
  },
  {
    userAgent: "DuckDuckBot",
    operator: "DuckDuckGo",
    class: "search",
    sendsReferralTraffic: true,
    purpose: "DuckDuckGo index.",
  },
  {
    userAgent: "Applebot",
    operator: "Apple",
    class: "search",
    sendsReferralTraffic: true,
    purpose: "Siri and Spotlight suggestions.",
    documentationUrl: "https://support.apple.com/en-us/119829",
  },
  {
    userAgent: "OAI-SearchBot",
    operator: "OpenAI",
    class: "citation",
    sendsReferralTraffic: true,
    purpose: "ChatGPT search index. Links back to the source.",
    documentationUrl: "https://platform.openai.com/docs/bots",
  },
  {
    userAgent: "ChatGPT-User",
    operator: "OpenAI",
    class: "citation",
    sendsReferralTraffic: true,
    purpose: "Live fetch when a user asks ChatGPT about a specific URL.",
    documentationUrl: "https://platform.openai.com/docs/bots",
  },
  {
    userAgent: "PerplexityBot",
    operator: "Perplexity",
    class: "citation",
    sendsReferralTraffic: true,
    purpose: "Perplexity answer index. Cites sources inline.",
    documentationUrl: "https://docs.perplexity.ai/guides/bots",
  },
  {
    userAgent: "ClaudeBot",
    operator: "Anthropic",
    class: "citation",
    sendsReferralTraffic: true,
    purpose: "Claude retrieval and citation.",
    documentationUrl: "https://support.anthropic.com/en/articles/8896518",
  },
  {
    userAgent: "Claude-User",
    operator: "Anthropic",
    class: "citation",
    sendsReferralTraffic: true,
    purpose: "Live fetch when a Claude user asks about a specific URL.",
  },
  {
    userAgent: "Applebot-Extended",
    operator: "Apple",
    class: "training",
    sendsReferralTraffic: false,
    purpose: "Apple foundation model training. Opting out does not affect Siri.",
    documentationUrl: "https://support.apple.com/en-us/119829",
  },
  {
    userAgent: "GPTBot",
    operator: "OpenAI",
    class: "training",
    sendsReferralTraffic: false,
    purpose: "Model training corpus. Separate from OAI-SearchBot.",
    documentationUrl: "https://platform.openai.com/docs/bots",
  },
  {
    userAgent: "Google-Extended",
    operator: "Google",
    class: "training",
    sendsReferralTraffic: false,
    purpose: "Gemini training. Blocking it does not remove you from Search.",
    documentationUrl: "https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers",
  },
  {
    userAgent: "anthropic-ai",
    operator: "Anthropic",
    class: "training",
    sendsReferralTraffic: false,
    purpose: "Legacy training crawler identifier.",
  },
  {
    userAgent: "CCBot",
    operator: "Common Crawl",
    class: "training",
    sendsReferralTraffic: false,
    purpose: "Open crawl corpus used as training data by many labs.",
    documentationUrl: "https://commoncrawl.org/ccbot",
  },
  {
    userAgent: "Bytespider",
    operator: "ByteDance",
    class: "training",
    sendsReferralTraffic: false,
    purpose: "Training corpus. Widely reported to ignore robots.txt.",
  },
  {
    userAgent: "AhrefsBot",
    operator: "Ahrefs",
    class: "seo-tool",
    sendsReferralTraffic: false,
    purpose: "Backlink index. Allowing it makes your own backlink data visible to you and to competitors.",
  },
  {
    userAgent: "SemrushBot",
    operator: "Semrush",
    class: "seo-tool",
    sendsReferralTraffic: false,
    purpose: "Competitive index. Same trade as AhrefsBot.",
  },
  {
    userAgent: "ia_archiver",
    operator: "Internet Archive",
    class: "archive",
    sendsReferralTraffic: false,
    purpose: "Wayback Machine preservation.",
  },
];

export type RobotsPolicy = {
  /** Crawler classes permitted to crawl. */
  allowClasses: CrawlerClass[];
  /** Paths every permitted crawler is denied. */
  disallowPaths: string[];
  /** Absolute sitemap URLs. */
  sitemaps: string[];
  /** Canonical host, no scheme. Bing reads this; Google ignores it. */
  host?: string;
  /** Extra user agents to deny outright, beyond the class rules. */
  denyUserAgents?: string[];
  /** Emit the reasoning as comments. Leave on: people read this file. */
  annotate?: boolean;
  crawlers?: CrawlerProfile[];
};

export const RECOMMENDED_POLICY: Pick<RobotsPolicy, "allowClasses"> = {
  // Allow anything that can send a visit or a citation. Deny training-only
  // crawlers and commercial SEO indexes, which take bandwidth and return nothing.
  allowClasses: ["search", "citation"],
};

export type RobotsRule = {
  userAgent: string | string[];
  allow?: string | string[];
  disallow?: string | string[];
};

/** Structured output, for `app/robots.ts` in Next.js. */
export function robotsRules(policy: RobotsPolicy): RobotsRule[] {
  const crawlers = policy.crawlers ?? CRAWLERS;
  const allowed = crawlers.filter((c) => policy.allowClasses.includes(c.class));
  const denied = crawlers.filter((c) => !policy.allowClasses.includes(c.class));
  const denyList = [
    ...denied.map((c) => c.userAgent),
    ...(policy.denyUserAgents ?? []),
  ];

  const rules: RobotsRule[] = [
    // The wildcard rule governs every crawler not named below, including ones
    // that do not exist yet.
    { userAgent: "*", allow: "/", disallow: policy.disallowPaths },
  ];

  for (const crawler of allowed) {
    rules.push({ userAgent: crawler.userAgent, allow: "/", disallow: policy.disallowPaths });
  }
  for (const userAgent of denyList) {
    rules.push({ userAgent, disallow: "/" });
  }

  return rules;
}

/** Plain text output, for a static robots.txt or the generator tool. */
export function robotsTxt(policy: RobotsPolicy): string {
  const crawlers = policy.crawlers ?? CRAWLERS;
  const annotate = policy.annotate ?? true;
  const lines: string[] = [];

  if (annotate) {
    lines.push(
      "# robots.txt",
      "#",
      "# Policy: allow any crawler that can return a visit or a citation.",
      "# Deny crawlers that consume bandwidth and return neither.",
      "#",
    );
  }

  const emit = (rule: RobotsRule, comment?: string) => {
    if (annotate && comment) lines.push(`# ${comment}`);
    const agents = Array.isArray(rule.userAgent) ? rule.userAgent : [rule.userAgent];
    for (const agent of agents) lines.push(`User-agent: ${agent}`);
    for (const allow of toArray(rule.allow)) lines.push(`Allow: ${allow}`);
    for (const disallow of toArray(rule.disallow)) lines.push(`Disallow: ${disallow}`);
    lines.push("");
  };

  emit(
    { userAgent: "*", allow: "/", disallow: policy.disallowPaths },
    "Default policy for every crawler not named below.",
  );

  for (const crawler of crawlers.filter((c) => policy.allowClasses.includes(c.class))) {
    emit(
      { userAgent: crawler.userAgent, allow: "/", disallow: policy.disallowPaths },
      `${crawler.operator}: ${crawler.purpose}`,
    );
  }

  const denied = crawlers.filter((c) => !policy.allowClasses.includes(c.class));
  for (const crawler of denied) {
    emit(
      { userAgent: crawler.userAgent, disallow: "/" },
      `${crawler.operator}: ${crawler.purpose} No referral traffic.`,
    );
  }
  for (const userAgent of policy.denyUserAgents ?? []) {
    emit({ userAgent, disallow: "/" });
  }

  for (const sitemap of policy.sitemaps) lines.push(`Sitemap: ${sitemap}`);
  if (policy.host) lines.push(`Host: ${policy.host}`);

  return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
}

function toArray(value: string | string[] | undefined): string[] {
  if (!value) return [];
  return Array.isArray(value) ? value : [value];
}

05The writing

Read the actual writing

Free chapter, in full

This is the bot economics chapter, complete. Judge the quality before deciding anything.

Bot economics

The wrong question and the right one

The principle

The question people ask is "should I block AI bots?". It is the wrong question, because it groups together crawlers that do completely different things and returns completely different value.

The right question is per crawler: can this crawler return a visit or a citation in exchange for the bandwidth it consumes?

That splits the population three ways:

  • Search crawlers index you and send clicks. Allow.
  • Citation crawlers fetch a page to answer a live query and name the source. Allow.
  • Training crawlers build a model. No link, no click, no attribution. Your call, and it is a genuine business decision rather than a technical one.

Why the split works

Because the major operators use separate user agents for the two jobs. Blocking OpenAI's training crawler does not remove you from ChatGPT's search index, because that index is fetched by a different agent. The same holds for Google and for Anthropic. That asymmetry is the entire mechanism, and it means you can take the citations without donating the training corpus.

The crawlers, one by one

Search crawlers: allow

User agentOperatorWhat it does
GooglebotGoogleSearch index. Also feeds AI Overviews.
BingbotMicrosoftBing index, and the retrieval layer behind Copilot.
DuckDuckBotDuckDuckGoDuckDuckGo index.
ApplebotAppleSiri and Spotlight suggestions.

Blocking any of these is a decision to not exist in that surface. There is no upside unless you are under crawl-rate pressure, and the fix for that is a crawl-delay conversation with the operator, not a block.

Citation crawlers: allow

User agentOperatorWhat it does
OAI-SearchBotOpenAIChatGPT search index. Links back to the source.
ChatGPT-UserOpenAILive fetch when a user asks about a specific URL.
PerplexityBotPerplexityAnswer index. Cites sources inline.
ClaudeBotAnthropicRetrieval and citation.
Claude-UserAnthropicLive fetch for a specific URL.

These are the crawlers that make answer engine visibility possible. Blocking them is the AEO equivalent of blocking Googlebot, and a surprising number of sites do it by accident by using a blocklist copied from a blog post that predates the split.

Training crawlers: your decision

User agentOperatorWhat it does
GPTBotOpenAITraining corpus. Separate from OAI-SearchBot.
Google-ExtendedGoogleGemini training. Blocking it does not affect Search.
Applebot-ExtendedAppleApple model training. Blocking it does not affect Siri.
anthropic-aiAnthropicLegacy training identifier.
CCBotCommon CrawlOpen corpus used as training data by many labs.
BytespiderByteDanceTraining corpus. Widely reported to ignore robots.txt entirely.

The argument for allowing them: models trained on your content may describe your product more accurately, and the effect on brand presence in a model's parametric knowledge is real if unmeasurable.

The argument against: it is a pure cost. Bandwidth out, nothing back, no attribution, no link.

There is no correct answer here, only a decision. The recommended default in this playbook is to deny, because a cost with no measurable return is hard to defend, and because the citation crawlers remain allowed so answer engine visibility is unaffected.

Commercial SEO crawlers: usually deny

AhrefsBot and SemrushBot index your backlinks. Allowing them makes your backlink profile visible to you through their tools, and equally visible to your competitors. It is a genuine trade rather than an obvious call. If you do not pay for either tool, you are paying bandwidth for a service you cannot use.

The implementation

typescript
export const RECOMMENDED_POLICY = {
  allowClasses: ["search", "citation"],
};

export function robotsTxt(policy: RobotsPolicy): string {
  // one block per allowed crawler with the full disallow list repeated,
  // one Disallow: / block per denied crawler,
  // then Sitemap and Host
}

Annotate the generated file. People read robots.txt as part of evaluating a site, and a file that explains its own reasoning is a small credibility signal that costs nothing:

text
# Default policy for every crawler not named below.
User-agent: *
Allow: /
Disallow: /api/

# OpenAI: ChatGPT search index. Links back to the source.
User-agent: OAI-SearchBot
Allow: /
Disallow: /api/

# OpenAI: Model training corpus. Separate from OAI-SearchBot. No referral traffic.
User-agent: GPTBot
Disallow: /

Score your site before spending anything

All 112 checks are free, with 26 implementation snippets unlocked. No account required.

Open the free checklist

06Pricing

Three tiers, all one-time payments

Checklist

Free

The full 112-check inventory, free, no account required.

Open the checklist

Playbook

$99

All 20 chapters, the watermarked PDF, and every checklist snippet.

Get the playbook

Playbook + Code Kit

Recommended

$149

Everything above, plus the tested TypeScript that implements it.

Get the playbook and code

$149is roughly two hours of a senior engineer's billing rate, paid once, against the monthly retainer an agency would quote a company of one. The schema graph composer alone is a day. Compare every tier.

07Fit

Who this is not for

  • If you cannot write code. Most of the value here is TypeScript you adopt into a repository. There is nothing to install.
  • If you have an SEO team already. Everything here is scoped to a company of one. A team that owns SEO full-time will know most of the checklist, though the Code Kit may still save it a sprint.
  • If you want rankings guaranteed. Nobody can offer that. This promises implementation completeness and says so in the terms.
  • If you want it done for you. Nothing here is a service. This is a playbook and code you adopt and ship yourself.
  • If you already score above 90 on the checklist. Go and find out first. It is free, and discovering you do not need this is a better outcome for both of us than a refund.

08Questions

Questions

What exactly is SEO for Solos?

SEO for Solos is a 20-chapter technical SEO playbook for solo SaaS founders, covering 112 implementation checks, 42 schema.org types and a working TypeScript code kit, taken from a production codebase of roughly 2,100 indexable URLs rather than from opinion. It exists so a one-person company can implement its own SEO once, without an agency retainer.

Who is SEO for Solos for?

SEO for Solos is written for solo SaaS founders and one-person product teams who do their own engineering. Every check is scoped to what one person can implement and verify alone, which is also why it sells code rather than a service: there is no agency retainer a company of one can sensibly carry.

Is this a subscription?

SEO for Solos is not a subscription. Every tier is a one-time payment that includes lifetime updates to the version purchased.

What framework does it assume?

SEO for Solos is Next.js App Router first, because that is what the audited codebase runs. Every chapter carries framework notes, and 8 frameworks have their own page with real per-framework code.

What if I already know most of this?

Score yourself on the free checklist before buying. SEO for Solos publishes all 112 checks with their verification commands at no cost, so you can find out what you are missing without paying for the answer.

Does it guarantee rankings?

SEO for Solos does not guarantee rankings and states plainly that no product can. It promises implementation completeness: you will know what is done, what is not, and what each remaining gap costs.

Can the Code Kit be used on client projects?

The SEO for Solos Code Kit covers unlimited projects owned by the licensee or the licensee's employer. An agency licence for client work is not offered, so client projects sit outside the licence.

Find out what you are missing first

All 112 checks and their verification commands are free, with 26 implementation snippets unlocked. Score your site before deciding whether the rest is worth $99.

No testimonials appear on this page because there are none yet. The numbers above come from the audit and are checkable. Invented social proof would not be.