Product pages are some of the most valuable pages on a shop's site. They rank for specific searches, collect links from reviews and comparisons, and convert visitors who already know what they want.

Then the product runs out. Or gets discontinued. And the code path for that is usually whatever the first developer happened to write: sometimes a 404, sometimes a redirect to the homepage, sometimes the page just stays up saying "available" forever.

Each of those has a cost. This checklist covers what to do in each case, from a developer's point of view, because these decisions live in your code, not in an SEO tool.

First: name your three states

Most shops treat "not available" as one state. It's actually three:

  1. Temporarily out of stock. It's coming back.
  2. Permanently discontinued, with a replacement. There's a successor product.
  3. Permanently discontinued, no replacement. It's gone.

Model them explicitly:

type Availability = | { kind: "in_stock" } | { kind: "out_of_stock"; restockDate?: string } | { kind: "discontinued"; replacementSlug?: string }; 

If your data model can't tell these apart, your pages can't either.

Checklist: temporarily out of stock

  • [ ] Keep the page live with a 200 status. The page still has value. It's still the right result for searches about the product.
  • [ ] Say clearly that it's out of stock, near the price, not hidden at the bottom.
  • [ ] Offer something useful: a restock date if you know it, a "notify me" option, and links to similar products.
  • [ ] Update structured data to match. If you use Product markup, the offer's availability should say out of stock, not in stock:
function offerAvailability(a: Availability): string { switch (a.kind) { case "in_stock": return "https://schema.org/InStock"; case "out_of_stock": return "https://schema.org/OutOfStock"; case "discontinued": return "https://schema.org/Discontinued"; } } 
  • [ ] Keep the price, description and images. Don't strip the page down to a "sorry" message. A thin page is less useful to visitors and to search engines.
  • [ ] Don't remove it from internal links entirely. Category pages can sort it lower, but don't make it an orphan.
  • [ ] Keep it in the sitemap.

Checklist: discontinued, with a replacement

  • [ ] Redirect permanently (301) to the replacement product, if the replacement is genuinely equivalent (the new model of the same thing).
  • [ ] Or keep the old page up with a prominent link to the replacement, if people still search for the old product specifically (for manuals, compatibility, reviews). This is often the better choice for well-known products.
  • [ ] Don't redirect to the homepage or a generic category. Search engines often treat irrelevant redirects like a missing page, and users find them confusing.

A small, explicit redirect map beats clever logic:

// redirects.ts — reviewed like any other code export const productRedirects: Record<string, string> = { "/products/trail-runner-3": "/products/trail-runner-4", "/products/kettle-classic": "/products/kettle-classic-2", }; 
app.get("/products/:slug", async (req, res) => { const target = productRedirects[req.path]; if (target) return res.redirect(301, target); // ...normal rendering }); 

Checklist: discontinued, no replacement

  • [ ] Decide: keep or remove? If the page still gets visits or has links from other sites, consider keeping it as an information page ("This product is no longer available") with links to related products. If it has no visits and no links, removing it is fine.
  • [ ] If removing, return 410 (Gone) or 404. Not a 200 page saying "not found". Real status codes help search engines drop the page cleanly.
if (product.availability.kind === "discontinued" && !product.keepPage) { return res.status(410).render("product-gone", { related: await related(product) }); } 
  • [ ] Still show a helpful page with the error status: search, related categories, popular products.
  • [ ] Remove it from the sitemap and from internal links.

Checklist: things that go wrong in code

  • [ ] Availability comes from one source. If the page template reads stock from one service and the structured data reads from a cache, they'll disagree.
  • [ ] Out-of-stock products aren't accidentally excluded from rendering by a query like WHERE stock > 0 in the product page route. (This is a surprisingly common way products become 404s overnight.)
  • [ ] Variants are handled. If one size is out of stock, the page shouldn't say the whole product is.
  • [ ] Bulk changes are reviewed. A script that marks 3,000 products discontinued shouldn't also 410 them all without a human looking.
  • [ ] Changes are monitored. Track how many product URLs return 200, 301, 404 and 410 over time. A sudden jump in 404s on product URLs is almost always a bug.

That last point is where tooling helps. An SEO monitor such as seotto.com can flag sudden status-code changes across many URLs, which is exactly the signal you want after a stock-sync or catalogue import.

A quick decision table

Situation Status Page content
Out of stock, coming back 200 Product + "out of stock" + alternatives
Discontinued, close replacement 301 → replacement (none)
Discontinued, still searched for 200 Info page + link to replacement
Discontinued, no value left 410 or 404 Helpful "gone" page

Testing it

Write a test for each state. It takes ten minutes and prevents the "stock sync turned 500 pages into 404s" incident:

test("out-of-stock product still renders with 200", async () => { const res = await request(app).get("/products/some-out-of-stock-item"); expect(res.status).toBe(200); expect(res.text).toContain("Out of stock"); }); 

Related reading

How does your shop handle discontinued products today: 404, redirect, or keep the page? And has a stock-sync bug ever taken product pages offline for you? Let's compare notes in the comments.