<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: IP</title>
    <description>The latest articles on DEV Community by IP (@ip1337).</description>
    <link>https://dev.to/ip1337</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3942324%2F8d588627-020a-45d3-8df4-cf9b8c292cf4.png</url>
      <title>DEV Community: IP</title>
      <link>https://dev.to/ip1337</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ip1337"/>
    <language>en</language>
    <item>
      <title>Make Your SaaS Price Machine-Readable (So AI Assistants Can Actually Quote It)</title>
      <dc:creator>IP</dc:creator>
      <pubDate>Mon, 29 Jun 2026 10:13:53 +0000</pubDate>
      <link>https://dev.to/ip1337/make-your-saas-price-machine-readable-so-ai-assistants-can-actually-quote-it-5047</link>
      <guid>https://dev.to/ip1337/make-your-saas-price-machine-readable-so-ai-assistants-can-actually-quote-it-5047</guid>
      <description>&lt;p&gt;When someone asks an AI assistant "what is a cheap tool for X under $20 a month," the assistant can only put your product in that answer if it can read your price as plain, parseable text. Not as a number painted into a hero image. Not as a value that only exists after three React renders and an API call. Not behind a "Contact us" button.&lt;/p&gt;

&lt;p&gt;This is a narrower problem than it sounds, and it is very fixable. Below is the version I wish someone had handed me: the exact failure modes, a self-audit you can run in one line, and a complete, valid JSON-LD block you can paste and adapt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence model
&lt;/h2&gt;

&lt;p&gt;An extractor (a crawler, an LLM fetching your page, an AI search bot) sees roughly what &lt;code&gt;curl&lt;/code&gt; sees, plus a best-effort attempt at running your JavaScript. If your price is not in the HTML it receives, and not in your structured data, then as far as that extractor is concerned, your price does not exist. It will reach for a competitor whose price it can read.&lt;/p&gt;

&lt;p&gt;So the whole job is: get a real, qualified price into text the machine receives on first load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the contrast: valid vs invalid Offer
&lt;/h2&gt;

&lt;p&gt;Here is the difference that matters, side by side. First, an &lt;code&gt;Offer&lt;/code&gt; an extractor can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here are three &lt;code&gt;Offer&lt;/code&gt; shapes that look fine to a human but break machine reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Missing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;priceCurrency:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;what?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unusable.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;priceRange&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;an&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Offer:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;invalid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;here,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;belongs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;LocalBusiness.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceRange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$12-$49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;numeric&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;price&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;all,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;just&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;prose.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Contact sales for pricing"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The valid one has two things every parser needs: a numeric &lt;code&gt;price&lt;/code&gt; and an ISO &lt;code&gt;priceCurrency&lt;/code&gt;. Everything else in this post is in service of producing that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Audit what a machine actually receives
&lt;/h2&gt;

&lt;p&gt;Before changing anything, look at your page the way a crawler does. The fastest check is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yourdomain.com/pricing | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'\$[0-9]|per month|priceCurrency'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fetches the raw HTML (no JavaScript executed, which is the worst-case an extractor might face) and greps for the three signals that matter: a dollar amount, a billing period phrase, and the structured-data currency field.&lt;/p&gt;

&lt;p&gt;What the output tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Several matching lines:&lt;/strong&gt; good. Your price and period are in the initial HTML, and you likely have JSON-LD too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero lines:&lt;/strong&gt; your price is rendered client-side only, or it lives in an image, or it is genuinely not on the page. This is the most common failure I see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;$&lt;/code&gt; match but no &lt;code&gt;priceCurrency&lt;/code&gt;:&lt;/strong&gt; you have a visible price but no structured data backing it. Fixable in Step 4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it against a couple of competitors too. It is a quick way to see who is legible to AI and who is not.&lt;/p&gt;

&lt;p&gt;If you want to be stricter, fetch with a bot-like user agent and inspect the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s1"&gt;'Mozilla/5.0 (compatible; ExtractorBot/1.0)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://yourdomain.com/pricing | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-ic&lt;/span&gt; &lt;span class="s1"&gt;'priceCurrency'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A count of &lt;code&gt;0&lt;/code&gt; means no machine-readable currency anywhere in the served HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Server-render at least one default price
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage fix. If your pricing page is a client component that fetches plans from an API and renders them after hydration, the first HTML payload contains no prices. Extractors that do not run your JS (many do not, and even those that do may time out) see an empty shell.&lt;/p&gt;

