vercel/commerce
May 5, 2026
Generated by Inkwell Forge — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
This Business Rules Assessment documents every business rule, validation, calculation, and conditional logic observable from the screen-level technical documentation of the Commerce application. Rules are extracted from five screens: Product Detail (/product/[handle]), Home (/), Search Detail (/search/[collection]), Search (/search), and Item Detail (/[page]). Business rules derived from screens not included in the assessed documentation — including the cart, checkout, account management, and any header/navigation components — are outside the scope of this document. This document reflects a partial view of the application; rules governing variant selection, add-to-cart mutations, payment processing, and order management are not covered here.
Rules were extracted by systematically scanning each screen's documentation across all documented sections: access control (§2), UI layout and conditional rendering (§3), data flow (§4), API call response handling (§5), event handlers (§6), forms and validation (§7), explicit business logic (§8), navigation (§9), error handling (§10), and security considerations (§14). Each extracted rule is expressed declaratively — stating what must be true or what must happen — rather than describing implementation mechanics. Rules are classified using BABOK v3.0 categories (Structural, Operative, Decision), expressed in controlled natural language per SBVR v1.5 principles, and structured for decision table representation per DMN v1.5 where multiple conditions determine an outcome.
All rules in this document are reverse-engineered from technical screen documentation written for developers; they are not derived from requirements documents, policy manuals, or stakeholder interviews. Business rationale is stated only where the documentation explicitly provides it; all other rationale fields are marked accordingly. Rules governing child components referenced in the screen documentation (e.g., ProductDescription, ThreeItemGrid, Carousel, Gallery, ProductGridItems) are not fully documented here because their internal implementations are outside the scope of the provided documentation. Where the documentation identifies a potential defect or edge case, that observation is captured in the Gap Analysis rather than as a rule.
This Business Rules Assessment covers 5 screens of the Commerce application:
| Screen Name | Route |
|---|---|
| Product Detail | /product/[handle] |
| Home | / |
| Search Detail | /search/[collection] |
| Search | /search |
| Item Detail | /[page] |
Business rules derived from screens not included in the assessed documentation are outside the scope of this document.
Generated by Inkwell Forge — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
| # | Rule Name | Domain | Category | Enforcement | Criticality | Screen(s) |
|---|---|---|---|---|---|---|
| BR-001 | Hidden Product Tag Suppresses SEO Indexing | Content Visibility & Access Control | Operative | Server | High | Product Detail (/product/[handle]) |
| BR-002 | Missing Product Handle Renders 404 | Content Visibility & Access Control | Operative | Server | Critical | Product Detail (/product/[handle]) |
| BR-003 | Missing Collection Handle Renders 404 | Content Visibility & Access Control | Operative | Server | Critical | Search Detail (/search/[collection]) |
| BR-004 | Missing Page Handle Renders 404 | Content Visibility & Access Control | Operative | Server | Critical | Item Detail (/[page]) |
| BR-005 | All Storefront Screens Are Publicly Accessible | Content Visibility & Access Control | Structural | Server | Low | Product Detail (/product/[handle]), Home (/), Search Detail (/search/[collection]), Search (/search), Item Detail (/[page]) |
| BR-006 | Product JSON-LD Availability Maps from availableForSale |
SEO & Structured Data | Decision | Server | Medium | Product Detail (/product/[handle]) |
| BR-007 | JSON-LD Uses AggregateOffer for Variant Price Range | SEO & Structured Data | Structural | Server | Medium | Product Detail (/product/[handle]) |
| BR-008 | JSON-LD Price Currency Sourced from Min Variant Price | SEO & Structured Data | Structural | Server | Low | Product Detail (/product/[handle]) |
| BR-009 | Product SEO Metadata Controlled by Hidden Product Tag | SEO & Structured Data | Decision | Server | High | Product Detail (/product/[handle]) |
| BR-010 | Collection SEO Title Uses Fallback Chain | SEO & Structured Data | Decision | Server | Medium | Search Detail (/search/[collection]) |
| BR-011 | Collection SEO Description Uses Three-Tier Fallback | SEO & Structured Data | Decision | Server | Medium | Search Detail (/search/[collection]) |
| BR-012 | Item Detail SEO Title Uses Two-Tier Fallback | SEO & Structured Data | Decision | Server | Medium | Item Detail (/[page]) |
| BR-013 | Item Detail SEO Description Uses Two-Tier Fallback | SEO & Structured Data | Decision | Server | Medium | Item Detail (/[page]) |
| BR-014 | Item Detail OpenGraph Type Is article |
SEO & Structured Data | Structural | Server | Low | Item Detail (/[page]) |
| BR-015 | Item Detail OpenGraph Timestamps Sourced from Shopify | SEO & Structured Data | Structural | Server | Low | Item Detail (/[page]) |
| BR-016 | Home Page OpenGraph Type Is website |
SEO & Structured Data | Structural | Server | Low | Home (/) |
| BR-017 | Product Gallery Capped at Five Images | Product Display & Merchandising | Structural | Server | Medium | Product Detail (/product/[handle]) |
| BR-018 | Related Products Section Suppressed When Empty | Product Display & Merchandising | Operative | Server | Low | Product Detail (/product/[handle]) |
| BR-019 | Homepage Features Exactly Three Products in Hero Grid | Product Display & Merchandising | Structural | Server | Medium | Home (/) |
| BR-020 | Empty Collection Renders "No Products" Message | Product Display & Merchandising | Operative | Server | Medium | Search Detail (/search/[collection]) |
| BR-021 | Empty Search Results Render "No Match" Message | Product Display & Merchandising | Operative | Server | Medium | Search (/search) |
| BR-022 | Search Result Count Uses Singular/Plural Grammar | Product Display & Merchandising | Operative | Server | Low | Search (/search) |
| BR-023 | Result Summary Suppressed When No Search Query | Product Display & Merchandising | Operative | Server | Low | Search (/search) |
| BR-024 | Product Grid Suppressed When No Results | Product Display & Merchandising | Operative | Server | Low | Search (/search) |
| BR-025 | Sort Parameter Resolves to Valid Sort Configuration | Search & Sort | Decision | Server | High | Search Detail (/search/[collection]), Search (/search) |
| BR-026 | Unrecognized Sort Parameter Falls Back to Default | Search & Sort | Operative | Server | Medium | Search Detail (/search/[collection]), Search (/search) |
| BR-027 | Related Product Tiles Link to Product Detail with Prefetch | Navigation & Routing | Operative | Client | Low | Product Detail (/product/[handle]) |
| BR-028 | Item Detail Date Formatted Using Server Locale | Content Rendering | Operative | Server | Low | Item Detail (/[page]) |
| BR-029 | Item Detail Page Body Rendered as Raw HTML | Content Rendering | Structural | Server | Medium | Item Detail (/[page]) |
Description: Rules governing which content is accessible to visitors, how missing or invalid content is handled, and the public-access policy of the storefront.
Screens involved: Product Detail (/product/[handle]), Home (/), Search Detail (/search/[collection]), Search (/search), Item Detail (/[page])
Domain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: High — a failure here would expose draft or internal products to search engine indexing, potentially surfacing incomplete or incorrect product data in search results.
Rule Statement: A product tagged with HIDDEN_PRODUCT_TAG must have its robots.index, robots.follow, and googleBot.index metadata directives set to false, preventing search engine crawling and indexing of that product page.
Conditions:
product.tags includes the value of the HIDDEN_PRODUCT_TAG constant → indexable = falseproduct.tags does not include HIDDEN_PRODUCT_TAG → indexable = trueindexable = false, the page metadata sets robots: { index: false, follow: false } and googleBot: { index: false, nosnippet: true } (or equivalent). When indexable = true, default indexing behavior applies.Validation Details:
product.tags array from the Shopify Storefront API responseindexable = !product.tags.includes(HIDDEN_PRODUCT_TAG) — boolean derived at render timeindexable = true → standard robots metadata; product is eligible for search engine indexingindexable = false → robots.index = false, robots.follow = false injected into page <head>; product is excluded from search engine results but remains accessible via direct URLSource Evidence: Product Detail (/product/[handle]) — §2, §8, §14
Business Rationale: Allows merchants to publish draft, internal, or test products at live URLs without those products appearing in search engine results. This is a soft-visibility mechanism, not an access control mechanism — the URL remains publicly accessible.
Related Rules: BR-009 (SEO metadata also controlled by this flag), BR-002 (the complementary hard-visibility rule)
Testing Implications:
HIDDEN_PRODUCT_TAG → verify robots metadata allows indexingHIDDEN_PRODUCT_TAG → verify robots.index = false and robots.follow = false in rendered <head>HIDDEN_PRODUCT_TAG → verify page still renders and is accessible via direct URL (not a 404)product.tags is an empty array → verify indexable = trueDomain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: Critical — without this rule, requests for non-existent product handles would produce broken or empty page renders rather than a proper HTTP 404 response.
Rule Statement: If getProduct returns a falsy value for a given handle, the application must call notFound() and render the 404 page, both during metadata generation and during page rendering.
Conditions:
getProduct(handle) returns a truthy product object → page renders normallygetProduct(handle) returns null or undefined → notFound() is callednotFound() triggers Next.js's not-found boundary, rendering the application's not-found.tsx page with a 404 HTTP statusValidation Details:
getProduct(params.handle) — either a product object or a falsy valueif (!product) notFound() — enforced independently in both generateMetadata and ProductPagenotFound() called → Next.js 404 page rendered; no product UI is shownSource Evidence: Product Detail (/product/[handle]) — §5, §8, §9, §10
Business Rationale: Ensures that invalid or deleted product URLs return a proper 404 HTTP response rather than a broken page, which is important for both user experience and SEO (search engines should receive 404 for removed products).
Related Rules: BR-003, BR-004 (same pattern on other screens)
Testing Implications:
generateMetadata receives invalid handle → verify 404 is triggered before page body rendersDomain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: Critical — without this rule, requests for non-existent or unpublished collections would produce broken or empty page renders.
Rule Statement: If getCollection returns a falsy value for a given collection handle during metadata generation, the application must call notFound() and render the 404 page.
Conditions:
getCollection(params.collection) returns a truthy collection object → metadata generation proceeds normallygetCollection(params.collection) returns a falsy value → notFound() is callednotFound() triggers Next.js's not-found boundary, rendering the application's 404 pageValidation Details:
getCollection(params.collection) in generateMetadataif (!collection) notFound() — enforced in generateMetadatanotFound() called → 404 page renderedSource Evidence: Search Detail (/search/[collection]) — §5, §8, §9, §10, §14
Business Rationale: Prevents access to invalid or unpublished Shopify collection handles, returning a standard 404 rather than a broken or empty page. Also prevents information leakage about the collection namespace.
Testing Implications:
Domain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: Critical — without this rule, requests for non-existent Shopify page handles would produce broken renders rather than a proper HTTP 404 response.
Rule Statement: If getPage returns a falsy value for a given page handle, the application must call notFound() and render the 404 page, both during metadata generation and during page rendering.
Conditions:
getPage(params.page) returns a truthy page object → page renders normallygetPage(params.page) returns null or undefined → notFound() is callednotFound() triggers Next.js's not-found boundary, rendering the application's 404 pageValidation Details:
getPage(params.page) — either a page object or a falsy valueif (!page) notFound() — enforced independently in both generateMetadata and the Page componentnotFound() called → Next.js 404 page renderedSource Evidence: Item Detail (/[page]) — §2, §5, §8, §9, §10
Business Rationale: Ensures that invalid or deleted Shopify page handles return a proper 404 HTTP response rather than a broken page.
Testing Implications:
"privacy-policy") → verify full page rendersgenerateMetadata receives invalid handle → verify 404 triggered before page body rendersgetPage returns falsy due to API error → verify 404 rendered (not a 500)Domain: Content Visibility & Access Control Category: Structural Enforcement: Server Criticality: Low — this is an intentional design constraint, not a defect risk. Criticality is low because the rule describes the absence of access control, which is the intended behavior for a public storefront.
Rule Statement: All five assessed screens must be accessible to any visitor without authentication, session validation, or role-based authorization checks.
Conditions: None — this rule applies unconditionally to all assessed screens.
Validation Details:
/product/[handle], /, /search/[collection], /search, or /[page]Source Evidence: Product Detail (/product/[handle]) — §2; Home (/) — §2; Search Detail (/search/[collection]) — §2; Search (/search) — §2; Item Detail (/[page]) — §2
Business Rationale: These screens display public product catalog and content data. Requiring authentication would create friction for prospective customers and is not appropriate for a public-facing storefront.
Related Rules: BR-001 (soft visibility via SEO tags, not access control)
Testing Implications:
Description: Rules governing how search engine metadata, Open Graph tags, robots directives, and JSON-LD structured data are generated and populated for each screen.
Screens involved: Product Detail (/product/[handle]), Home (/), Search Detail (/search/[collection]), Item Detail (/[page])
availableForSaleDomain: SEO & Structured Data Category: Decision Enforcement: Server Criticality: Medium — incorrect availability data in structured markup could mislead search engines about product stock status, affecting rich result accuracy.
Rule Statement: The availability field in the product's JSON-LD structured data must be set to https://schema.org/InStock when product.availableForSale is true, and to https://schema.org/OutOfStock when product.availableForSale is false.
Conditions:
product.availableForSale === true → availability: "https://schema.org/InStock"product.availableForSale === false → availability: "https://schema.org/OutOfStock"Validation Details:
product.availableForSale boolean from the Shopify Storefront APIavailableForSale ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"schema.org availability URI injected into the JSON-LD <script> blockavailableForSale is undefinedSource Evidence: Product Detail (/product/[handle]) — §8
Business Rationale: Enables Google and other search engines to display accurate in-stock/out-of-stock status in product rich results, improving click-through quality.
Testing Implications:
availableForSale = true → verify JSON-LD contains "https://schema.org/InStock"availableForSale = false → verify JSON-LD contains "https://schema.org/OutOfStock"availableForSale is undefined → verify behavior (likely renders "https://schema.org/OutOfStock" due to falsy evaluation)Domain: SEO & Structured Data Category: Structural Enforcement: Server Criticality: Medium — using the wrong schema type would produce invalid structured data, potentially disqualifying the product from Google rich results.
Rule Statement: The product JSON-LD structured data must use the AggregateOffer schema type to represent the product's price range, populating highPrice from priceRange.maxVariantPrice.amount and lowPrice from priceRange.minVariantPrice.amount.
Conditions: None — this rule applies to all products unconditionally.
Validation Details:
product.priceRange.maxVariantPrice.amount and product.priceRange.minVariantPrice.amount from the Shopify APIoffers object uses @type: "AggregateOffer" with highPrice and lowPrice fields populated from the respective price range amountsAggregateOffer block injected into the JSON-LD <script> tagundefined values, producing invalid structured dataSource Evidence: Product Detail (/product/[handle]) — §8
Business Rationale: AggregateOffer is the correct schema.org type for products with multiple variants at different price points. Using it enables Google to display price ranges in rich results.
Testing Implications:
@type is "AggregateOffer" and highPrice ≥ lowPricehighPrice equals lowPrice in JSON-LDpriceRange.maxVariantPrice.amount is missing → verify JSON-LD does not contain undefined as a stringDomain: SEO & Structured Data Category: Structural Enforcement: Server Criticality: Low — incorrect currency code in structured data would affect rich result display accuracy but would not break the page or user workflow.
Rule Statement: The priceCurrency field in the product JSON-LD structured data must be sourced from priceRange.minVariantPrice.currencyCode.
Conditions: None — this rule applies unconditionally.
Validation Details:
product.priceRange.minVariantPrice.currencyCode (e.g., "USD", "EUR")priceCurrency field in the AggregateOffer JSON-LD block is set directly from this valuecurrencyCode is missing, priceCurrency will be undefined in the JSON-LD outputSource Evidence: Product Detail (/product/[handle]) — §8
Business Rationale: Not stated in documentation — verify with business stakeholders.
[Not documented — WHO: Product owner or SEO specialist; WHAT: Is it intentional to source priceCurrency from the minimum variant price rather than a store-level currency setting, and what should happen if variants have different currency codes?; WHERE: Insert in the Business Rationale field of BR-008 above]
Related Rules: BR-007
Testing Implications:
minVariantPrice.currencyCode = "USD" → verify JSON-LD priceCurrency is "USD"Domain: SEO & Structured Data Category: Decision Enforcement: Server Criticality: High — failure to suppress robots metadata for hidden products would allow draft or internal products to be indexed by search engines.
Rule Statement: If a product is tagged with HIDDEN_PRODUCT_TAG, the page metadata must set robots.index = false, robots.follow = false, and equivalent googleBot directives; otherwise, default indexing metadata must apply.
Conditions:
product.tags.includes(HIDDEN_PRODUCT_TAG) is true → robots: { index: false, follow: false }, googleBot: { index: false } (or equivalent)product.tags.includes(HIDDEN_PRODUCT_TAG) is false → default robots metadata (indexing permitted)Validation Details:
product.tags array; HIDDEN_PRODUCT_TAG constant from lib/constantsindexable = !product.tags.includes(HIDDEN_PRODUCT_TAG) → used to conditionally set robots metadata in generateMetadatarobots.index = false injected into <head>; page excluded from search resultsSource Evidence: Product Detail (/product/[handle]) — §2, §8
Business Rationale: Allows merchants to maintain draft or internal products at live URLs without those pages appearing in search engine results.
Related Rules: BR-001
Testing Implications:
HIDDEN_PRODUCT_TAG → verify <meta name="robots"> allows indexingHIDDEN_PRODUCT_TAG → verify <meta name="robots" content="noindex,nofollow"> in rendered HTMLHIDDEN_PRODUCT_TAG removed → verify robots metadata reverts to indexable on next renderDomain: SEO & Structured Data Category: Decision Enforcement: Server Criticality: Medium — incorrect or missing SEO titles reduce search result quality but do not break functionality.
Rule Statement: The <title> tag for a collection page must use collection.seo.title if present; otherwise it must fall back to collection.title.
Conditions:
collection.seo?.title is truthy → use collection.seo.title as the page titlecollection.seo?.title is falsy → use collection.title as the page titleValidation Details:
collection.seo?.title and collection.title from the Shopify APIcollection.seo?.title || collection.title — optional chaining with logical OR fallbackSource Evidence: Search Detail (/search/[collection]) — §8
Business Rationale: Allows merchants to set SEO-optimized titles that differ from the collection's display name, following Shopify's standard SEO override pattern.
Related Rules: BR-011
Testing Implications:
seo.title set → verify <title> uses seo.titleseo.title → verify <title> uses collection.titleseo.title and collection.title are empty → verify <title> behaviorDomain: SEO & Structured Data Category: Decision Enforcement: Server Criticality: Medium — missing or poor meta descriptions reduce search result quality but do not break functionality.
Rule Statement: The meta description for a collection page must use collection.seo.description if present; otherwise collection.description; otherwise a generated string of the form "[collection.title] products".
Conditions:
collection.seo?.description is truthy → use as meta descriptioncollection.seo?.description is falsy AND collection.description is truthy → use collection.description"${collection.title} products" as the generated fallbackValidation Details:
collection.seo?.description, collection.description, collection.titlecollection.seo?.description || collection.description || \${collection.title} products``collection.title is also falsy, the generated fallback produces " products" (empty prefix)Source Evidence: Search Detail (/search/[collection]) — §8
Business Rationale: Ensures every collection page has a meaningful meta description for search engines, even when merchants have not explicitly configured SEO fields.
Related Rules: BR-010
Testing Implications:
seo.description → verify meta description uses seo.descriptionseo.description but with description → verify fallback to description"[title] products" stringDomain: SEO & Structured Data Category: Decision Enforcement: Server Criticality: Medium — missing SEO titles reduce search result quality but do not break functionality.
Rule Statement: The <title> tag for an item detail page must use page.seo.title if present; otherwise it must fall back to page.title.
Conditions:
page.seo?.title is truthy → use page.seo.titlepage.seo?.title is falsy → use page.titleValidation Details:
page.seo?.title and page.title from the Shopify APIpage.seo?.title || page.title — optional chaining with logical OR fallbackSource Evidence: Item Detail (/[page]) — §8
Business Rationale: Follows Shopify's standard SEO override pattern, allowing merchants to set search-optimized titles distinct from the page's display title.
Related Rules: BR-013
Testing Implications:
seo.title set → verify <title> uses seo.titleseo.title → verify <title> uses page.title<title> tag outputDomain: SEO & Structured Data Category: Decision Enforcement: Server Criticality: Medium — missing meta descriptions reduce search result quality but do not break functionality.
Rule Statement: The meta description for an item detail page must use page.seo.description if present; otherwise it must fall back to page.bodySummary.
Conditions:
page.seo?.description is truthy → use page.seo.descriptionpage.seo?.description is falsy → use page.bodySummaryValidation Details:
page.seo?.description and page.bodySummary from the Shopify APIpage.seo?.description || page.bodySummarySource Evidence: Item Detail (/[page]) — §8
Business Rationale: Follows Shopify's standard SEO override pattern, allowing merchants to set search-optimized descriptions distinct from the page body summary.
Related Rules: BR-012
Testing Implications:
seo.description → verify meta description uses seo.descriptionseo.description but with bodySummary → verify fallback to bodySummaryarticleDomain: SEO & Structured Data Category: Structural Enforcement: Server Criticality: Low — incorrect OpenGraph type affects social sharing previews but does not break functionality.
Rule Statement: All item detail pages (/[page]) must set the OpenGraph type metadata to "article".
Conditions: None — this rule applies unconditionally to all item detail pages.
Validation Details:
openGraph.type = "article" set in generateMetadataog:type = "article" when the URL is sharedSource Evidence: Item Detail (/[page]) — §8
Business Rationale: Treats all Shopify Pages (policy pages, about pages, FAQs) as article-type content for social sharing purposes, enabling richer link previews on platforms that support the article OpenGraph type.
Testing Implications:
<meta property="og:type" content="article"> in rendered HTMLog:type is "website", not "article" (BR-016)Domain: SEO & Structured Data Category: Structural Enforcement: Server Criticality: Low — incorrect timestamps affect social sharing metadata accuracy but do not break functionality.
Rule Statement: The OpenGraph publishedTime metadata for an item detail page must be sourced from page.createdAt, and modifiedTime must be sourced from page.updatedAt.
Conditions: None — this rule applies unconditionally.
Validation Details:
page.createdAt and page.updatedAt ISO 8601 timestamps from the Shopify APIopenGraph.publishedTime = page.createdAt, openGraph.modifiedTime = page.updatedAtSource Evidence: Item Detail (/[page]) — §8
Business Rationale: Provides social platforms and search engines with accurate content freshness signals for Shopify page content.
Testing Implications:
createdAt and updatedAt → verify og:published_time and og:modified_time in rendered HTMLupdatedAt is malformed → verify OpenGraph tag outputwebsiteDomain: SEO & Structured Data Category: Structural Enforcement: Server Criticality: Low — incorrect OpenGraph type affects social sharing previews but does not break functionality.
Rule Statement: The home page (/) must set the OpenGraph type metadata to "website".
Conditions: None — this rule applies unconditionally to the home page.
Validation Details:
metadata exportopenGraph.type = "website" set as a static constant in app/page.tsxog:type = "website" when the home URL is sharedSource Evidence: Home (/) — §8
Business Rationale: Identifies the home page as a generic website page (as opposed to a product or article), which is the correct OpenGraph type for a storefront homepage.
Related Rules: BR-014
Testing Implications:
<meta property="og:type" content="website"> in rendered HTMLDescription: Rules governing how products are displayed, how many are shown, how empty states are handled, and how merchandising decisions are encoded in the UI.
Screens involved: Product Detail (/product/[handle]), Home (/), Search Detail (/search/[collection]), Search (/search)
Domain: Product Display & Merchandising Category: Structural Enforcement: Server Criticality: Medium — exceeding this limit is not possible given the implementation, but the rule defines the maximum gallery size regardless of how many images Shopify returns.
Rule Statement: The product gallery must display at most 5 images, regardless of the number of images returned by the Shopify API for a given product.
Conditions: None — this cap applies unconditionally to all products.
Validation Details:
product.images array from the Shopify API (may contain any number of images)product.images.slice(0, 5).map(...) — array is sliced to a maximum of 5 elements before being passed to the Gallery componentGallery; additional images are silently discardedSource Evidence: Product Detail (/product/[handle]) — §4, §8
Business Rationale: Not stated in documentation — verify with business stakeholders.
[Not documented — WHO: Product owner or UX designer; WHAT: What is the business rationale for capping the gallery at 5 images? Is this a UX decision, a performance decision, or a design constraint?; WHERE: Insert in the Business Rationale field of BR-017 above]
Related Rules: None
Testing Implications:
Gallery receives 3 imagesGallery receives all 5Gallery receives exactly 5 (6th and beyond are discarded)Gallery receives an empty array and handles it gracefullyDomain: Product Display & Merchandising Category: Operative Enforcement: Server Criticality: Low — this is a UI cleanliness rule; no data or workflow is affected.
Rule Statement: The Related Products section must not render if getProductRecommendations returns an empty array.
Conditions:
relatedProducts.length === 0 → RelatedProducts component returns null; section is absent from the DOMrelatedProducts.length > 0 → section renders with product tilesValidation Details:
getProductRecommendations(product.id)if (!relatedProducts.length) return null<h2>Related Products</h2> and product tilesSource Evidence: Product Detail (/product/[handle]) — §5, §8, §10
Business Rationale: Prevents a visually empty "Related Products" section from appearing on product pages that have no algorithmic recommendations, keeping the page clean.
Related Rules: None
Testing Implications:
getProductRecommendations returns null instead of an empty array → verify behaviorDomain: Product Display & Merchandising Category: Structural Enforcement: Server Criticality: Medium — if fewer than three products are available, the ThreeItemGrid component's behavior is undefined at the page level; the grid may render incorrectly.
Rule Statement: The homepage hero grid must feature exactly three products, as determined by the ThreeItemGrid component's data source.
Conditions: None — the three-product constraint is structural to the ThreeItemGrid component.
Validation Details:
ThreeItemGrid from Shopify (collection or featured products)ThreeItemGrid component is designed to render exactly three products; the specific collection or curation mechanism is internal to that componentThreeItemGridSource Evidence: Home (/) — §3, §8
Business Rationale: The three-product hero grid is a merchandising decision encoding a specific visual layout for the homepage above-the-fold section, giving maximum visual prominence to three curated products.
Related Rules: None
Testing Implications:
ThreeItemGrid behavior (not documented at page level)⚠️ This workflow continues beyond the documented screens. The internal data fetching and rendering logic of ThreeItemGrid (components/grid/three-items) is not covered in this document. Verify the complete behavior with the development team before treating this as a comprehensive description.
Domain: Product Display & Merchandising Category: Operative Enforcement: Server Criticality: Medium — without this rule, an empty collection would render a blank page with no feedback to the user.
Rule Statement: If a collection exists but contains zero products, the page must render the message "No products found in this collection" instead of a product grid.
Conditions:
products.length === 0 → render <p>No products found in this collection</p>; do not render the Grid componentproducts.length > 0 → render the Grid with ProductGridItemsValidation Details:
products array returned by getCollectionProductsproducts.lengthSource Evidence: Search Detail (/search/[collection]) — §3, §8, §10
Business Rationale: Provides clear user feedback when a collection has no products, rather than displaying a confusing blank page.
Related Rules: BR-021
Testing Implications:
"No products found in this collection" message rendersDomain: Product Display & Merchandising Category: Operative Enforcement: Server Criticality: Medium — without this rule, a search with no results would render a blank page with no feedback.
Rule Statement: If a search query returns zero products, the page must render the message "There are no products that match " followed by the bolded search term, and must not render the product grid.
Conditions:
products.length === 0 AND searchValue is truthy → render no-results message with bolded search termproducts.length > 0 → render product grid (and summary paragraph if searchValue is truthy)Validation Details:
products array from getProducts; searchValue from searchParams.q{products.length === 0 && searchValue && <p>There are no products that match <strong>{searchValue}</strong></p>}Source Evidence: Search (/search) — §3, §8, §10
Business Rationale: Provides clear user feedback when a search query returns no matching products.
Related Rules: BR-020, BR-023, BR-024
Testing Implications:
"There are no products that match" message with bolded search termDomain: Product Display & Merchandising Category: Operative Enforcement: Server Criticality: Low — this is a display quality rule; incorrect grammar does not affect functionality.
Rule Statement: The search results summary must display "result" (singular) when exactly one product is returned, and "results" (plural) when more than one product is returned.
Conditions:
products.length > 1 → resultsText = "results"products.length <= 1 (i.e., 0 or 1) → resultsText = "result"Validation Details:
products.length integerconst resultsText = products.length > 1 ? "results" : "result"Source Evidence: Search (/search) — §4, §8
Business Rationale: Ensures grammatically correct display of result counts (e.g., "Showing 1 result for" vs. "Showing 3 results for").
Related Rules: BR-023
Testing Implications:
"result" (singular) is displayed"results" (plural) is displayed"result" is used (though the no-results message from BR-021 would display instead)Domain: Product Display & Merchandising Category: Operative Enforcement: Server Criticality: Low — this is a UI cleanliness rule; no data or workflow is affected.
Rule Statement: The search results summary paragraph must not render when no search query (searchValue) is present.
Conditions:
searchValue is truthy → render results summary paragraphsearchValue is falsy (absent or empty) → do not render results summary paragraphValidation Details:
searchValue derived from searchParams.q{searchValue && <p>Showing {products.length} {resultsText} for <strong>{searchValue}</strong></p>}Source Evidence: Search (/search) — §3, §8
Business Rationale: When a user browses all products without a search query, displaying "Showing N results for [empty]" would be confusing and unhelpful.
Testing Implications:
?q=hoodie → verify summary paragraph renders?q= parameter → verify summary paragraph is absent from DOM?q= with empty string → verify summary paragraph behaviorDomain: Product Display & Merchandising Category: Operative Enforcement: Server Criticality: Low — this is a UI cleanliness rule preventing an empty grid container from rendering.
Rule Statement: The product grid (Grid and ProductGridItems components) must not render when products.length === 0.
Conditions:
products.length > 0 → render Grid with ProductGridItemsproducts.length === 0 → do not render Grid or ProductGridItemsValidation Details:
products array from getProducts{products.length > 0 && <Grid ...><ProductGridItems .../></Grid>}Source Evidence: Search (/search) — §3, §8
Business Rationale: Prevents an empty grid container from appearing in the DOM, which could cause layout issues or confuse users.
Related Rules: BR-021
Testing Implications:
Grid component rendersGrid component is absent from DOM (not just empty)Description: Rules governing how sort parameters are resolved, validated, and applied to product listings.
Screens involved: Search Detail (/search/[collection]), Search (/search)
Domain: Search & Sort Category: Decision Enforcement: Server Criticality: High — if an invalid sort key were passed to the Shopify API, the API call could fail or return unexpected results, breaking the product listing.
Rule Statement: The sort URL query parameter must be resolved to a valid sortKey and reverse pair by matching against the sorting constants array; if no match is found, the defaultSort configuration must be used.
Conditions:
sort param matches a slug in the sorting array → use that entry's sortKey and reversesort param does not match any slug (absent, misspelled, or tampered) → use defaultSort.sortKey and defaultSort.reverseValidation Details:
searchParams.sort string (may be any value)const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSortsortKey and reverse passed to APIdefaultSort values used; no error shown to userSource Evidence: Search Detail (/search/[collection]) — §4, §7, §8; Search (/search) — §4, §7, §8
Business Rationale: Ensures that only valid Shopify ProductSortKeys enum values are ever passed to the Shopify Storefront API, preventing API errors from invalid sort parameters.
Related Rules: BR-026
Testing Implications:
?sort=price-asc (valid slug) → verify correct sortKey and reverse are used?sort=invalid-value → verify defaultSort is applied?sort= parameter → verify defaultSort is applied?sort= with empty string → verify defaultSort is applied?sort=PRICE (using the raw Shopify enum value instead of the slug) → verify defaultSort is applied (slug matching is case-sensitive)Domain: Search & Sort Category: Operative Enforcement: Server Criticality: Medium — without this rule, an unrecognized sort parameter could cause an API error or expose an invalid sort key to Shopify.
Rule Statement: When the sort URL parameter is absent or does not match any recognized sort slug, the application must silently apply the defaultSort configuration without displaying an error message to the user.
Conditions:
sort is absent or unrecognized → apply defaultSort; no error shownValidation Details:
searchParams.sort — any string or undefined|| defaultSort short-circuit in the sort resolution expressionSource Evidence: Search Detail (/search/[collection]) — §7, §8; Search (/search) — §7, §8
Business Rationale: Provides a graceful degradation path for invalid or missing sort parameters, ensuring the page always renders with a valid sort configuration rather than showing an error.
Related Rules: BR-025
Testing Implications:
?sort=<script> → verify default sort applied (no XSS risk from this path since value is only used for array lookup)Description: Rules governing how navigation links are constructed and how prefetching is applied.
Screens involved: Product Detail (/product/[handle])
Domain: Navigation & Routing Category: Operative Enforcement: Client Criticality: Low — prefetch is a performance optimization; its absence would not break navigation, only slow it.
Rule Statement: Each related product tile must render as a Next.js <Link> pointing to /product/[product.handle] with prefetch={true} enabled.
Conditions: None — this rule applies to all related product tiles unconditionally.
Validation Details:
product.handle string for each recommended product<Link href={/product/${product.handle}} prefetch={true}> wrapping each GridTileImageSource Evidence: Product Detail (/product/[handle]) — §6, §9
Business Rationale: Enables near-instant navigation to related product pages by prefetching their data when the tiles become visible, improving perceived performance.
Related Rules: None
Testing Implications:
href is /product/[handle] for each tileprefetch attribute is present on each linkhandle containing special characters → verify URL is correctly formedDescription: Rules governing how content is formatted and rendered on the Item Detail screen.
Screens involved: Item Detail (/[page])
Domain: Content Rendering Category: Operative Enforcement: Server Criticality: Low — locale-dependent date formatting is a display quality concern; it does not affect data integrity or functionality.
Rule Statement: The updatedAt timestamp on an item detail page must be formatted as a human-readable date string using Intl.DateTimeFormat with year: "numeric", month: "long", and day: "numeric" options, using the server's default locale.
Conditions: None — this rule applies unconditionally to all item detail pages.
Validation Details:
page.updatedAt ISO 8601 timestamp string from the Shopify APInew Intl.DateTimeFormat(undefined, { year: "numeric", month: "long", day: "numeric" }).format(new Date(page.updatedAt))updatedAt → new Date(malformed) produces Invalid Date; Intl.DateTimeFormat.format() outputs the string "Invalid Date" visibly on the pageSource Evidence: Item Detail (/[page]) — §3, §8
Business Rationale: Provides users with a human-readable "last updated" date for content pages such as privacy policies and FAQs, helping users assess content freshness.
Related Rules: BR-015
Testing Implications:
updatedAt → verify formatted date renders correctly (e.g., "January 15, 2024")updatedAt string → verify "Invalid Date" does not appear on the page (see GAP-07)Domain: Content Rendering Category: Structural Enforcement: Server Criticality: Medium — raw HTML rendering from an external source carries XSS risk if the content pipeline is compromised; however, Shopify sanitizes page body content server-side.
Rule Statement: The body content of an item detail page must be rendered as raw HTML via the Prose component's html prop, using the HTML string returned by the Shopify Storefront API in page.body.
Conditions: None — this rule applies unconditionally.
Validation Details:
page.body — an HTML string from the Shopify Storefront API<Prose html={page.body} /> — the Prose component receives the raw HTML string and renders it (implementation details of Prose are outside the scope of this document)Prosepage.body is an empty string, Prose renders with no content; title and date still displaySource Evidence: Item Detail (/[page]) — §3, §14
Business Rationale: Shopify Pages are authored in a rich text editor that produces HTML output. Rendering the HTML directly preserves the merchant's formatting (headings, lists, links, bold/italic text) without requiring a custom content renderer.
Related Rules: None
Testing Implications:
page.body is an empty string → verify page renders without error (title and date still visible)<script> tags → verify scripts are not executed (depends on Prose implementation)Related Rules: BR-001, BR-009 Hit Policy: Unique
Condition: product.tags includes HIDDEN_PRODUCT_TAG |
→ indexable |
→ robots.index |
→ robots.follow |
→ Page Accessible via URL |
|---|---|---|---|---|
true |
false |
false |
false |
Yes |
false |
true |
true (default) |
true (default) |
Yes |
Related Rules: BR-021, BR-023, BR-024 Hit Policy: Unique
Condition: searchValue present |
Condition: products.length > 0 |
→ Summary Paragraph | → Product Grid |
|---|---|---|---|
false |
false |
Not rendered | Not rendered |
false |
true |
Not rendered | Rendered |
true |
false |
"No products that match [term]" | Not rendered |
true |
true |
"Showing N result(s) for [term]" | Rendered |
Related Rules: BR-010 Hit Policy: First
Condition: collection.seo?.title truthy |
Condition: collection.title truthy |
→ Page <title> Value |
|---|---|---|
true |
— | collection.seo.title |
false |
true |
collection.title |
false |
false |
Empty / undefined |
Related Rules: BR-011 Hit Policy: First
Condition: collection.seo?.description truthy |
Condition: collection.description truthy |
Condition: collection.title truthy |
→ Meta Description Value |
|---|---|---|---|
true |
— | — | collection.seo.description |
false |
true |
— | collection.description |
false |
false |
true |
"[collection.title] products" |
false |
false |
false |
" products" (empty prefix) |
Related Rules: BR-012, BR-013 Hit Policy: First
| Field | Condition: Primary Source Truthy | Condition: Fallback Truthy | → Value Used |
|---|---|---|---|
<title> |
page.seo?.title is truthy |
— | page.seo.title |
<title> |
page.seo?.title is falsy |
page.title is truthy |
page.title |
<title> |
page.seo?.title is falsy |
page.title is falsy |
Empty |
<meta description> |
page.seo?.description is truthy |
— | page.seo.description |
<meta description> |
page.seo?.description is falsy |
page.bodySummary is truthy |
page.bodySummary |
<meta description> |
page.seo?.description is falsy |
page.bodySummary is falsy |
Empty |
Related Rules: BR-006 Hit Policy: Unique
Condition: product.availableForSale |
→ JSON-LD availability Value |
|---|---|
true |
"https://schema.org/InStock" |
false |
"https://schema.org/OutOfStock" |
undefined |
"https://schema.org/OutOfStock" (inferred from JavaScript falsy evaluation — verify against getProduct return type definition in lib/shopify) |
| Rule ID | Field / Input | Validation Type | Constraint | Error Message / Behavior |
|---|---|---|---|---|
| BR-002 | params.handle (URL segment) |
Custom | getProduct(handle) must return a truthy value |
notFound() called → Next.js 404 page rendered; no user-facing error message |
| BR-003 | params.collection (URL segment) |
Custom | getCollection(collection) must return a truthy value |
notFound() called → Next.js 404 page rendered; no user-facing error message |
| BR-004 | params.page (URL segment) |
Custom | getPage(page) must return a truthy value |
notFound() called → Next.js 404 page rendered; no user-facing error message |
| BR-025 | searchParams.sort (query param) |
Custom | Value must match a slug in the sorting constants array |
Silent fallback to defaultSort; no error message shown to user |
| BR-001 | product.tags array |
Custom | If HIDDEN_PRODUCT_TAG present, indexable = false |
robots.index = false, robots.follow = false injected into <head>; page still renders |
| Rule ID | Output | Formula / Logic | Inputs | Precision / Rounding |
|---|---|---|---|---|
| BR-006 | JSON-LD availability URI |
availableForSale ? "https://schema.org/InStock" : "https://schema.org/OutOfStock" |
product.availableForSale (boolean) |
Not applicable |
| BR-001 | indexable boolean |
!product.tags.includes(HIDDEN_PRODUCT_TAG) |
product.tags (string array), HIDDEN_PRODUCT_TAG (string constant) |
Not applicable |
| BR-022 | resultsText string |
products.length > 1 ? "results" : "result" |
products.length (integer) |
Not applicable |
| BR-028 | Formatted date string | new Intl.DateTimeFormat(undefined, { year: "numeric", month: "long", day: "numeric" }).format(new Date(page.updatedAt)) |
page.updatedAt (ISO 8601 string) |
Not applicable — locale-dependent string output |
| BR-017 | Gallery images array | product.images.slice(0, 5).map(img => ({ src: img.url, altText: img.altText })) |
product.images array |
Not applicable — array truncation |
| Rule | Depends On | Type | Description |
|---|---|---|---|
| BR-009 | BR-001 | Refinement | BR-009 is the SEO metadata expression of the same HIDDEN_PRODUCT_TAG check defined in BR-001; both derive from the same indexable boolean |
| BR-007 | BR-008 | Complement | BR-007 and BR-008 together define the complete AggregateOffer JSON-LD block; both must be satisfied for valid structured data |
| BR-006 | BR-007 | Complement | BR-006 (availability) and BR-007 (price range) together populate the AggregateOffer object in the JSON-LD block |
| BR-026 | BR-025 | Refinement | BR-026 is the fallback branch of the sort resolution logic defined in BR-025; BR-025 must be evaluated before BR-026 applies |
| BR-021 | BR-024 | Complement | BR-021 (no-results message) and BR-024 (grid suppression) together define the complete empty-state rendering behavior for the Search screen |
| BR-023 | BR-022 | Prerequisite | BR-023 must be satisfied (searchValue is truthy) before BR-022's resultsText calculation is rendered; if BR-023 suppresses the paragraph, BR-022's output is never displayed |
| BR-003 | BR-020 | Prerequisite | BR-003 must pass (collection exists) before BR-020 can be evaluated; an invalid collection handle triggers a 404 before the empty-state check is reached |
| BR-004 | BR-028 | Prerequisite | BR-004 must pass (page exists) before BR-028 can be evaluated; a missing page handle triggers a 404 before date formatting occurs |
| BR-004 | BR-029 | Prerequisite | BR-004 must pass before BR-029 can be evaluated; a missing page handle triggers a 404 before body rendering occurs |
| BR-012 | BR-013 | Complement | BR-012 and BR-013 together define the complete SEO metadata for item detail pages; both must be evaluated to produce a full <head> metadata set |
| BR-010 | BR-011 | Complement | BR-010 and BR-011 together define the complete SEO metadata for collection pages |
| BR-015 | BR-014 | Complement | BR-014 (type) and BR-015 (timestamps) together define the complete OpenGraph metadata for item detail pages |
Type: Implicit Rule Affected Domain: Product Display & Merchandising Risk Level: Low Description: The 5-image cap (BR-017) is enforced by product.images.slice(0, 5) in the page component before passing the array to Gallery. However, the Shopify API query for product images may request more than 5 images (the documentation notes "images (up to 5+)"), meaning the API call fetches data that is then discarded. There is no documented enforcement of this cap at the API query level (e.g., requesting only 5 images from Shopify). This is an efficiency concern rather than a correctness concern. Recommendation: Verify whether the Shopify GraphQL query for product.images limits the result to 5 nodes at the query level (e.g., images(first: 5)). If not, consider adding this limit to the query to avoid fetching and discarding unnecessary data. Related Rules: BR-017
Type: Implicit Rule Affected Domain: SEO & Structured Data Risk Level: Medium Description: The product JSON-LD block is injected via dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}. The documentation explicitly notes that JSON.stringify does not escape HTML special characters (<, >, &), which could allow a product title or description containing </script> sequences to break out of the <script> tag. The data source is the Shopify API (not user input), which reduces the risk, but it is not zero — a compromised or misconfigured Shopify store could return malicious content. Recommendation: Add a rule requiring that the JSON-LD serialization use an HTML-safe JSON serializer that escapes <, >, and & characters (e.g., replacing < with \u003c). This should be enforced as a code-level standard for all dangerouslySetInnerHTML JSON-LD injections. Related Rules: BR-006, BR-007, BR-008
featuredImage in JSON-LD BodyType: Edge Case Affected Domain: SEO & Structured Data Risk Level: High Description: The documentation explicitly identifies that productJsonLd accesses product.featuredImage.url without a null guard in the page body. If featuredImage is null or undefined, this will throw a runtime error, causing the page to fail to render. The generateMetadata function does guard this with product.featuredImage || {}, but the page body does not apply the same guard. This is an inconsistency between the metadata and page rendering paths. Recommendation: Add a null guard for product.featuredImage in the productJsonLd construction: product.featuredImage?.url. Define a business rule that the JSON-LD image field must be omitted (not set to undefined) when featuredImage is absent. Related Rules: BR-006, BR-007
searchParams Type Cast Bypasses Array-Value SafetyType: Edge Case Affected Domain: Search & Sort Risk Level: Low Description: On both the Search (/search) and Search Detail (/search/[collection]) screens, searchParams is cast with as { [key: string]: string }. In Next.js App Router, searchParams values can be string | string[] when the same query parameter appears multiple times in the URL (e.g., ?sort=price-asc&sort=trending). The type cast bypasses TypeScript's type safety for this case. If sort or q were passed as arrays, the behavior would be undefined — the array object would be passed to getProducts or the sort lookup, producing unexpected results. The sort fallback (BR-026) would handle the sort case gracefully, but the q parameter passed as an array to getProducts could produce unexpected API behavior. Recommendation: Add runtime validation of searchParams values to ensure they are strings before use, rather than relying on a type cast. Define a rule that multi-value query parameters for q and sort must be handled by taking the first value or rejecting the request. Related Rules: BR-025, BR-026
getPage Called Twice Per Request Without Visible DeduplicationType: Implicit Rule Affected Domain: Content Rendering Risk Level: Low Description: On the Item Detail screen, getPage is called independently in both generateMetadata and the Page component, resulting in two separate Shopify API calls per page request. The documentation notes that Next.js's fetch deduplication may apply if getPage uses the native fetch API internally, but this is not confirmed. If deduplication is not active, this doubles the API call volume for every item detail page render, which could contribute to Shopify API rate limit consumption. Recommendation: Verify whether getPage in lib/shopify uses the native fetch API with Next.js's automatic request deduplication. If not, consider refactoring to use React's cache() function to deduplicate the call within a single render pass. Define a rule that each Shopify API function must be called at most once per page render. Related Rules: BR-004, BR-012
[Not documented — WHO: Development team; WHAT: Does getPage in lib/shopify use the native fetch API, and is Next.js request deduplication active for this call?; WHERE: Insert in the Description field of GAP-05 above]
Type: Missing Rule Affected Domain: Product Display & Merchandising Risk Level: Medium Description: The Home screen documentation states that if ThreeItemGrid fetches zero products (e.g., an empty Shopify collection), the grid component is responsible for rendering an appropriate empty state — but no empty-state behavior is documented for ThreeItemGrid or Carousel at the page level. There is no documented rule for what the homepage should display if the featured collection is empty or if the Shopify API returns no products. This is a gap compared to the Search and Search Detail screens, which have explicit empty-state rules (BR-020, BR-021). Recommendation: Document the expected empty-state behavior for ThreeItemGrid and Carousel. Define a rule specifying what the homepage renders when the featured collection contains fewer than three products. Related Rules: BR-019
updatedAt Produces Visible "Invalid Date" StringType: Edge Case Affected Domain: Content Rendering Risk Level: Medium Description: The Item Detail screen formats page.updatedAt using new Intl.DateTimeFormat(...).format(new Date(page.updatedAt)). The documentation explicitly identifies that if page.updatedAt is malformed, new Date(malformed) produces an Invalid Date object, and Intl.DateTimeFormat.format() outputs the string "Invalid Date" visibly on the page. There is no guard or fallback for this case. Recommendation: Add a rule requiring that the updatedAt date be validated before formatting. If the date is invalid, the "last updated" paragraph should either be omitted or display a fallback string. Implement a guard such as const date = new Date(page.updatedAt); if (!isNaN(date.getTime())) { /* render date */ }. Related Rules: BR-028
page.title Has No FallbackType: Edge Case Affected Domain: Content Rendering Risk Level: Low Description: The Item Detail screen renders page.title directly in an <h1> element with no fallback. The documentation explicitly notes: "If page.title is missing, <h1> renders empty; no fallback is implemented." An empty <h1> is both a UX problem (blank heading) and an accessibility concern (screen readers announce an empty heading). This contrasts with the SEO metadata, which has a two-tier fallback (BR-012). Recommendation: Define a rule that the <h1> heading must not render empty. Add a fallback (e.g., a placeholder string, or suppress the <h1> if page.title is falsy). Related Rules: BR-012, BR-004
HIDDEN_PRODUCT_TAG Constant Value Not DocumentedType: Missing Rule Affected Domain: Content Visibility & Access Control Risk Level: Medium Description: The HIDDEN_PRODUCT_TAG constant is referenced in BR-001 and BR-009 as the mechanism for suppressing SEO indexing of draft or internal products. However, the actual string value of this constant is defined in lib/constants and is not documented in the screen documentation. Without knowing the exact tag value, merchants cannot correctly apply the tag in Shopify's admin, and QA engineers cannot test the behavior. Recommendation: Document the exact string value of HIDDEN_PRODUCT_TAG from lib/constants. Add this value to the Glossary and to the rule entries for BR-001 and BR-009.
[Not documented — WHO: Development team; WHAT: What is the exact string value of the HIDDEN_PRODUCT_TAG constant in lib/constants?; WHERE: Insert in the Glossary entry for HIDDEN_PRODUCT_TAG and in the Validation Details of BR-001 and BR-009]
Type: Missing Rule Affected Domain: Content Visibility & Access Control Risk Level: Medium Description: None of the five assessed screens document any rate limiting, request throttling, or abuse prevention for the Shopify Storefront API calls they make. All five screens are publicly accessible without authentication, meaning any visitor can trigger server-side Shopify API calls by requesting these pages. High-volume requests (e.g., from scrapers or denial-of-service attempts) could exhaust Shopify API rate limits or increase infrastructure costs. The documentation notes that rate limiting, if any, would be enforced by the Shopify Storefront API itself or at the Vercel platform level — but no such configuration is documented. Recommendation: Document whether rate limiting is configured at the Vercel edge, Next.js middleware, or Shopify API level. If no rate limiting is in place, define a rule specifying acceptable request rates and implement appropriate controls.
[Not documented — WHO: Infrastructure/DevOps team; WHAT: Is rate limiting configured at the Vercel edge, Next.js middleware, or any other layer for requests to the assessed screens?; WHERE: Insert in the Description field of GAP-10 above and create a new rule entry if rate limiting is confirmed]
Related Rules: BR-005
| Term | Definition in this context |
|---|---|
| Collection | A Shopify grouping of products, identified by a URL-safe handle string (e.g., "t-shirts", "new-arrivals"). Equivalent to a product category. Used as the [collection] route segment on the Search Detail screen. |
| Shopify Page | A content entity in Shopify's admin (distinct from product or collection pages) used for static informational content such as privacy policies, FAQs, and about pages. Rendered by the Item Detail screen. |
availableForSale |
A Shopify boolean field on a product indicating whether any variant of the product is currently purchasable. Used to determine the JSON-LD availability value. |
featuredImage |
The primary/hero image of a Shopify product, used for Open Graph tags, JSON-LD structured data, and as the default display image. |
priceRange |
A Shopify object containing minVariantPrice and maxVariantPrice, each with amount (string) and currencyCode (e.g., "USD"). Represents the price spread across all product variants. |
HIDDEN_PRODUCT_TAG |
A Shopify product tag (string constant from lib/constants) that marks a product as non-indexable by search engines. Products with this tag are accessible by URL but excluded from SEO crawling. The exact string value is defined in lib/constants. |
defaultSort |
The fallback sort configuration from lib/constants, used when the sort URL parameter is absent or unrecognized. Defines a sortKey and reverse value. |
sorting |
An array of sort option objects defined in lib/constants, each containing a URL-safe slug, a Shopify sortKey enum value, and a reverse boolean. Maps user-facing sort options to Shopify API parameters. |
handle |
A URL-safe, human-readable string identifier for a Shopify resource (product, collection, or page). Unique per resource type. Used as the dynamic route segment on Product Detail, Search Detail, and Item Detail screens. |
page.body |
The full HTML content of a Shopify Page, authored in Shopify's rich text editor and returned as an HTML string by the Storefront API. |
page.bodySummary |
A plain-text excerpt or summary of a Shopify Page body, provided by Shopify and used as a fallback for the SEO meta description on Item Detail pages. |
page.seo |
An optional object on a Shopify Page containing title and description fields that override the default title and body summary for search engine metadata. |
indexable |
A derived boolean computed at render time on the Product Detail screen. true if the product does not carry HIDDEN_PRODUCT_TAG; false if it does. Controls the robots metadata directives. |
| Term | Definition in this context |
|---|---|
| Structural rule | A BABOK v3.0 rule category. Defines constraints on data or entities — what must be true about the structure of data. Example: "A product gallery must contain at most 5 images." |
| Operative rule | A BABOK v3.0 rule category. Governs processes or actions — what must happen under certain conditions. Example: "If a product handle is not found, the application must render a 404 page." |
| Decision rule | A BABOK v3.0 rule category. Defines condition-based outcomes — if [condition], then [result]. Example: "If availableForSale is true, then JSON-LD availability is InStock." |
| Hit Policy | A DMN v1.5 attribute on a decision table specifying how overlapping rows are resolved. Unique: only one row can match. First: first matching row wins. Any: multiple rows may match but produce the same result. Collect: all matching rows are applied. |
| Enforcement point | Where a rule is evaluated and enforced. Client: in the browser (UI/form). Server: on the server (API/backend). Both: enforced at both layers. |
| Criticality | The severity of impact if a rule is violated. Critical: data corruption or security breach. High: broken workflows or incorrect business behavior. Medium: degraded user experience. Low: cosmetic or minor quality issues. |
| Term | Definition in this context |
|---|---|
AggregateOffer |
A schema.org type used in JSON-LD to represent a product available at a range of prices (across variants), as opposed to a single fixed price (Offer). Used in the product JSON-LD block to represent multi-variant pricing. |
JSON-LD |
JavaScript Object Notation for Linked Data — a structured data format embedded in a <script type="application/ld+json"> tag that search engines (especially Google) use to understand page content and generate rich search results. |
sortKey |
A Shopify Storefront API enum value (e.g., PRICE, BEST_SELLING, CREATED_AT, TITLE) that determines the field by which products are sorted in collection and search queries. |
reverse |
A boolean passed to the Shopify Storefront API that inverts the sort direction when true (e.g., highest price first instead of lowest). |
slug |
A URL-safe string identifier for a sort option (e.g., "price-asc", "trending"), used to match the ?sort= query parameter to a sort configuration object in the sorting constants array. |
Prose |
An internal component (components/prose) that renders a raw HTML string with typographic styling applied, similar to Tailwind CSS's prose utility class pattern. Used on the Item Detail screen to render page.body. |
notFound() |
A Next.js App Router function from next/navigation that, when called, interrupts rendering and triggers the nearest not-found.tsx boundary, resulting in a 404 HTTP response. |
generateMetadata |
A Next.js App Router convention — an exported async function from a page file that returns a Metadata object used to populate <head> tags (title, description, Open Graph, robots directives, etc.). |
ThreeItemGrid |
A custom component (components/grid/three-items) that renders a curated grid of exactly three featured products, used as the hero/above-the-fold section of the storefront homepage. |
Carousel |
A custom component (components/carousel) that renders a horizontally scrollable list of products, used on the homepage to expose a broader product catalog. |
ProductGridItems |
A component (components/layout/product-grid-items) that maps over a products array and renders individual product cards in a grid layout, including image, title, and price. |
GridTileImage |
A reusable image tile component (components/grid/tile) used in grid and list layouts, supporting a label overlay with title and price. Used for related product tiles on the Product Detail screen. |
RelatedProducts |
An async React Server Component on the Product Detail screen that fetches and renders Shopify's algorithmic product recommendations for the current product. Returns null if no recommendations exist. |
| Open Graph | A metadata protocol (originally by Facebook) that controls how a URL is represented when shared on social media platforms. This application sets og:type, og:published_time, and og:modified_time on relevant screens. |
Intl.DateTimeFormat |
A built-in JavaScript API for locale-aware date and time formatting. Used on the Item Detail screen to render page.updatedAt as a human-readable string using the server's default locale. |
End of Business Rules Assessment — Commerce Application — May 2026