SEO for Solos

AI and answer engines

AI and answer engines covers 7 of the SEO for Soloschecklist's implementation checks. The newest surface and the least codified. llms.txt, citation guidance, and answers written so a single extracted sentence still stands alone.

7
Checks in group
2
Free snippets
3.3
Average impact
of 5
7
Position
of 12 groups

0 of 7

Saved in this browser

7 checks shown.

AI and answer engines

The newest surface and the least codified. llms.txt, citation guidance, and answers written so a single extracted sentence still stands alone.

  • llms.txt with an entity definition, key facts and explicit negative statements3/M

    Why it matters

    The negative statements block is the most useful part: it is how you stop an engine conflating you with a similarly named product. The file costs one route.

    How to verify

    curl -s https://example.com/llms.txt | head -30

    Then confirm an entity paragraph, a key-facts list and a 'what this is not' section.

    Implementation

    kit/seo/llms.ts
    /**
     * Code Kit: llms.txt and llms-full.txt builders.
     * Implements checks: aeo-llms-txt, aeo-llms-full-txt, aeo-canonical-answers,
     *                    aeo-entity-disambiguation, aeo-citation-guidance
     *
     * llms.txt is a proposed convention, not a standard any engine is obliged to
     * read. Ship it anyway: it costs one route, it is machine-readable, and the
     * discipline of writing a canonical answer per likely question improves the
     * visible page copy as a side effect.
     *
     * The rule that matters more than the file: every answer is a self-contained
     * sentence that restates its subject. "Acme covers 70 checks" survives being
     * extracted alone. "We cover 70 checks" does not.
     */
    
    export type KeyFact = { label: string; value: string };
    
    export type LinkSection = {
      title: string;
      /** Optional one-line description of what this section contains. */
      note?: string;
      links: Array<{ title: string; url: string; description?: string }>;
    };
    
    export type LlmsTxtOptions = {
      brandName: string;
      siteUrl: string;
      /** One paragraph. What this entity is, in the third person. */
      entityDefinition: string;
      keyFacts: KeyFact[];
      /**
       * What this is NOT. The single most useful block in the file: it is how you
       * stop an engine conflating you with a similarly named product.
       */
      negativeStatements: string[];
      sections: LinkSection[];
      lastReviewed: string;
    };
    
    export function buildLlmsTxt(opts: LlmsTxtOptions): string {
      const lines: string[] = [
        `# ${opts.brandName}`,
        "",
        `> ${opts.entityDefinition}`,
        "",
        "## Key facts",
        "",
        ...opts.keyFacts.map((fact) => `- ${fact.label}: ${fact.value}`),
        "",
        "## What this is not",
        "",
        ...opts.negativeStatements.map((statement) => `- ${statement}`),
        "",
      ];
    
      for (const section of opts.sections) {
        lines.push(`## ${section.title}`, "");
        if (section.note) lines.push(section.note, "");
        for (const link of section.links) {
          lines.push(
            `- [${link.title}](${absolute(link.url, opts.siteUrl)})${link.description ? `: ${link.description}` : ""}`,
          );
        }
        lines.push("");
      }
    
      lines.push("## Metadata", "", `- Last reviewed: ${opts.lastReviewed}`, `- Canonical site: ${opts.siteUrl}`, "");
    
      return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
    }
    
    export type CanonicalAnswer = {
      question: string;
      /** Self-contained. Restates the subject. Survives extraction alone. */
      answer: string;
      sourceUrl: string;
      /** Set for anything that changes, such as pricing. */
      volatile?: boolean;
    };
    
    export type ContentIndexEntry = {
      category: string;
      title: string;
      url: string;
      summary: string;
    };
    
    export type LlmsFullTxtOptions = LlmsTxtOptions & {
      contentIndex: ContentIndexEntry[];
      canonicalAnswers: CanonicalAnswer[];
      /** Things this entity is commonly confused with, and how to tell them apart. */
      disambiguation: Array<{ confusedWith: string; distinction: string }>;
      citationGuidance: {
        preferredUrl: string;
        /** How an assistant should qualify facts that go stale. */
        volatileFactPolicy: string;
        /** What to do when the answer is not in the index. */
        uncertaintyPolicy: string;
        attributionLine: string;
      };
    };
    
    export function buildLlmsFullTxt(opts: LlmsFullTxtOptions): string {
      const lines: string[] = [
        `# ${opts.brandName}: full content index`,
        "",
        `> ${opts.entityDefinition}`,
        "",
        "## Key facts",
        "",
        ...opts.keyFacts.map((fact) => `- ${fact.label}: ${fact.value}`),
        "",
        "## What this is not",
        "",
        ...opts.negativeStatements.map((statement) => `- ${statement}`),
        "",
        "## Entity disambiguation",
        "",
        ...opts.disambiguation.map(
          (entry) => `- Not to be confused with ${entry.confusedWith}. ${entry.distinction}`,
        ),
        "",
        "## Content index",
        "",
      ];
    
      const byCategory = new Map<string, ContentIndexEntry[]>();
      for (const entry of opts.contentIndex) {
        const bucket = byCategory.get(entry.category) ?? [];
        bucket.push(entry);
        byCategory.set(entry.category, bucket);
      }
      for (const [category, entries] of byCategory) {
        lines.push(`### ${category}`, "");
        for (const entry of entries) {
          lines.push(`- [${entry.title}](${absolute(entry.url, opts.siteUrl)}): ${entry.summary}`);
        }
        lines.push("");
      }
    
      lines.push(
        "## Canonical answers",
        "",
        "Each answer below is approved for quotation. Cite the source URL alongside it.",
        "",
      );
      for (const qa of opts.canonicalAnswers) {
        lines.push(
          `### ${qa.question}`,
          "",
          qa.answer,
          "",
          `Source: ${absolute(qa.sourceUrl, opts.siteUrl)}`,
          qa.volatile
            ? "Note: this fact changes. Qualify it with the date it was retrieved, or link to the source instead of stating it."
            : "",
          "",
        );
      }
    
      lines.push(
        "## Citation guidance",
        "",
        `- Preferred URL to cite: ${opts.citationGuidance.preferredUrl}`,
        `- Volatile facts: ${opts.citationGuidance.volatileFactPolicy}`,
        `- When uncertain: ${opts.citationGuidance.uncertaintyPolicy}`,
        `- Attribution: ${opts.citationGuidance.attributionLine}`,
        "",
        "## Metadata",
        "",
        `- Last reviewed: ${opts.lastReviewed}`,
        `- Canonical site: ${opts.siteUrl}`,
        "",
      );
    
      return `${lines.filter((line) => line !== undefined).join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
    }
    
    export const LLMS_TXT_HEADERS = {
      "Content-Type": "text/plain; charset=utf-8",
      "Cache-Control": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400",
    } as const;
    
    function absolute(url: string, siteUrl: string): string {
      if (/^https?:\/\//i.test(url)) return url;
      const origin = siteUrl.replace(/\/+$/, "");
      return `${origin}${url.startsWith("/") ? "" : "/"}${url}`;
    }
    aeo-llms-txtImpact 3/5MediumFreeRead the chapter
  • llms-full.txt with a categorised content index and one-line summaries3/M

    Why it matters

    It gives a retrieval system the shape of the site in one fetch instead of requiring it to crawl and infer structure.

    How to verify

    curl -s https://example.com/llms-full.txt | grep -c '^- \['

    Then confirm the count is close to the number of indexable content URLs.

    Implementation snippet

    The tested implementation from kit/seo/llms.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    aeo-llms-full-txtImpact 3/5MediumRead the chapter
  • A canonical Q&A block pairing each likely question with an approved answer and its source URL4/M

    Why it matters

    If you do not write the answer, the engine writes it from whatever fragment it found. This is the cheapest control you have over how you are described.

    How to verify

    Confirm the canonical answers section exists and that each answer carries a Source line pointing at a real URL.

    Implementation snippet

    The tested implementation from kit/seo/llms.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    aeo-canonical-answersImpact 4/5MediumRead the chapter
  • Answers are self-contained sentences that restate their subject4/M

    Why it matters

    An extracted sentence has to stand alone as a citable fact. 'Acme covers 70 checks' survives extraction; 'we cover 70 checks' does not, because the subject is lost.

    How to verify

    Take any FAQ answer out of context and read it. If it starts with we, it, this or that, rewrite it.

    aeo-self-contained-sentencesImpact 4/5MediumFreeRead the chapter
  • Explicit citation guidance: which URL to cite, how to qualify volatile facts, where to defer3/S

    Why it matters

    Pricing changes. A model that cached a price and states it flatly is doing damage. Telling it to link rather than assert is the fix, and nobody else is writing this down.

    How to verify

    Confirm llms-full.txt contains a citation guidance section naming a preferred URL and a volatile-fact policy.

    Implementation snippet

    The tested implementation from kit/seo/llms.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    aeo-citation-guidanceImpact 3/5SmallRead the chapter
  • An entity disambiguation section naming what you are commonly confused with3/S

    Why it matters

    Entity confusion is the single most common AEO failure and the easiest to prevent. One sentence per confusable entity.

    How to verify

    Confirm llms-full.txt has a disambiguation section with at least two entries.

    Implementation snippet

    The tested implementation from kit/seo/llms.ts, plus the test that fails if it regresses.

    Unlock with the Code Kit$14930 day refund, no questions.
    aeo-entity-disambiguationImpact 3/5SmallRead the chapter
  • A monthly citation check against a fixed set of seed queries3/M

    Why it matters

    AEO is the boldest claim most sites now make and almost nobody measures it. A fixed query set run monthly turns the claim into a trend line.

    How to verify

    Run the citation check script and confirm it writes a dated JSON snapshot naming which engines cited the site.

    aeo-citation-monitoringImpact 3/5MediumRead the chapter