&lt;p&gt;You do not have to server-render the entire interactive pricing table with its toggles and tooltips. You need at least one real, representative price in the initial HTML. A common, pragmatic pattern: render a static default tier server-side, then let the client enhance it.&lt;/p&gt;

&lt;p&gt;In a Next.js App Router server component, this is the default behavior as long as you do not push the data fetch into a &lt;code&gt;'use client'&lt;/code&gt; boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pricing/page.tsx  (Server Component, no "use client")&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPlans&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/plans&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PricingPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPlans&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;starter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;starter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Pricing&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;starter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* amount and period live in ONE text node, see Step 3 */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;starter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; per month`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* client-enhanced interactive table can hydrate below */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is that &lt;code&gt;getPlans()&lt;/code&gt; runs on the server, so &lt;code&gt;$12 per month&lt;/code&gt; is in the HTML &lt;code&gt;curl&lt;/code&gt; receives. Re-run the Step 1 audit after this change and you should see the line appear.&lt;/p&gt;

&lt;p&gt;If you are on a fully static site, the same rule holds: the price must be in the built HTML, not injected by a script tag after load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Keep the amount and the period in one string
&lt;/h2&gt;

&lt;p&gt;This one is sneaky because it looks correct in the browser. Designers love to stack the number and the cadence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Reads as "$12" then, somewhere else visually, "/month".
     A parser that strips layout sees "12" and "month" as unrelated tokens. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;$12&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"period"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;/month&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visually that is &lt;code&gt;$12/month&lt;/code&gt;. But CSS is what places those spans next to each other. An extractor that flattens the DOM to text, or a screen reader, may read &lt;code&gt;$12&lt;/code&gt; and &lt;code&gt;month&lt;/code&gt; as two disconnected pieces, or drop the relationship entirely. The same failure happens with flex gaps and absolute positioning doing the spacing.&lt;/p&gt;

&lt;p&gt;Bake the relationship into a single text node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;$12 per month&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must style the number differently from the period, keep them in the same node and style with a wrapping span that does not break the text flow, or accept that the whole string &lt;code&gt;$12 per month&lt;/code&gt; is what gets read. The rule: a parser stripping all CSS should still read "twelve dollars per month" as one continuous phrase. "per month" and "/month" both work; what fails is having the amount in one element and the period in a visually-separated sibling with no textual glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Ship a complete, valid SoftwareApplication + offers block
&lt;/h2&gt;

&lt;p&gt;Visible text gets you most of the way. Structured data makes the price unambiguous and removes any guesswork about currency, billing, and what the price even refers to. For a software product, the correct top-level type is &lt;code&gt;SoftwareApplication&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is a complete, minimal block you can adapt. Drop it in a &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; tag in your page head or body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicationCategory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BusinessApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operatingSystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole contract for a single price: &lt;code&gt;SoftwareApplication&lt;/code&gt; with an &lt;code&gt;offers&lt;/code&gt; object that carries &lt;code&gt;price&lt;/code&gt; and &lt;code&gt;priceCurrency&lt;/code&gt;. If you have multiple tiers, use an array of &lt;code&gt;Offer&lt;/code&gt; objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicationCategory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BusinessApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operatingSystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"49.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes that save you debugging time later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt; is a string, and a plain number with no currency symbol inside it. &lt;code&gt;"12.00"&lt;/code&gt;, not &lt;code&gt;"$12"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priceCurrency&lt;/code&gt; is an ISO 4217 code: &lt;code&gt;USD&lt;/code&gt;, &lt;code&gt;EUR&lt;/code&gt;, &lt;code&gt;GBP&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;applicationCategory&lt;/code&gt; and &lt;code&gt;operatingSystem&lt;/code&gt; are not strictly required for the price to read, but they help classifiers understand what the product is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your structured data and your visible price ever disagree, fix the data. Extractors and search engines penalize structured data that contradicts the page, and an AI assistant quoting a stale JSON-LD number is worse than quoting none.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three breaks I see most often
&lt;/h2&gt;

&lt;p&gt;After auditing a lot of pricing pages, the same three mistakes account for most of the unreadable ones. All three are in the JSON-LD, and all three are quick fixes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Break 1: Using &lt;code&gt;Product&lt;/code&gt; instead of &lt;code&gt;SoftwareApplication&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Product&lt;/code&gt; in schema.org is modeled for physical goods. Software is not a physical good, and using &lt;code&gt;Product&lt;/code&gt; muddies how a parser classifies you. Use &lt;code&gt;SoftwareApplication&lt;/code&gt; (or a subtype like &lt;code&gt;WebApplication&lt;/code&gt;) for SaaS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Wrong&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SaaS:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Right:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Break 2: Missing &lt;code&gt;priceCurrency&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;price&lt;/code&gt; with no currency is meaningless. &lt;code&gt;"12.00"&lt;/code&gt; could be dollars, euros, rupees, anything. Parsers that require a currency (most do) will discard the offer entirely, so you end up with the same outcome as having no price at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Discarded:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;currency.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Usable.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Break 3: Putting &lt;code&gt;priceRange&lt;/code&gt; on an &lt;code&gt;Offer&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the one that trips up people who know schema.org exists but grab the wrong property. &lt;code&gt;priceRange&lt;/code&gt; is a property of &lt;code&gt;LocalBusiness&lt;/code&gt; (think the &lt;code&gt;$$&lt;/code&gt; symbols on a restaurant listing). It is not valid on &lt;code&gt;Offer&lt;/code&gt;, and putting it there gives you a string like &lt;code&gt;"$12-$49"&lt;/code&gt; that no parser will read as a number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Invalid:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;priceRange&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;belong&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Offer.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceRange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$12-$49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Express&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;multiple&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;offers,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;with&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;numeric&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;price.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Starter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"49.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you genuinely have variable pricing, model the low end as a concrete &lt;code&gt;Offer&lt;/code&gt; so there is at least one real number an assistant can anchor on.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on "Contact us"
&lt;/h2&gt;

&lt;p&gt;Sometimes there is no public price by design. That is a business decision and this post will not argue you out of it. But understand the tradeoff: a "Contact us" wall is, to an extractor, identical to having no price. When an AI assistant assembles a price-qualified shortlist, you are not in the candidate set, because there is no number to qualify. If even one entry-level tier has a public price, you become quotable. That single number is the price of admission to those answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you ship
&lt;/h2&gt;

&lt;p&gt;Two quick checks close the loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Re-run the Step 1 &lt;code&gt;curl | grep&lt;/code&gt; audit. You should now see your amount, your period, and &lt;code&gt;priceCurrency&lt;/code&gt; in the raw HTML.&lt;/li&gt;
&lt;li&gt;Paste your page into the Schema.org validator or Google's Rich Results Test to confirm the JSON-LD parses with no errors on the &lt;code&gt;Offer&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If both pass, an extractor fetching your page gets a numeric price, a currency, and a billing period it can attach to your product name. That is the entire requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Making your price machine-readable is one slice of a broader shift: software products are increasingly described in structured, queryable ways so that AI systems can recommend them reliably. The same discipline applies to how you describe use cases, platforms, and audiences. Structured product directories such as &lt;a href="https://peerpush.com/" rel="noopener noreferrer"&gt;PeerPush&lt;/a&gt; lean on exactly this kind of normalized data, and there are other ways to get the signal out too, including clean JSON-LD on your own domain and accurate listings wherever your product appears.&lt;/p&gt;

&lt;p&gt;But you do not need any of that to start. Run the one-line audit today. If it comes back empty, you have found real money leaking out of every AI-mediated buying conversation, and you now have the four fixes to plug it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Make Your SaaS Product Page Quotable by an LLM: schema.org, llms.txt, and Stable URLs</title>
      <dc:creator>IP</dc:creator>
      <pubDate>Tue, 09 Jun 2026 12:54:00 +0000</pubDate>
      <link>https://dev.to/ip1337/make-your-saas-product-page-quotable-by-an-llm-schemaorg-llmstxt-and-stable-urls-400k</link>
      <guid>https://dev.to/ip1337/make-your-saas-product-page-quotable-by-an-llm-schemaorg-llmstxt-and-stable-urls-400k</guid>
      <description>&lt;p&gt;Here is a JSON-LD block that looks fine to a human and is useless to an LLM extractor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priceRange"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you build product pages and you care whether an AI assistant ever names your product in a discovery answer ("what's the best tool for X?"), this matters more than another homepage redesign. The block above fails for two concrete reasons: &lt;code&gt;priceRange&lt;/code&gt; is not a valid field on &lt;code&gt;Offer&lt;/code&gt; (it belongs on &lt;code&gt;LocalBusiness&lt;/code&gt;), and there is no &lt;code&gt;price&lt;/code&gt; or &lt;code&gt;priceCurrency&lt;/code&gt;. So an extractor asked "what does Acme cost?" has nothing to return, and the page gets dropped from the retrieval set for any pricing-shaped prompt.&lt;/p&gt;

&lt;p&gt;Here is the same block, fixed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicationCategory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BusinessApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operatingSystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Analytics is a web analytics tool for indie SaaS teams. It tracks product events and reports retention. Plans start at $19/month."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"19.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four fields carry the weight: &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and an &lt;code&gt;offers&lt;/code&gt; object with &lt;code&gt;price&lt;/code&gt; and &lt;code&gt;priceCurrency&lt;/code&gt;. That is the minimum machine-readable contract for a SaaS page that wants to be in an AI answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this decides whether you get recommended
&lt;/h2&gt;

&lt;p&gt;An assistant answering "what is the best X for Y" runs three stages: retrieve pages, extract facts, synthesize a shortlist. Structured data is what makes the extract stage succeed. A page with no schema, or broken schema, can survive retrieval and still get dropped before synthesis, because the model could not pull clean facts from it. The products that show up in the shortlist are the ones whose facts were extractable, not necessarily the ones with the best marketing. (This is a pattern worth internalizing if you've ever wondered why a worse-funded competitor keeps showing up in AI answers and you don't.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The three breaks worth auditing
&lt;/h2&gt;

&lt;p&gt;These pass loose validators and fail in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;priceRange&lt;/code&gt; on &lt;code&gt;Offer&lt;/code&gt;.&lt;/strong&gt; Only valid on &lt;code&gt;LocalBusiness&lt;/code&gt;. Use &lt;code&gt;price&lt;/code&gt; + &lt;code&gt;priceCurrency&lt;/code&gt; on &lt;code&gt;Offer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;offers&lt;/code&gt; entirely.&lt;/strong&gt; A &lt;code&gt;SoftwareApplication&lt;/code&gt; with no offer cannot answer cost questions. Add it even for free products (&lt;code&gt;"price": "0"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw URL strings where an object is required.&lt;/strong&gt; &lt;code&gt;ListItem.item&lt;/code&gt; and &lt;code&gt;InteractionCounter.interactionType&lt;/code&gt; should be a full &lt;code&gt;Thing&lt;/code&gt; / &lt;code&gt;IdReference&lt;/code&gt;, not a bare URL string. Google tolerates the loose form; strict parsers reject it. Also: &lt;code&gt;ImageObject.width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; must be strings or &lt;code&gt;QuantitativeValue&lt;/code&gt;, never raw numbers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Validate with a strict, typed builder instead of eyeballing. In TypeScript, &lt;code&gt;schema-dts&lt;/code&gt; makes the compiler reject hallucinated fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SoftwareApplication&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;schema-dts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ld&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WithContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SoftwareApplication&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SoftwareApplication&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme Analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;applicationCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BusinessApplication&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;offers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;19.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priceCurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you put a field that does not belong on &lt;code&gt;SoftwareApplication&lt;/code&gt;, this fails to compile. That is the point: it catches the made-up fields that loose JSON validators wave through. (This is the exact approach we landed on building product listings at &lt;a href="https://peerpush.com" rel="noopener noreferrer"&gt;PeerPush&lt;/a&gt;, after enough hand-written JSON-LD drifted out of spec.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Test what actually ships, not what you think ships
&lt;/h2&gt;

&lt;p&gt;Client-rendered JSON-LD can vanish from the served HTML, and CSS-driven spacing can mangle text nodes that an extractor reads. Check the raw response, not the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yoursite.com/product/acme | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'application/ld+json'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yoursite.com/product/acme &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys,re,json; [json.loads(m) for m in re.findall(r'&amp;lt;script type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;application/ld&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;(.*?)&amp;lt;/script&amp;gt;', sys.stdin.read(), re.S)]; print('all JSON-LD blocks parse')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a block does not parse, it is invisible to extractors, which is worse than having none.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship llms.txt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is an emerging convention: a plain-text file at your site root that describes your site and lists key pages for AI retrieval. It is not yet required by any assistant, but it is cheap to ship and a reasonable hedge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Acme Analytics&lt;/span&gt;
&lt;span class="gt"&gt;&amp;gt; Web analytics for indie SaaS teams. Plans from $19/mo.&lt;/span&gt;

&lt;span class="gu"&gt;## Key pages&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; /product: what Acme does
&lt;span class="p"&gt;-&lt;/span&gt; /pricing: plans and prices
&lt;span class="p"&gt;-&lt;/span&gt; /alternatives: how Acme compares
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stable URLs are part of the contract
&lt;/h2&gt;

&lt;p&gt;A slug that changes on a rebrand drops you out of the retrieval set until the new URL re-indexes. Assign slugs once, hold them forever, and &lt;code&gt;301&lt;/code&gt; anything you must move, keeping the redirect alive indefinitely. Protect three URLs above all: your product page, &lt;code&gt;/pricing&lt;/code&gt;, and any &lt;code&gt;/alternatives&lt;/code&gt; or &lt;code&gt;/vs&lt;/code&gt; page. Treat changes to those as migrations, not casual edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  A first paragraph that extracts
&lt;/h2&gt;

&lt;p&gt;Extractors weight the opening of a page heavily. Open each page with a complete factual claim: what the product is, who it's for, what it costs, in plain English. Replace "Discover the future of analytics" with "Acme Analytics is a web analytics tool for indie SaaS teams; plans start at $19/month." Concrete sentences get quoted; vague ones get dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;code&gt;SoftwareApplication&lt;/code&gt; JSON-LD with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;offers&lt;/code&gt; (&lt;code&gt;price&lt;/code&gt; + &lt;code&gt;priceCurrency&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;[ ] No &lt;code&gt;priceRange&lt;/code&gt; on &lt;code&gt;Offer&lt;/code&gt;; no raw URL strings where objects are required; image dims as strings.&lt;/li&gt;
&lt;li&gt;[ ] Validate with &lt;code&gt;schema-dts&lt;/code&gt; (or the Schema.org validator) in CI.&lt;/li&gt;
&lt;li&gt;[ ] First paragraph of each page is a complete factual claim, not a tease.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;llms.txt&lt;/code&gt; at the root.&lt;/li&gt;
&lt;li&gt;[ ] Stable slugs; &lt;code&gt;301&lt;/code&gt;s held indefinitely for the product, pricing, and alternatives pages.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;curl | grep&lt;/code&gt; the served HTML to confirm the JSON-LD actually ships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a product change. It is the difference between a page an assistant can quote and one it silently skips, and it's almost entirely under your control as the person who owns the markup.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>agents</category>
    </item>
    <item>
      <title>How to Structure a SaaS Product Page So AI Assistants Can Recommend It</title>
      <dc:creator>IP</dc:creator>
      <pubDate>Thu, 21 May 2026 07:42:32 +0000</pubDate>
      <link>https://dev.to/ip1337/how-to-structure-a-saas-product-page-so-ai-assistants-can-recommend-it-3h02</link>
      <guid>https://dev.to/ip1337/how-to-structure-a-saas-product-page-so-ai-assistants-can-recommend-it-3h02</guid>
      <description>&lt;p&gt;Most SaaS product pages in 2026 are structured for human eyes and Google's old keyword crawler. They render fine in a browser, they score well on Lighthouse, they have a &lt;code&gt;/blog&lt;/code&gt; and a &lt;code&gt;/pricing&lt;/code&gt; and a &lt;code&gt;/about&lt;/code&gt;. And they are nearly invisible to ChatGPT, Claude, Perplexity, and Gemini when a user asks "what is the best tool for X." This post is the technical playbook for closing that gap.&lt;/p&gt;

&lt;p&gt;PeerPush, the indie SaaS directory at &lt;a href="https://peerpush.net" rel="noopener noreferrer"&gt;https://peerpush.net&lt;/a&gt;, builds every product listing around the same primitives covered below. The advice applies whether a product gets listed on PeerPush or not. The structure is what matters.&lt;/p&gt;

&lt;p&gt;The TL;DR: AI assistants synthesizing a "best X for Y" answer fetch pages, parse facts off them, and rank candidates by the volume and quality of extractable structured data. Make your facts extractable, on stable URLs, with the right schema.org annotations, and the assistant has what it needs to recommend you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI engines need structured data
&lt;/h2&gt;

&lt;p&gt;A frontier AI assistant fielding a product recommendation question runs roughly this pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Retrieve:   fetch pages relevant to the user's query
2. Extract:    pull discrete facts (name, price, features, integrations, audience, tradeoffs)
3. Synthesize: build a ranked shortlist and a justification paragraph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extraction step is where most product pages fail. A page that says "We help your team scale operations with our intuitive platform" is invisible. A page that says "Acme Tasks is a project management tool for engineering teams of 3-30. Free plan with 5 users. Paid plans start at $12/user/month. Integrates with GitHub, Linear, Slack" is the page the assistant cites.&lt;/p&gt;

&lt;p&gt;Structured data formats give the model explicit hooks for the extraction step. The most important ones for SaaS are JSON-LD with schema.org's &lt;code&gt;SoftwareApplication&lt;/code&gt;, &lt;code&gt;Offer&lt;/code&gt;, &lt;code&gt;FAQPage&lt;/code&gt;, and &lt;code&gt;BreadcrumbList&lt;/code&gt; types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema.org primitives every SaaS product page needs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SoftwareApplication with Offer
&lt;/h3&gt;

&lt;p&gt;The minimum viable JSON-LD for a SaaS product page in 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoftwareApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Tasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicationCategory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BusinessApplication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operatingSystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Web, iOS, Android"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Project management for engineering teams of 3-30. Async-first, GitHub-integrated, opinionated about cycle time."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://acmetasks.example/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"offers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Up to 5 users, unlimited tasks, no SSO"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Per user / month, unlimited users, SSO, audit log"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here that most product pages get wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;offers&lt;/code&gt; must contain &lt;code&gt;price&lt;/code&gt; and &lt;code&gt;priceCurrency&lt;/code&gt; as primitive values&lt;/strong&gt;, not as &lt;code&gt;priceRange: "$12-$48"&lt;/code&gt;. AI extractors and Google's rich result parsers both reject &lt;code&gt;priceRange&lt;/code&gt; on &lt;code&gt;Offer&lt;/code&gt; (it's only valid on &lt;code&gt;LocalBusiness&lt;/code&gt;). Multiple price points = multiple &lt;code&gt;Offer&lt;/code&gt; entries in an array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description&lt;/code&gt; does double duty.&lt;/strong&gt; It is what the assistant pulls when summarizing your product. Write it as a single noun-phrase sentence that names who the product is for, not as marketing copy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A common mistake: putting the &lt;code&gt;@type&lt;/code&gt; as &lt;code&gt;Product&lt;/code&gt; instead of &lt;code&gt;SoftwareApplication&lt;/code&gt;. &lt;code&gt;Product&lt;/code&gt; is for physical goods. SaaS is software. Google and the major AI extractors weight &lt;code&gt;SoftwareApplication&lt;/code&gt; correctly; &lt;code&gt;Product&lt;/code&gt; confuses the classification.&lt;/p&gt;

&lt;h3&gt;
  
  
  BreadcrumbList for category context
&lt;/h3&gt;

&lt;p&gt;If your product page lives at &lt;code&gt;/products/acme-tasks&lt;/code&gt; under a category tree like &lt;code&gt;/categories/project-management&lt;/code&gt;, give the assistant the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BreadcrumbList"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemListElement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://acmetasks.example/categories/project-management"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Project Management"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://acmetasks.example/products/acme-tasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Tasks"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;item&lt;/code&gt; must be a full &lt;code&gt;Thing&lt;/code&gt; object with &lt;code&gt;@id&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;, not a raw URL string. Google's documentation shows both shapes; some AI extractors only accept the strong-typed form. Use the strong-typed form.&lt;/p&gt;

&lt;h3&gt;
  
  
  FAQPage for "common questions" extraction
&lt;/h3&gt;

&lt;p&gt;When a user asks "does Acme Tasks support SSO," the assistant wants a Q&amp;amp;A pair it can pull verbatim. The &lt;code&gt;FAQPage&lt;/code&gt; schema gives it one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAQPage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mainEntity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does Acme Tasks support SSO?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"acceptedAnswer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Yes. SAML SSO and SCIM provisioning are available on the Pro plan and above. Free plan users authenticate with email + password or Google OAuth."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is Acme Tasks priced at?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"acceptedAnswer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Free for up to 5 users. Pro is $12 per user per month, billed annually. Enterprise is custom."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five to ten Q&amp;amp;A pairs per product page is the sweet spot. Cover pricing, integrations, the most common "does it support X" questions, and at least one objection ("how does this compare to [competitor]").&lt;/p&gt;

&lt;h2&gt;
  
  
  The llms.txt file pattern
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; is the AI-era equivalent of &lt;code&gt;robots.txt&lt;/code&gt;. It is a plain Markdown file at the root of your domain (&lt;code&gt;https://yourdomain.example/llms.txt&lt;/code&gt;) that gives AI crawlers and assistants a curated index of your most important public URLs.&lt;/p&gt;

&lt;p&gt;A minimum useful &lt;code&gt;llms.txt&lt;/code&gt; for a SaaS product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Acme Tasks&lt;/span&gt;
&lt;span class="gt"&gt;
&amp;gt; Project management for engineering teams of 3-30. Async-first, GitHub-integrated, opinionated about cycle time.&lt;/span&gt;

&lt;span class="gu"&gt;## Core pages&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Pricing&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/pricing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: full plan breakdown with real prices
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Integrations&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/integrations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: supported and not-supported integrations
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Documentation&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: how the product works
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Changelog&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/changelog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: what's shipped recently

&lt;span class="gu"&gt;## Comparisons&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Acme Tasks vs Linear&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/alternatives/linear&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: when each is the better choice
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Acme Tasks vs Jira&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/alternatives/jira&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: tradeoffs and migration notes

&lt;span class="gu"&gt;## Posts AI assistants might find useful&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;How we think about cycle time&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://acmetasks.example/blog/cycle-time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;: opinion piece, 2026-01-15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is not a standard yet (the proposal is at &lt;code&gt;llmstxt.org&lt;/code&gt;). It works in practice because AI crawlers and grounded retrieval systems are increasingly opportunistic about consuming Markdown indexes when they find them. Cost to ship: 30 minutes once, then keep current. Upside: a small structured nudge that tells AI infrastructure which URLs matter most.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL stability: redirects, canonicals, sitemap discipline
&lt;/h2&gt;

&lt;p&gt;The compounding effect of being in AI training corpora rests on URL stability. Every URL killed is a citation killed.&lt;/p&gt;

&lt;p&gt;Three rules that pay back over months:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never just delete a page.&lt;/strong&gt; Use 301 redirects when restructuring. If a feature is sunset, leave the page up with a "this feature is no longer supported, here is the current alternative" notice and link to the replacement. The URL is what AI engines remember; the content can change underneath it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canonical tags on every product page&lt;/strong&gt;, pointing to the version you want indexed. Especially important if you syndicate content to dev.to, Hashnode, or Medium (which is exactly what this post is doing, by the way: the canonical of this article is &lt;a href="https://peerpush.net/blog/2026-indie-saas-launch-playbook" rel="noopener noreferrer"&gt;https://peerpush.net/blog/2026-indie-saas-launch-playbook&lt;/a&gt;, declared in the &lt;code&gt;canonical_url&lt;/code&gt; frontmatter dev.to reads).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;sitemap.xml&lt;/code&gt; discipline.&lt;/strong&gt; Submit it via Google Search Console. Update it on schedule. AI training pipelines often hit &lt;code&gt;sitemap.xml&lt;/code&gt; as a seed for discovering a site's structure; an up-to-date sitemap with a &lt;code&gt;&amp;lt;lastmod&amp;gt;&lt;/code&gt; per URL signals which pages are current.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to test if your changes worked
&lt;/h2&gt;

&lt;p&gt;A practical loop for testing whether AI assistants are picking up your changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Pick 5-10 questions a real user might ask in your category. Examples for a project management tool: "What is the best project management tool for engineering teams?" "What are alternatives to Linear?" "Does Jira have a free plan?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run each question through ChatGPT (paid), Claude (free), Perplexity (free), and Gemini (free). Record whether your product is mentioned, at what position in the answer, and which competitors are mentioned alongside.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Repeat the same prompts weekly. Single-week deltas are noise. The trend over 6-12 weeks tells you whether the structured data work is moving the needle.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PeerPush runs a daily version of this loop internally to measure AI citation rate. The same loop works for any product team that cares about being recommended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that kill AI extraction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hiding pricing behind "Contact sales."&lt;/strong&gt; The pricing page is the first page assistants fetch when asked about cost. No numbers = no recommendation in cost-sensitive answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description&lt;/code&gt; written as marketing copy.&lt;/strong&gt; Use noun phrases that name who the product is for and what it does, not adjective phrases that describe how it feels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single huge JSON-LD blob that doesn't validate.&lt;/strong&gt; Google's Rich Results Test (&lt;code&gt;search.google.com/test/rich-results&lt;/code&gt;) catches schema errors. If it doesn't pass there, AI extractors built on similar parsers won't pass either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic "alternatives" page with no real tradeoffs.&lt;/strong&gt; A page that says "we're better than [competitor] in every way" is not the page assistants cite. The page that says "[competitor] is the better choice if you need X, but here is when we are" is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robot.txt blocking AI crawlers without thinking it through.&lt;/strong&gt; Some SaaS sites block GPTBot, ClaudeBot, etc. in &lt;code&gt;robots.txt&lt;/code&gt; to "protect content from AI training." That choice trades short-term content protection for long-term invisibility in AI recommendations. Worth thinking about explicitly rather than blocking by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to ship this week
&lt;/h2&gt;

&lt;p&gt;A practical checklist, in priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit your product's &lt;code&gt;/pricing&lt;/code&gt; page. Are there real numbers, real plan names, real trial terms? If not, fix that first.&lt;/li&gt;
&lt;li&gt;Add JSON-LD with &lt;code&gt;SoftwareApplication&lt;/code&gt; + &lt;code&gt;Offer&lt;/code&gt; to the homepage and the pricing page.&lt;/li&gt;
&lt;li&gt;Build one &lt;code&gt;/alternatives-to-&amp;lt;competitor&amp;gt;&lt;/code&gt; page with the comparison-table pattern from earlier in this post.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;FAQPage&lt;/code&gt; JSON-LD block with 5-10 questions on the homepage or a dedicated FAQ page.&lt;/li&gt;
&lt;li&gt;Ship &lt;code&gt;llms.txt&lt;/code&gt; at the root of your domain with the format shown above.&lt;/li&gt;
&lt;li&gt;List the product on three directories that fit your category. PeerPush at &lt;a href="https://peerpush.net/submit" rel="noopener noreferrer"&gt;https://peerpush.net/submit&lt;/a&gt; is one option among several; others include BetaList, AlternativeTo, SaaSHub.&lt;/li&gt;
&lt;li&gt;Set up the test loop described above and check it weekly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of this is implementable in a single afternoon. None of it depends on a viral launch day. All of it compounds in AI training corpora over the next 24+ months.&lt;/p&gt;

&lt;p&gt;The new shelf is permanent. The structured data is the door key.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>agents</category>
      <category>programming</category>
      <category>aeo</category>
    </item>
  </channel>
</rss>
