Litestore · Developer guide
Storefront

SEO

Understand how Litestore scores pages for search engines and AI assistants, and how renamed URLs keep redirecting.

Litestore checks how well each page is set up for search engines and for AI assistants, and it keeps old URLs working when you rename a slug. Read this page when you want to change a rule, a threshold or how the sitemap and feeds behave.

The SEO audit and the AIO report are both rule engines. Neither one calls an AI model. The source states that they produce facts you can check, not generated text.

Because they follow fixed rules, the same page gets the same score every time. Every lost point traces back to a named rule, and each rule comes with a hint on how to fix it.

Both engines are pure functions. They don't read the database, crawl pages or make network calls. They only judge data the app already has.

The SEO audit

lib/seo/audit.ts checks a page against fixed rules and gives it a score out of 100.

All thresholds are in one object, SEO_LIMITS, which makes them easy to tune:

titleMin        30
titleMax        60
descriptionMin  70
descriptionMax  160
bodyMin         50

bodyMin sets the minimum amount of page copy. If a page has fewer than 50 characters of copy, the audit counts it as thin.

Each issue has a severity, and the severity decides how many points it costs:

error    25
warning  10
info      3

seoScore subtracts the total penalty from 100 and never goes below zero. A page missing both its title and its description drops to 50 from those two errors alone.

auditSeo is the only function that decides whether a page's SEO is healthy. The checklist row on each product or page and the store-wide /admin/seo report both call it. They can never disagree, so a product's checklist can't show green while the report lists it as broken.

Checks that compare several pages are the exception, because the engine only sees one page at a time. The report finds duplicate titles, descriptions and body copy, and passes them into the engine as flags on the page.

The engine also knows when a rule doesn't apply. title_uncustomised fires when a meta title is just the item's name. A blog post's headline is meant to be its title, so the engine skips this rule for blog posts and applies it only to catalog items.

The seven checklist sections

The checklist sorts the issue codes into seven sections:

  • Missing metadata flags a missing title or description. It is the only section marked critical.
  • Title quality flags titles that are too short, too long or not customised.
  • Duplicate titles flags titles used on more than one page.
  • Descriptions flags descriptions that are too short or too long.
  • Page copy flags thin content.
  • URL slugs flags uppercase letters, underscores, whitespace or double hyphens.
  • Image previews flags a missing image, or an image without alt text.

When a score changes, you can always point to the rule that changed it.

Slug history

When you rename a slug, Litestore adds the old value to previousSlugs. The old URL then sends a permanent redirect. Even after several renames, the redirect goes straight to the current URL in one hop.

There is no separate redirect table. Each record with a slug stores its own history on its own row, so a chain of renames costs nothing extra:

/product/a  →  renamed to b  →  renamed to c

previousSlugs on the row: ["a", "b"]

/product/a  →  301  →  /product/c

slugHistoryPatch writes the history in the same update that changes the slug. A rename can never happen without its redirect.

If you rename back to an earlier value, Litestore removes that value from the history. After a → b → a, the slug a is current and redirects nowhere.

Litestore calls resolveSlugRedirectUrl only after a normal slug lookup has failed and the request is heading for a 404. The search through the history arrays has no index. Running it only on misses keeps it away from normal traffic.

When you delete a record, its history goes with it. The global soft-delete extension hides soft-deleted rows, so their old URLs return a real 404 instead of redirecting to a page that no longer exists.

Categories are excluded from slug redirects

previousSlugs exists on Product, Collection and Page only. Renaming a category slug does not leave a redirect behind, so an inbound link to the old category URL breaks.

Litestore leaves categories out on purpose. A category's URL is a slug path made of its parents' slugs and its own, and renaming one changes the URLs of every category below it. Keeping old category URLs working is a harder problem than remembering one old value on one row.

Sitemap and feeds

Litestore rebuilds the sitemap every hour and lists at most 50,000 URLs of each type.

That limit is the maximum the sitemaps.org protocol allows in one file. A catalog bigger than that would need a sitemap index. Until then, stopping at the protocol limit is better than quietly dropping URLs at some lower number.

Litestore leaves out paths that don't render a page or shouldn't be indexed. It matches them by prefix:

/checkout
/cart
/account
/dashboard
/auth
/404
/search

A stored page whose route starts with one of these prefixes must never appear in the sitemap, because robots.ts disallows those paths.

You can turn off indexing for the whole site. seo_noindex_site makes robots.txt disallow everything, which is what you want on a staging deployment. Without it, robots.txt disallows /admin/, /auth/ and /dashboard/. Both versions point to the dynamic sitemap and use the real site URL.

Litestore also serves RSS feeds at these paths:

/rss/products.xml
/rss/collections.xml

AIO

AIO checks how well AI assistants can read your store. Assistants need separate facts they can check, not marketing copy.

AIO has no route of its own. It appears inside /admin/seo as a group of stats and four checklist rows that start with AI ·.

The four rows cover structured specs, intent tags, the recommendation graph and category coverage. They use the same SeoIssue shape as the SEO audit, so the same report code renders them.

The rules come from lib/aio/:

  • A peer norm is a property key that at least half the products in a category declare, in a category with at least three products. The report flags a product that is missing one.
  • aio_min_spec_count defaults to 3. A product needs at least that many specifications before the report counts it as described.
  • Each product can have at most 60 property values, and Litestore removes duplicate keys regardless of case.

auditAioProduct works with any attribute keys. It never names a specific attribute, because specifications are free-form pairs of keys and values that each operator fills in their own way. The report layer works out the peer norms and passes them to the engine as peerGapKeys, so the engine itself stays pure.

The three-product minimum stops the report from inventing a norm out of nothing. With a single product, that product's keys would become the norm, and products in other categories would look incomplete by comparison.

Discrete, not prose

lib/aio/attributes.ts is the only place that turns product data into name and value pairs a machine can read. Both the JSON-LD additionalProperty and the merchant feed use it.

An option with several values becomes several pairs instead of one comma-separated string:

Size = S / M / L

  →  { name: "Size", value: "S" }
     { name: "Size", value: "M" }
     { name: "Size", value: "L" }

An AI assistant reads three structured facts here, not one sentence it has to parse. When a key appears both in the curated specifications and in the variant options, the specifications win.

The 60-value cap stops an extreme product, with many options that each have many values, from bloating a JSON-LD document. Real catalogs stay far below it.

What the two AI surfaces share

The product page's structured data and the merchant feed share only one thing, the return policy, through lib/aio/commerce.ts. Shipping details are not extracted yet, and each of the two computes price and availability on its own.

merchantReturnPolicy builds a schema.org MerchantReturnPolicy from the operator's return window. When the window is zero or negative, it returns nothing at all. It never makes up a policy just to fill the field.

Where a change goes

Change SEO_LIMITS when you want different thresholds, such as how long a title should be or how much copy counts as thin. The checklist row, the report and the fix hints all read the same constants, so one edit changes all three.

Add an issue code to auditSeo when you enforce a new rule. Add the code to a checklist group as well, so the issue shows up somewhere.

Put logic that compares records in the report layer, not in the engine. Duplicates and peer norms reach the engine as flags, which keeps both engines pure and easy to test.

Add to lib/aio/ when the reader is an AI assistant rather than a search engine, and keep the code working for any attribute key.

Before you add a rule, don't ask only whether it would improve rankings. Ask this:

Can this be checked from data the store already has, and can an operator see exactly why it failed?

On this page