Litestore · Developer docs
Storefront

SEO

Litestore derives page metadata, structured data and the sitemap from the records it already has, and renamed URLs keep redirecting to the new address.

Litestore builds each storefront page's metadata, structured data and sitemap entry from the record it is rendering, and it keeps old URLs working when you rename a slug. Read this page when you need to change how a page appears in search results, how the sitemap and feeds behave, or what AI assistants can read from a product.

Linen shirt
S$48M$48LNo price
4 of 5 checks pass
A product's search title and description default to the name and copy in the product editor.

There is no SEO form, audit or score in the admin. The defaults come from the data, and an override is a stored field that the product page's Fill with AI or the assistant's setSeoMeta tool writes.

SEO subject

modules/storefront/seo/subject.ts defines SeoSubject, the one description of how a storefront entity appears, ranks and links. Metadata, JSON-LD, the sitemap and breadcrumbs all read a subject instead of each working out the canonical URL, title and description on its own. A builder exists for each kind: productSubject, collectionSubject, categorySubject, pageSubject, blogPostSubject, feedSubject and indexSubject. They are built from data the page already fetched, so they add no query.

FieldDefaultOverride
TitleThe entity's name. The root metadata applies SEO_TITLE_TEMPLATE ("%s · {site}") from platform/config/storefront.ts.A stored metaTitle.
DescriptionderiveMetaDescription: the first ~155 characters of the plain-text description or content, cut on a word.A stored metaDescription.

setSeoMeta sets the title, the description or both on a product, collection, category or page. Only the fields given change, and an empty string clears the override. Like every change-making assistant tool, it only runs after an operator approves it.

SITEMAP_DEFAULTS in the same file is the one per-kind sitemap policy (products 0.8 weekly, collections and categories 0.7 weekly, pages and blog posts 0.5 monthly), and the sitemap reads it too, so the two can't drift.

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, so 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, and running it only on misses keeps it away from normal traffic.

slugHistoryPatch and resolveSlugRedirectUrl live in modules/storefront/slug-redirects.ts, and BaseCRUD.update calls the patch for the redirectable models.

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

app/sitemap.ts is rebuilt every hour (revalidate = 3600) 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.

It only lists what renders a real page: published products, collections and categories that actually have products to show, active custom pages, published blog posts and the home page. Writes that change what the sitemap lists call invalidateSitemap() from platform/cache.ts.

robots.txt disallows /admin/, /auth/ and /dashboard/. It points to the dynamic sitemap and uses the real site URL.

Litestore also serves RSS feeds at these paths:

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

Structured data for AI readers

AI assistants need separate facts they can check, not marketing copy. modules/storefront/seo/structured-data.ts builds the JSON-LD for products, collections and articles, and two small modules in modules/storefront/seo/aio/ shape what it contains.

Structured values

modules/storefront/seo/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 product feed at /api/products/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 code works with any attribute key and never names a specific one, because specifications are free-form pairs that each operator fills in their own way.

Each product is capped at 60 property values, with duplicate keys removed regardless of case. The 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.

Return policy

The product page's structured data and the product feed share the return policy through modules/storefront/seo/aio/commerce.ts. merchantReturnPolicy builds a schema.org MerchantReturnPolicy from the store's refund window (REFUND_REQUEST_WINDOW_DAYS in platform/config/store-policies.ts). When the window is zero or negative, it returns nothing at all. It never makes up a policy just to fill the field. Litestore doesn't extract shipping details into structured data yet.

SEO changes

Before you add an SEO field, ask this: can it be derived from data the store already has? If it can, derive it in the subject instead of storing it.

On this page