myowjaYOY/commerce
April 19, 2026
Business Rules Assessment
commerce
Generated by DocAgent — automated codebase documentation analysis. Based on analysis of 5 screens. Subject matter expert review is recommended before distribution.
April 20, 2026
Commerce Application
Business Rules Assessment
April 2026
Scope. This Business Rules Assessment documents all business rules, validations, calculations, and conditional logic observable from the implemented screen documentation for the Commerce application. Rules are extracted from five screens: Home (/), Item Detail (/[page]), Product Detail (/product/[handle]), Search (/search), and Search Detail (/search/[collection]). Rules embedded in child components whose source code was not provided (e.g., ProductDescription, Gallery, Carousel, ThreeItemGrid, Footer, ProductGridItems) are outside the scope of this document, as are rules governing checkout, cart, account management, and any other routes not listed above.
Methodology. Rules are reverse-engineered from technical screen documentation written for developers. Each rule is expressed declaratively per SBVR principles — stating what must be true rather than how the code implements it. Rules are classified using BABOK v3.0 categories (Structural, Operative, Decision), structured using DMN v1.5 conventions for decision tables, and governed per Business Rules Manifesto v2.0 principles of rule independence. Enforcement points (Client, Server, Both) are assigned based on where the rule logic is described as executing in the documentation.
Limitations. This document is derived entirely from per-screen technical documentation, not from requirements documents, policy manuals, or stakeholder interviews. Business rationale is stated only where the documentation explicitly provides it; all other rationale is marked as not documented or formally inferred. Rules implemented within undocumented child components cannot be cataloged here. Because all five documented screens are React Server Components with no client-side state, the majority of rules are enforced server-side. This document reflects a partial view of the application — rules from screens not included in the assessed documentation are outside scope.
Coverage. This Business Rules Assessment covers 5 screens of the Commerce application:
Home (/)
Item Detail (/[page])
Product Detail (/product/[handle])
Search (/search)
Search Detail (/search/[collection])
Business rules derived from screens not included in the assessed documentation are outside the scope of this document.
How to use this document. Use the Rule Inventory for quick lookup by domain, category, or criticality. Use the Business Rules Catalog for full rule details including conditions, validation logic, and testing implications. Use the Decision Tables for multi-condition conditional logic. Use the Gap Analysis to identify risks, missing rules, and items requiring stakeholder verification.
Disclosure. Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
| # | Rule Name | Domain | Category | Enforcement | Criticality | Screen(s) |
|---|---|---|---|---|---|---|
| BR-001 | Public Page Requires No Authentication | Content Visibility & Access Control | Structural | Server | Low | Home (/), Item Detail (/[page]), Product Detail (/product/[handle]), Search (/search), Search Detail (/search/[collection]) |
| BR-002 | Invalid Page Handle Returns 404 | Content Visibility & Access Control | Operative | Server | High | Item Detail (/[page]) |
| BR-003 | Invalid Product Handle Returns 404 | Content Visibility & Access Control | Operative | Server | High | Product Detail (/product/[handle]) |
| BR-004 | Invalid Collection Slug Returns 404 | Content Visibility & Access Control | Operative | Server | High | Search Detail (/search/[collection]) |
| BR-005 | Hidden Product Excluded from Search Indexing | Content Visibility & Access Control | Operative | Server | Medium | Product Detail (/product/[handle]) |
| BR-006 | Hidden Product Remains Directly Accessible | Content Visibility & Access Control | Structural | Server | Medium | Product Detail (/product/[handle]) |
| BR-007 | Product Image Gallery Capped at Five Images | Product Display | Structural | Server | Low | Product Detail (/product/[handle]) |
| BR-008 | Related Products Section Suppressed When Empty | Product Display | Operative | Server | Low | Product Detail (/product/[handle]) |
| BR-009 | Product Availability Expressed in Structured Data | Product Data Integrity | Decision | Server | Medium | Product Detail (/product/[handle]) |
| BR-010 | Product Price Range Expressed as Aggregate Offer | Product Data Integrity | Structural | Server | Low | Product Detail (/product/[handle]) |
| BR-011 | Item Detail SEO Title Fallback Chain | SEO & Metadata | Decision | Server | Medium | Item Detail (/[page]) |
| BR-012 | Item Detail SEO Description Fallback Chain | SEO & Metadata | Decision | Server | Medium | Item Detail (/[page]) |
| BR-013 | Item Detail Classified as OpenGraph Article | SEO & Metadata | Structural | Server | Low | Item Detail (/[page]) |
| BR-014 | Item Detail OpenGraph Timestamps from Shopify Dates | SEO & Metadata | Structural | Server | Low | Item Detail (/[page]) |
| BR-015 | Product Detail SEO Title Fallback Chain | SEO & Metadata | Decision | Server | Medium | Product Detail (/product/[handle]) |
| BR-016 | Product Detail SEO Description Fallback Chain | SEO & Metadata | Decision | Server | Medium | Product Detail (/product/[handle]) |
| BR-017 | Product OpenGraph Image Conditionally Included | SEO & Metadata | Decision | Server | Low | Product Detail (/product/[handle]) |
| BR-018 | Collection SEO Title Fallback Chain | SEO & Metadata | Decision | Server | Medium | Search Detail (/search/[collection]) |
| BR-019 | Collection SEO Description Fallback Chain | SEO & Metadata | Decision | Server | Medium | Search Detail (/search/[collection]) |
| BR-020 | Home Page Classified as OpenGraph Website | SEO & Metadata | Structural | Server | Low | Home (/) |
| BR-021 | Sort Parameter Resolved with Default Fallback | Search & Filtering | Decision | Server | Medium | Search (/search), Search Detail (/search/[collection]) |
| BR-022 | Search Results Summary Shown Only with Query | Search & Filtering | Operative | Server | Low | Search (/search) |
| BR-023 | Zero Search Results Displays No-Results Message | Search & Filtering | Operative | Server | Medium | Search (/search) |
| BR-024 | Product Grid Suppressed When No Products | Search & Filtering | Operative | Server | Low | Search (/search) |
| BR-025 | Results Count Text Grammatically Pluralized | Search & Filtering | Decision | Server | Low | Search (/search) |
| BR-026 | Empty Collection Displays No-Products Message | Collection Browsing | Operative | Server | Medium | Search Detail (/search/[collection]) |
| BR-027 | Last Updated Date Formatted with Runtime Locale | Product Data Integrity | Structural | Server | Low | Item Detail (/[page]) |
| BR-028 | Related Product Links Prefetched on Viewport Entry | Product Display | Operative | Client | Low | Product Detail (/product/[handle]) |
3.1: Content Visibility & Access Control
Description: Rules governing which pages are publicly accessible, which URL handles produce a 404 response, and how product visibility is controlled for search engine indexing.
Screens involved: Home (/), Item Detail (/[page]), Product Detail (/product/[handle]), Search (/search), Search Detail (/search/[collection])
BR-001: Public Page Requires No Authentication
Domain: Content Visibility & Access Control Category: Structural Enforcement: Server Criticality: Low
Rule Statement: All five documented screens must be accessible to any visitor without authentication or role-based authorization.
Conditions: Not applicable — this is an unconditional structural constraint.
Validation Details:
Input: Any HTTP request to /, /[page], /product/[handle], /search, or /search/[collection]
Rule logic: No middleware guard, session check, or conditional rendering based on user identity is present in any of the five documented page components
Pass behavior: Page renders and is served to the requesting visitor
Fail behavior: Not applicable — no access denial mechanism exists at the page level for these routes
Source Evidence: Home (/) §2, Item Detail (/[page]) §2, Product Detail (/product/[handle]) §2, Search (/search) §2, Search Detail (/search/[collection]) §2
Business Rationale: Public storefront pages must be accessible to all potential customers without requiring account creation or login, maximizing reach and enabling search engine crawling.
Related Rules: BR-002, BR-003, BR-004, BR-005
Testing Implications:
Positive: Confirm all five routes return HTTP 200 with no Authorization header or session cookie present
Negative: Not applicable — no auth gate to bypass
Edge case: Confirm that authenticated users also receive the same public content (no role-based content switching)
BR-002: Invalid Page Handle Returns 404
Domain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: High
Rule Statement: If a requested Shopify page handle does not resolve to an existing page, the application must return a 404 Not Found response rather than rendering an empty or broken page.
Conditions:
Condition 1: getPage(params.page) returns a falsy value (null, undefined, or empty)
Action/Result: notFound() is called, halting rendering and triggering the Next.js 404 response
Validation Details:
Input: The page URL segment string passed to getPage
Rule logic: The return value of getPage is evaluated for truthiness; if falsy, notFound() is invoked
Pass behavior: A valid page object is returned; the page renders normally
Fail behavior: notFound() is called; Next.js renders the application's 404 page
Source Evidence: Item Detail (/[page]) §8, §4, §9
Business Rationale: Prevents arbitrary URL segments from producing rendered pages with empty or undefined content, maintaining content integrity and a consistent user experience for broken links.
Related Rules: BR-001
Testing Implications:
Positive: Request /valid-handle where the handle exists in Shopify → page renders
Negative: Request /nonexistent-handle → HTTP 404 response
Edge case: Request / (root) — confirm it does not conflict with the Home route; request a handle that is a valid route prefix (e.g., /search) — confirm routing precedence is correct
BR-003: Invalid Product Handle Returns 404
Domain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: High
Rule Statement: If a requested Shopify product handle does not resolve to an existing product, the application must return a 404 Not Found response.
Conditions:
Condition 1: getProduct(handle) returns null
Action/Result: notFound() is called in both generateMetadata and ProductPage, halting rendering
Validation Details:
Input: The handle route parameter from /product/[handle]
Rule logic: Return value of getProduct is checked; if null, notFound() is invoked
Pass behavior: Product object is returned; page renders with product data
Fail behavior: notFound() called; Next.js 404 page rendered. This behavior is consistent across both the metadata generation phase and the page render phase
Source Evidence: Product Detail (/product/[handle]) §4, §8, §9, §10
Business Rationale: Prevents broken product URLs from rendering empty pages, ensuring consistent 404 behavior for invalid or removed product handles.
Testing Implications:
Positive: Request /product/valid-handle → page renders with product data
Negative: Request /product/nonexistent-handle → HTTP 404
Edge case: Request /product/ (empty handle) — confirm routing behavior; test a handle that was valid but has since been deleted from Shopify
BR-004: Invalid Collection Slug Returns 404
Domain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: High
Rule Statement: If a requested collection slug does not resolve to an existing Shopify collection, the application must return a 404 Not Found response.
Conditions:
Condition 1: getCollection(params.collection) returns a falsy value during metadata generation
Action/Result: notFound() is called, aborting metadata generation and rendering the 404 page
Validation Details:
Input: The collection URL segment from /search/[collection]
Rule logic: Return value of getCollection is evaluated for truthiness; if falsy, notFound() is invoked
Pass behavior: A valid collection object is returned; metadata is generated and the page renders
Fail behavior: notFound() called during generateMetadata; Next.js 404 page rendered
Source Evidence: Search Detail (/search/[collection]) §2, §5, §8, §10
Business Rationale: Ensures that only valid, existing Shopify collections can be viewed, preventing broken or empty collection pages from being served.
Testing Implications:
Positive: Request /search/valid-collection → page renders with collection products
Negative: Request /search/nonexistent-collection → HTTP 404
Edge case: A collection that exists in Shopify but has been unpublished; a collection slug containing special characters
BR-005: Hidden Product Excluded from Search Indexing
Domain: Content Visibility & Access Control Category: Operative Enforcement: Server Criticality: Medium
Rule Statement: If a product is tagged with HIDDEN_PRODUCT_TAG, the product detail page must be marked noindex, nofollow in its metadata, preventing search engine indexing and link following.
Conditions:
Condition 1: product.tags includes the value of HIDDEN_PRODUCT_TAG
Action/Result: robots.index = false, robots.follow = false, and equivalent googleBot directives are set in the page metadata
Validation Details:
Input: product.tags array from the Shopify product object
Rule logic: indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG). The indexable boolean is applied to robots.index, robots.follow, and googleBot metadata fields
Pass behavior: Product is not tagged → indexable = true → page is indexed and followed by search engines
Fail behavior: Product is tagged → indexable = false → noindex, nofollow directives are emitted in <head> metadata
Source Evidence: Product Detail (/product/[handle]) §2, §8, §14
Business Rationale: Not stated in documentation — verify with business stakeholders. [Not documented — WHO: Product owner or merchandising team; WHAT: What is the business purpose of the HIDDEN_PRODUCT_TAG mechanism — e.g., staging products, discontinued items, internal SKUs?; WHERE: Insert in the Business Rationale field of BR-005]
Testing Implications:
Positive: Product without HIDDEN_PRODUCT_TAG → <meta name="robots" content="index, follow"> in rendered HTML
Negative: Product with HIDDEN_PRODUCT_TAG → <meta name="robots" content="noindex, nofollow"> in rendered HTML
Edge case: Product with multiple tags including HIDDEN_PRODUCT_TAG alongside other tags; confirm tag matching is case-sensitive or case-insensitive per the includes implementation
BR-006: Hidden Product Remains Directly Accessible
Domain: Content Visibility & Access Control Category: Structural Enforcement: Server Criticality: Medium
Rule Statement: A product tagged with HIDDEN_PRODUCT_TAG must remain accessible via its direct URL; the tag must not prevent page rendering or trigger a 404 response.
Conditions: Not applicable — this is an unconditional structural constraint for tagged products.
Validation Details:
Input: A direct HTTP request to /product/[handle] for a product tagged with HIDDEN_PRODUCT_TAG
Rule logic: The HIDDEN_PRODUCT_TAG check only affects metadata robots directives; it does not call notFound() or alter the page render
Pass behavior: Page renders normally with full product content
Fail behavior: Not applicable — no access denial is implemented for hidden products
Source Evidence: Product Detail (/product/[handle]) §2, §8, §14
Business Rationale: Not stated in documentation — verify with business stakeholders. [Not documented — WHO: Product owner; WHAT: Is it intentional that hidden products are fully accessible via direct URL, or should they also be access-controlled?; WHERE: Insert in the Business Rationale field of BR-006]
Related Rules: BR-005
Testing Implications:
Positive: Direct URL request to a hidden product → HTTP 200, full page content rendered
Negative: Not applicable
Edge case: Confirm that a hidden product does not appear in search results or related products (rules governing those components are outside the scope of this document)
3.2: Product Data Integrity
Description: Rules governing the accuracy and completeness of product data as presented to users and search engines, including availability signaling, price representation, and date formatting.
Screens involved: Item Detail (/[page]), Product Detail (/product/[handle])
BR-009: Product Availability Expressed in Structured Data
Domain: Product Data Integrity Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The JSON-LD structured data for a product must express availability as https://schema.org/InStock when the product is available for sale, and https://schema.org/OutOfStock otherwise.
Conditions:
Condition 1: product.availableForSale === true
Action/Result: offers.availability set to "https://schema.org/InStock" in JSON-LD
Condition 2: product.availableForSale === false
Action/Result: offers.availability set to "https://schema.org/OutOfStock" in JSON-LD
Validation Details:
Input: product.availableForSale boolean from the Shopify product object
Rule logic: Ternary expression: product.availableForSale ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
Pass behavior: Correct Schema.org availability URL is emitted in the <script type="application/ld+json"> tag
Fail behavior: Not applicable — the rule always produces one of two valid outputs
Source Evidence: Product Detail (/product/[handle]) §8
Business Rationale: Communicates stock status to search engines via structured data, enabling accurate rich result display (e.g., "In Stock" badges in Google Shopping).
Related Rules: BR-010
Testing Implications:
Positive: Product with availableForSale: true → JSON-LD contains "availability": "https://schema.org/InStock"
Negative: Product with availableForSale: false → JSON-LD contains "availability": "https://schema.org/OutOfStock"
Edge case: Confirm behavior when availableForSale is null or undefined (not documented — may default to OutOfStock via falsy evaluation)
BR-010: Product Price Range Expressed as Aggregate Offer
Domain: Product Data Integrity Category: Structural Enforcement: Server Criticality: Low
Rule Statement: The JSON-LD structured data for a product must represent pricing using the AggregateOffer type, specifying both highPrice from priceRange.maxVariantPrice.amount and lowPrice from priceRange.minVariantPrice.amount.
Conditions: Not applicable — this is an unconditional structural constraint applied to all product detail pages.
Validation Details:
Input: product.priceRange.maxVariantPrice.amount and product.priceRange.minVariantPrice.amount from the Shopify product object
Rule logic: Both values are mapped directly into the AggregateOffer JSON-LD object
Pass behavior: JSON-LD emits "@type": "AggregateOffer" with both highPrice and lowPrice populated
Fail behavior: Not documented — if either price field is absent, the JSON-LD may emit null or undefined values, which could invalidate the structured data
Source Evidence: Product Detail (/product/[handle]) §8
Business Rationale: Accurately represents products with multiple variant price points to search engines, enabling correct price range display in rich results.
Related Rules: BR-009
Testing Implications:
Positive: Product with multiple variants at different prices → JSON-LD highPrice ≠ lowPrice
Positive: Product with a single variant → JSON-LD highPrice === lowPrice
Edge case: Product with no variants or missing priceRange fields — confirm no runtime error is thrown
BR-027: Last Updated Date Formatted with Runtime Locale
Domain: Product Data Integrity Category: Structural Enforcement: Server Criticality: Low
Rule Statement: The "last updated" date displayed on an Item Detail page must be formatted using the server runtime's default locale with year: "numeric", month: "long", and day: "numeric" options.
Conditions: Not applicable — this formatting is applied unconditionally to page.updatedAt.
Validation Details:
Input: page.updatedAt — an ISO 8601 date string from the Shopify API
Rule logic: new Intl.DateTimeFormat(undefined, { year: "numeric", month: "long", day: "numeric" }).format(new Date(page.updatedAt)). The undefined locale argument resolves to the server's runtime locale, not the user's browser locale
Pass behavior: A human-readable date string (e.g., "April 15, 2026") is rendered in a small italic paragraph
Fail behavior: If page.updatedAt is malformed or unparseable, new Date(page.updatedAt) produces an Invalid Date object, and Intl.DateTimeFormat.format() outputs the string "Invalid Date" visibly on the page
Source Evidence: Item Detail (/[page]) §3, §4, §8, §10
Business Rationale: Not stated in documentation — verify with business stakeholders. [Not documented — WHO: Product owner; WHAT: Should the date be formatted in the user's browser locale rather than the server locale? Is the current server-locale behavior intentional?; WHERE: Insert in the Business Rationale field of BR-027 and note in GAP-04]
Related Rules: None
Testing Implications:
Positive: Valid ISO 8601 updatedAt string → formatted date rendered correctly
Negative: Malformed updatedAt string (e.g., "not-a-date") → "Invalid Date" string rendered on page
Edge case: Server running in a non-English locale — confirm date format matches expected output; updatedAt as null — confirm no runtime crash
3.3: SEO & Metadata
Description: Rules governing the generation of <head> metadata, Open Graph tags, robots directives, and JSON-LD structured data across all documented screens.
Screens involved: Home (/), Item Detail (/[page]), Product Detail (/product/[handle]), Search Detail (/search/[collection])
BR-011: Item Detail SEO Title Fallback Chain
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The SEO title for an Item Detail page must use page.seo.title if present; otherwise it must fall back to page.title.
Conditions:
Condition 1: page.seo?.title is truthy
Action/Result: page.seo.title is used as the metadata title
Condition 2: page.seo?.title is falsy (null, undefined, or empty)
Action/Result: page.title is used as the metadata title
Validation Details:
Input: page.seo object (optional) and page.title string from the Shopify API
Rule logic: Optional chaining page.seo?.title with fallback via || or ?? to page.title
Pass behavior: A non-empty title string is emitted in the <title> tag and OpenGraph og:title
Fail behavior: If both page.seo.title and page.title are empty, an empty title is emitted (no guard documented)
Source Evidence: Item Detail (/[page]) §8
Business Rationale: Ensures that pages without explicit SEO overrides in Shopify still produce meaningful metadata, while allowing merchants to customize SEO titles independently of display titles.
Related Rules: BR-012
Testing Implications:
Positive: Page with seo.title set → metadata title equals seo.title
Positive: Page with seo.title absent → metadata title equals page.title
Edge case: Page with seo.title as empty string — confirm whether empty string is treated as falsy and triggers fallback
BR-012: Item Detail SEO Description Fallback Chain
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The SEO description for an Item Detail page must use page.seo.description if present; otherwise it must fall back to page.bodySummary.
Conditions:
Condition 1: page.seo?.description is truthy
Action/Result: page.seo.description is used as the metadata description
Condition 2: page.seo?.description is falsy
Action/Result: page.bodySummary is used as the metadata description
Validation Details:
Input: page.seo object (optional) and page.bodySummary string from the Shopify API
Rule logic: Optional chaining page.seo?.description with fallback to page.bodySummary
Pass behavior: A description string is emitted in <meta name="description"> and og:description
Fail behavior: If both values are absent or empty, an empty description is emitted
Source Evidence: Item Detail (/[page]) §8
Business Rationale: Ensures that pages without explicit SEO descriptions still produce meaningful metadata using the page body summary, while allowing merchant SEO overrides.
Related Rules: BR-011
Testing Implications:
Positive: Page with seo.description set → metadata description equals seo.description
Positive: Page with seo.description absent → metadata description equals bodySummary
Edge case: Both seo.description and bodySummary are empty strings
BR-013: Item Detail Classified as OpenGraph Article
Domain: SEO & Metadata Category: Structural Enforcement: Server Criticality: Low
Rule Statement: All Item Detail pages must set openGraph.type to "article" in their metadata.
Conditions: Not applicable — this is applied unconditionally to all /[page] routes.
Validation Details:
Input: No input — this is a static metadata assignment
Rule logic: openGraph.type = "article" is set unconditionally in generateMetadata
Pass behavior: <meta property="og:type" content="article"> is emitted in the page <head>
Fail behavior: Not applicable
Source Evidence: Item Detail (/[page]) §8
Business Rationale: Classifies all Shopify CMS pages as article-type content for social sharing and SEO purposes, enabling article-specific rich previews on social platforms.
Testing Implications:
Positive: Any valid Item Detail page → og:type equals "article" in rendered HTML head
Edge case: Confirm this does not conflict with any parent layout that also sets og:type
BR-014: Item Detail OpenGraph Timestamps from Shopify Dates
Domain: SEO & Metadata Category: Structural Enforcement: Server Criticality: Low
Rule Statement: Item Detail page metadata must populate openGraph.publishedTime from page.createdAt and openGraph.modifiedTime from page.updatedAt.
Conditions: Not applicable — applied unconditionally when the page object is valid.
Validation Details:
Input: page.createdAt and page.updatedAt ISO 8601 date strings from the Shopify API
Rule logic: Direct assignment of page.createdAt to publishedTime and page.updatedAt to modifiedTime in the OpenGraph metadata block
Pass behavior: og:article:published_time and og:article:modified_time tags are emitted with ISO 8601 values
Fail behavior: If either date field is absent or malformed, the corresponding OpenGraph tag may emit null or undefined (behavior not explicitly documented)
Source Evidence: Item Detail (/[page]) §8
Business Rationale: Provides social platforms and search engines with accurate publication and modification timestamps for article-type content, supporting freshness signals in search ranking.
Testing Implications:
Positive: Valid page → og:article:published_time equals createdAt, og:article:modified_time equals updatedAt
Edge case: createdAt or updatedAt is null — confirm no malformed tag is emitted
BR-015: Product Detail SEO Title Fallback Chain
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The SEO title for a Product Detail page must use product.seo.title if present; otherwise it must fall back to product.title.
Conditions:
Condition 1: product.seo.title is truthy
Action/Result: product.seo.title is used as the metadata title
Condition 2: product.seo.title is falsy
Action/Result: product.title is used as the metadata title
Validation Details:
Input: product.seo.title and product.title from the Shopify product object
Rule logic: Fallback expression product.seo.title || product.title
Pass behavior: A non-empty title string is emitted in <title> and og:title
Fail behavior: If both are absent, an empty title is emitted
Source Evidence: Product Detail (/product/[handle]) §8
Business Rationale: Allows merchants to set SEO-specific titles distinct from display titles, while ensuring all products have meaningful metadata even without explicit SEO configuration.
Testing Implications:
Positive: Product with seo.title set → metadata title equals seo.title
Positive: Product with seo.title absent → metadata title equals product.title
Edge case: seo.title is an empty string — confirm fallback behavior
BR-016: Product Detail SEO Description Fallback Chain
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The SEO description for a Product Detail page must use product.seo.description if present; otherwise it must fall back to product.description.
Conditions:
Condition 1: product.seo.description is truthy
Action/Result: product.seo.description is used as the metadata description
Condition 2: product.seo.description is falsy
Action/Result: product.description is used as the metadata description
Validation Details:
Input: product.seo.description and product.description from the Shopify product object
Rule logic: Fallback expression product.seo.description || product.description
Pass behavior: A description string is emitted in <meta name="description"> and og:description
Fail behavior: If both are absent, an empty description is emitted
Source Evidence: Product Detail (/product/[handle]) §8
Business Rationale: Allows merchants to set SEO-specific descriptions, while ensuring all products have meaningful metadata using the product description as a fallback.
Testing Implications:
Positive: Product with seo.description set → metadata description equals seo.description
Positive: Product with seo.description absent → metadata description equals product.description
Edge case: Both seo.description and product.description are empty strings
BR-017: Product OpenGraph Image Conditionally Included
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Low
Rule Statement: The OpenGraph image metadata for a Product Detail page must only be included if product.featuredImage.url is truthy; if absent, the OpenGraph images array must be omitted or set to null.
Conditions:
Condition 1: product.featuredImage is truthy and product.featuredImage.url is truthy
Action/Result: OpenGraph images array is populated with the featured image URL and dimensions
Condition 2: product.featuredImage is falsy or product.featuredImage.url is falsy
Action/Result: OpenGraph images metadata is set to null or omitted
Validation Details:
Input: product.featuredImage object from the Shopify product object
Rule logic: Conditional check on product.featuredImage.url truthiness before populating the OpenGraph images array
Pass behavior: og:image tag is emitted with the featured image URL
Fail behavior: og:image tag is omitted from the page <head>
Source Evidence: Product Detail (/product/[handle]) §8, §10
Business Rationale: Prevents broken or empty og:image tags from being emitted for products without a featured image, which could degrade social sharing previews.
Testing Implications:
Positive: Product with featuredImage.url set → og:image tag present in <head>
Negative: Product with no featuredImage → no og:image tag in <head>
Edge case: featuredImage object exists but url is an empty string — confirm conditional evaluates correctly
BR-018: Collection SEO Title Fallback Chain
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The SEO title for a Search Detail page must use collection.seo.title if present; otherwise it must fall back to collection.title.
Conditions:
Condition 1: collection.seo.title is truthy
Action/Result: collection.seo.title is used as the metadata title
Condition 2: collection.seo.title is falsy
Action/Result: collection.title is used as the metadata title
Validation Details:
Input: collection.seo.title and collection.title from the Shopify collection object
Rule logic: Fallback expression collection.seo.title || collection.title
Pass behavior: A non-empty title string is emitted in <title> and og:title
Fail behavior: If both are absent, an empty title is emitted
Source Evidence: Search Detail (/search/[collection]) §8
Business Rationale: Allows merchants to set SEO-specific collection titles, while ensuring all collection pages have meaningful metadata using the collection display title as a fallback.
Related Rules: BR-019, BR-011, BR-015
Testing Implications:
Positive: Collection with seo.title set → metadata title equals seo.title
Positive: Collection with seo.title absent → metadata title equals collection.title
Edge case: seo.title is an empty string — confirm fallback behavior
BR-019: Collection SEO Description Fallback Chain
Domain: SEO & Metadata Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The SEO description for a Search Detail page must use collection.seo.description if present; if absent, it must fall back to collection.description; if that is also absent, it must fall back to the generated string "${collection.title} products".
Conditions:
Condition 1: collection.seo.description is truthy
Action/Result: collection.seo.description is used as the metadata description
Condition 2: collection.seo.description is falsy and collection.description is truthy
Action/Result: collection.description is used as the metadata description
Condition 3: Both collection.seo.description and collection.description are falsy
Action/Result: The generated string "${collection.title} products" is used as the metadata description
Validation Details:
Input: collection.seo.description, collection.description, and collection.title from the Shopify collection object
Rule logic: Three-level fallback chain
Pass behavior: A non-empty description string is always emitted (the third fallback guarantees a non-empty value as long as collection.title is non-empty)
Fail behavior: If collection.title is also empty, the generated fallback produces " products" (a string with a leading space)
Source Evidence: Search Detail (/search/[collection]) §8
Business Rationale: Ensures all collection pages have a meaningful SEO description even when merchants have not configured explicit SEO fields, using a generated description as a last resort.
Testing Implications:
Positive: Collection with seo.description set → metadata description equals seo.description
Positive: seo.description absent, description present → metadata description equals collection.description
Positive: Both absent → metadata description equals "${collection.title} products"
Edge case: All three values absent or empty — confirm output is " products" and assess whether this is acceptable
BR-020: Home Page Classified as OpenGraph Website
Domain: SEO & Metadata Category: Structural Enforcement: Server Criticality: Low
Rule Statement: The Home page must set openGraph.type to "website" in its metadata.
Conditions: Not applicable — this is applied unconditionally to the / route.
Validation Details:
Input: No input — this is a static metadata assignment
Rule logic: openGraph.type = "website" is set in the metadata export of the Home page component
Pass behavior: <meta property="og:type" content="website"> is emitted in the page <head>
Fail behavior: Not applicable
Source Evidence: Home (/) §13
Business Rationale: Identifies the home page as a generic website page for social sharing platforms, as opposed to article, product, or other content-specific types.
Related Rules: BR-013
Testing Implications:
Positive: Home page → og:type equals "website" in rendered HTML head
Edge case: Confirm no parent layout overrides this value
3.4: Search & Filtering
Description: Rules governing the display of search results, sort parameter resolution, results summary messaging, and product grid visibility on the Search screen.
Screens involved: Search (/search), Search Detail (/search/[collection])
BR-021: Sort Parameter Resolved with Default Fallback
Domain: Search & Filtering Category: Decision Enforcement: Server Criticality: Medium
Rule Statement: The sort URL query parameter must be resolved to a valid { sortKey, reverse } pair by matching against the sorting constants array; if no match is found, defaultSort must be used.
Conditions:
Condition 1: sort query parameter matches a slug in the sorting array
Action/Result: The matching entry's sortKey and reverse values are used for the API query
Condition 2: sort query parameter is absent, unrecognized, or does not match any slug
Action/Result: defaultSort's sortKey and reverse values are used; no error is shown to the user
Validation Details:
Input: sort string from URL query parameters (searchParams.sort)
Rule logic: sorting.find((item) => item.slug === sort) || defaultSort — strict equality match against the slug field of each entry in the sorting array
Pass behavior: A valid { sortKey, reverse } pair is always produced and passed to the Shopify API
Fail behavior: Invalid or absent sort value → silently falls back to defaultSort; no user-facing error or message
Source Evidence: Search (/search) §4, §7, §8; Search Detail (/search/[collection]) §4, §7, §8
Business Rationale: Prevents invalid sort values from reaching the Shopify API, ensuring the page always renders with a valid product ordering even when the URL is manually edited or a link is malformed.
Testing Implications:
Positive: ?sort=price-asc (valid slug) → products sorted by price ascending
Negative: ?sort=invalid-value → products sorted by defaultSort configuration
Negative: No sort parameter → products sorted by defaultSort configuration
Edge case: ?sort= (empty string) — confirm fallback behavior; ?sort=PRICE (uppercase, not matching slug format) — confirm no match and fallback
BR-022: Search Results Summary Shown Only with Query
Domain: Search & Filtering Category: Operative Enforcement: Server Criticality: Low
Rule Statement: The search results summary paragraph must only be rendered when a search query (q parameter) is present in the URL.
Conditions:
Condition 1: searchValue (from ?q) is truthy
Action/Result: Results summary paragraph is rendered (showing either a count or a no-results message)
Condition 2: searchValue is falsy (absent or empty)
Action/Result: Results summary paragraph is not rendered
Validation Details:
Input: searchValue derived from searchParams.q
Rule logic: Conditional rendering: {searchValue && (<p>...</p>)}
Pass behavior: Summary paragraph appears when a query is present
Fail behavior: Summary paragraph is suppressed when no query is present
Source Evidence: Search (/search) §3, §8
Business Rationale: Avoids displaying a confusing results summary when the user is browsing all products without a search term.
Related Rules: BR-023, BR-024, BR-025
Testing Implications:
Positive: /search?q=shoes → summary paragraph rendered
Negative: /search (no q param) → no summary paragraph rendered
Edge case: /search?q= (empty string) — confirm whether empty string is treated as falsy
BR-023: Zero Search Results Displays No-Results Message
Domain: Search & Filtering Category: Operative Enforcement: Server Criticality: Medium
Rule Statement: When a search query is present and returns zero products, the application must display the message "There are no products that match "<term>"" and must not render a product grid.
Conditions:
Condition 1: searchValue is truthy AND products.length === 0
Action/Result: No-results message is displayed; product grid is not rendered
Validation Details:
Input: searchValue (from ?q) and products array returned by getProducts
Rule logic: The results summary paragraph renders the no-results message when products.length === 0 and searchValue is truthy; the product grid is suppressed by the products.length > 0 condition on BR-024
Pass behavior: User sees "There are no products that match "<term>"" with the search term in bold
Fail behavior: Not applicable — this is the defined behavior for zero results
Source Evidence: Search (/search) §3, §8, §10
Business Rationale: Provides clear user feedback when a search returns no results, preventing a confusing blank page.
Testing Implications:
Positive: /search?q=xyznonexistent (no matching products) → no-results message displayed, no grid rendered
Edge case: Search term containing special characters (quotes, angle brackets) — confirm correct escaping in the rendered message
BR-024: Product Grid Suppressed When No Products
Domain: Search & Filtering Category: Operative Enforcement: Server Criticality: Low
Rule Statement: The product grid must only be rendered when the products array contains at least one item.
Conditions:
Condition 1: products.length > 0
Action/Result: Grid and ProductGridItems are rendered
Condition 2: products.length === 0
Action/Result: Grid components are not rendered
Validation Details:
Input: products array returned by getProducts
Rule logic: Conditional rendering: {products.length > 0 && (<Grid>...</Grid>)}
Pass behavior: Grid renders when products exist
Fail behavior: Grid is suppressed when no products exist
Source Evidence: Search (/search) §3, §8
Business Rationale: Prevents an empty grid container from being rendered, which could produce a confusing blank layout area.
Testing Implications:
Positive: Search returns products → grid rendered
Negative: Search returns no products → grid not rendered, no empty container in DOM
Edge case: Exactly one product returned — confirm grid renders correctly with a single item
BR-025: Results Count Text Grammatically Pluralized
Domain: Search & Filtering Category: Decision Enforcement: Server Criticality: Low
Rule Statement: The results count label must display "result" (singular) when exactly one product is returned, and "results" (plural) when more than one product is returned.
Conditions:
Condition 1: products.length > 1
Action/Result: resultsText = "results"
Condition 2: products.length === 1
Action/Result: resultsText = "result"
Condition 3: products.length === 0
Action/Result: resultsText is computed but not rendered (the no-results message path is used instead)
Validation Details:
Input: products.length integer
Rule logic: Ternary: products.length > 1 ? "results" : "result"
Pass behavior: Grammatically correct label is rendered in the summary (e.g., "Showing 1 result for..." or "Showing 4 results for...")
Fail behavior: Not applicable — the ternary always produces one of two valid strings
Source Evidence: Search (/search) §4, §8
Business Rationale: Maintains grammatically correct UI copy, improving perceived quality of the user interface.
Related Rules: BR-022
Testing Implications:
Positive: 1 product returned → summary reads "Showing 1 result for..."
Positive: 4 products returned → summary reads "Showing 4 results for..."
Edge case: 0 products — confirm resultsText is not rendered (covered by BR-022 and BR-023)
3.5: Collection Browsing
Description: Rules governing the display of products within a specific Shopify collection on the Search Detail screen.
Screens involved: Search Detail (/search/[collection])
BR-026: Empty Collection Displays No-Products Message
Domain: Collection Browsing Category: Operative Enforcement: Server Criticality: Medium
Rule Statement: When a valid collection exists but contains no products, the application must display the message "No products found in this collection" rather than an empty grid.
Conditions:
Condition 1: getCollectionProducts() returns an empty array (products.length === 0)
Action/Result: <p>No products found in this collection</p> is rendered; no Grid or ProductGridItems is rendered
Validation Details:
Input: products array returned by getCollectionProducts({ collection, sortKey, reverse })
Rule logic: Conditional rendering based on products.length === 0
Pass behavior: User sees the no-products message
Fail behavior: Not applicable — this is the defined behavior for an empty collection
Source Evidence: Search Detail (/search/[collection]) §3, §8, §10
Business Rationale: Provides clear user feedback when a collection exists but has no products, preventing a confusing blank layout.
Testing Implications:
Positive: Collection with products → grid rendered
Negative: Collection with zero products → no-products message rendered, no grid in DOM
Edge case: Collection that had products but all were removed — confirm the empty state message appears correctly
3.6: Product Display
Description: Rules governing how product content is presented on the Product Detail screen, including image limits, related product behavior, and link prefetching.
Screens involved: Product Detail (/product/[handle])
BR-007: Product Image Gallery Capped at Five Images
Domain: Product Display Category: Structural Enforcement: Server Criticality: Low
Rule Statement: The product image gallery must receive no more than five images, regardless of how many images the product has in Shopify.
Conditions: Not applicable — this cap is applied unconditionally.
Validation Details:
Input: product.images array from the Shopify product object
Rule logic: product.images.slice(0, 5).map(({ url: src, altText }) => ({ src, altText })) — only the first five images are passed to the Gallery component
Pass behavior: Gallery receives an array of 0–5 image objects
Fail behavior: Images at index 5 and beyond are silently discarded; no error or warning is produced
Source Evidence: Product Detail (/product/[handle]) §4, §8
Business Rationale: Not stated in documentation — verify with business stakeholders. [Not documented — WHO: Product owner or front-end team; WHAT: What is the business or UX rationale for capping gallery images at five? Is this a performance constraint, a design decision, or a Shopify API limitation?; WHERE: Insert in the Business Rationale field of BR-007]
Related Rules: BR-008
Testing Implications:
Positive: Product with 3 images → Gallery receives 3 images
Positive: Product with 5 images → Gallery receives 5 images
Negative: Product with 8 images → Gallery receives only the first 5; images 6–8 are not displayed
Edge case: Product with 0 images → Gallery receives an empty array; confirm Gallery handles this gracefully
BR-008: Related Products Section Suppressed When Empty
Domain: Product Display Category: Operative Enforcement: Server Criticality: Low
Rule Statement: The Related Products section must not be rendered when getProductRecommendations returns an empty array.
Conditions:
Condition 1: relatedProducts.length === 0
Action/Result: RelatedProducts component returns null; the "Related Products" heading and product list are not rendered
Validation Details:
Input: relatedProducts array returned by getProductRecommendations(product.id)
Rule logic: Early return if (!relatedProducts.length) return null in the RelatedProducts component
Pass behavior: Related products section renders when recommendations exist
Fail behavior: Entire related products section (including the <h2> heading) is omitted from the DOM
Source Evidence: Product Detail (/product/[handle]) §5, §8, §10
Business Rationale: Prevents an empty "Related Products" section with a heading but no content from being displayed, maintaining a clean page layout.
Related Rules: BR-007
Testing Implications:
Positive: Product with recommendations → related products section rendered with heading and tiles
Negative: Product with no recommendations → no related products section in DOM (including no heading)
Edge case: Shopify returns recommendations for a product that has since been deleted — confirm GridTileImage handles missing product data
BR-028: Related Product Links Prefetched on Viewport Entry
Domain: Product Display Category: Operative Enforcement: Client Criticality: Low
Rule Statement: Each related product link must be prefetched by Next.js when it enters the viewport.
Conditions: Not applicable — prefetch={true} is set unconditionally on all related product <Link> components.
Validation Details:
Input: Related product handle strings from getProductRecommendations
Rule logic: <Link href={/product/${product.handle}} prefetch={true}> — Next.js prefetches the RSC payload for each linked product page when the link becomes visible
Pass behavior: RSC payload for linked product pages is loaded in the background when links enter the viewport
Fail behavior: Not applicable — prefetch failure is silent and does not affect navigation functionality
Source Evidence: Product Detail (/product/[handle]) §3, §6, §9
Business Rationale: Improves perceived navigation performance by preloading related product page data before the user clicks, reducing time-to-interactive for subsequent page loads.
Related Rules: BR-008
Testing Implications:
Positive: Related products visible in viewport → network tab shows prefetch requests for /product/[handle] RSC payloads
Edge case: Related products below the fold — confirm prefetch triggers on scroll into view, not on page load
DT-01: Search Results Display Logic
Related Rules: BR-022, BR-023, BR-024 Hit Policy: Unique
| Condition: searchValue present | Condition: products.length | → Summary Paragraph | → Product Grid |
|---|---|---|---|
| No | Any | Not rendered | Not rendered |
| Yes | 0 | "There are no products that match "<term>"" | Not rendered |
| Yes | > 0 | "Showing N result(s) for "<term>"" | Rendered |
| No | > 0 | Not rendered | Rendered |
DT-02: SEO Metadata Fallback — Item Detail Page
Related Rules: BR-011, BR-012 Hit Policy: First
| Condition: page.seo.title present | Condition: page.title present | → Metadata Title |
|---|---|---|
| Yes | Any | page.seo.title |
| No | Yes | page.title |
| No | No | Empty string |
| Condition: page.seo.description present | Condition: page.bodySummary present | → Metadata Description |
|---|---|---|
| Yes | Any | page.seo.description |
| No | Yes | page.bodySummary |
| No | No | Empty string |
DT-03: SEO Metadata Fallback — Product Detail Page
Related Rules: BR-015, BR-016, BR-017 Hit Policy: First
| Condition: product.seo.title present | Condition: product.title present | → Metadata Title |
|---|---|---|
| Yes | Any | product.seo.title |
| No | Yes | product.title |
| No | No | Empty string |
| Condition: product.seo.description present | Condition: product.description present | → Metadata Description |
|---|---|---|
| Yes | Any | product.seo.description |
| No | Yes | product.description |
| No | No | Empty string |
| Condition: product.featuredImage.url truthy | → OpenGraph Image |
|---|---|
| Yes | Included in metadata |
| No | Omitted / null |
DT-04: SEO Metadata Fallback — Search Detail Page
Related Rules: BR-018, BR-019 Hit Policy: First
| Condition: collection.seo.title present | Condition: collection.title present | → Metadata Title |
|---|---|---|
| Yes | Any | collection.seo.title |
| No | Yes | collection.title |
| No | No | Empty string |
| Condition: collection.seo.description present | Condition: collection.description present | → Metadata Description |
|---|---|---|
| Yes | Any | collection.seo.description |
| No | Yes | collection.description |
| No | No | "${collection.title} products" |
DT-05: Product Availability in Structured Data
Related Rules: BR-009 Hit Policy: Unique
| Condition: product.availableForSale | → JSON-LD offers.availability |
|---|---|
| true | "https://schema.org/InStock" |
| false | "https://schema.org/OutOfStock" |
| Rule ID | Field / Input | Validation Type | Constraint | Error Message / Behavior |
|---|---|---|---|---|
| BR-002 | params.page (URL segment) | Custom | Must resolve to an existing Shopify page via getPage | notFound() called → Next.js 404 page rendered |
| BR-003 | params.handle (URL segment) | Custom | Must resolve to an existing Shopify product via getProduct | notFound() called → Next.js 404 page rendered |
| BR-004 | params.collection (URL segment) | Custom | Must resolve to an existing Shopify collection via getCollection | notFound() called → Next.js 404 page rendered |
| BR-021 | searchParams.sort (query parameter) | Custom | Must match a slug in the sorting constants array | Silent fallback to defaultSort; no user-facing error |
| BR-005 | product.tags array | Custom | Checked for presence of HIDDEN_PRODUCT_TAG | noindex, nofollow directives set in metadata; no user-facing error |
| Rule ID | Output | Formula / Logic | Inputs | Precision / Rounding |
|---|---|---|---|---|
| BR-025 | resultsText label | products.length > 1 ? "results" : "result" | products.length (integer) | Not applicable |
| BR-027 | 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-formatted string output |
| BR-005 | indexable boolean | !product.tags.includes(HIDDEN_PRODUCT_TAG) | product.tags (string array), HIDDEN_PRODUCT_TAG (string constant) | Not applicable |
| BR-007 | Gallery images array | product.images.slice(0, 5).map(({ url: src, altText }) => ({ src, altText })) | product.images array | Not applicable — array truncation |
| BR-021 | { sortKey, reverse } pair | sorting.find((item) => item.slug === sort) || defaultSort | sort query param string, sorting constants array, defaultSort constant | Not applicable |
| Rule | Depends On | Type | Description |
|---|---|---|---|
| BR-002 | BR-001 | Prerequisite | BR-001 establishes that the route is public; BR-002 applies the only gate (data existence) |
| BR-003 | BR-001 | Prerequisite | BR-001 establishes that the route is public; BR-003 applies the only gate (data existence) |
| BR-004 | BR-001 | Prerequisite | BR-001 establishes that the route is public; BR-004 applies the only gate (data existence) |
| BR-005 | BR-003 | Prerequisite | BR-003 must pass (product must exist) before BR-005 can evaluate the tags array |
| BR-006 | BR-005 | Complement | BR-005 and BR-006 together define the complete behavior for hidden products: de-indexed but accessible |
| BR-007 | BR-003 | Prerequisite | Product must exist before image array can be sliced |
| BR-008 | BR-003 | Prerequisite | Product must exist before recommendations can be fetched |
| BR-009 | BR-003 | Prerequisite | Product must exist before availability can be evaluated |
| BR-010 | BR-003 | Prerequisite | Product must exist before price range can be expressed |
| BR-011 | BR-002 | Prerequisite | Page must exist before SEO metadata can be generated |
| BR-012 | BR-002 | Prerequisite | Page must exist before SEO metadata can be generated |
| BR-011 | BR-012 | Complement | Together define the complete SEO metadata fallback for Item Detail pages |
| BR-015 | BR-003 | Prerequisite | Product must exist before SEO metadata can be generated |
| BR-016 | BR-003 | Prerequisite | Product must exist before SEO metadata can be generated |
| BR-015 | BR-016 | Complement | Together define the complete SEO metadata fallback for Product Detail pages |
| BR-017 | BR-003 | Prerequisite | Product must exist before OpenGraph image can be evaluated |
| BR-018 | BR-004 | Prerequisite | Collection must exist before SEO metadata can be generated |
| BR-019 | BR-004 | Prerequisite | Collection must exist before SEO metadata can be generated |
| BR-018 | BR-019 | Complement | Together define the complete SEO metadata fallback for Search Detail pages |
| BR-021 | BR-022 | Prerequisite | Sort must be resolved before products are fetched; product fetch result drives summary display |
| BR-022 | BR-023 | Refinement | BR-023 is a specific case of BR-022 (summary shown with query, zero results) |
| BR-023 | BR-024 | Complement | BR-023 and BR-024 together define the complete zero-results display behavior |
| BR-026 | BR-004 | Prerequisite | Collection must exist (BR-004 passed) before empty-collection state can be evaluated |
| BR-026 | BR-021 | Prerequisite | Sort must be resolved before collection products are fetched |
| BR-028 | BR-008 | Prerequisite | Related products section must be rendered (BR-008 passed) before prefetch links exist |
| BR-013 | BR-020 | Complement | Together define the OpenGraph type classification across the two content-type screens |
| BR-014 | BR-013 | Prerequisite | og:article:published_time and og:article:modified_time are only meaningful when og:type is "article" |
GAP-01: Search Input Sanitization Not Documented at Page Level
Type: Single-Side Enforcement Affected Domain: Search & Filtering Risk Level: Medium Description: The searchValue (from ?q) and collection slug are passed directly to getProducts and getCollectionProducts respectively without any sanitization or validation at the page component level. The documentation states that sanitization is expected to occur within lib/shopify, but this is not confirmed in the provided source. If lib/shopify uses string interpolation rather than parameterized GraphQL variables, injection into the Shopify API query is possible. Recommendation: Confirm that lib/shopify uses parameterized GraphQL variables for all user-supplied inputs. Document the sanitization rule explicitly. If sanitization is absent, add input validation at the page level as a defense-in-depth measure. Related Rules: BR-021
GAP-02: No Maximum Image Count Enforcement Beyond Gallery Cap
Type: Implicit Rule Affected Domain: Product Display Risk Level: Low Description: BR-007 caps the gallery at five images, but there is no documented rule governing the minimum number of images required for a product to be displayed, or what the Gallery component renders when it receives zero images. The behavior for a product with no images is undocumented at the page level. Recommendation: Document the expected behavior when product.images is empty or contains zero items. Verify with the Gallery component implementation whether an empty images array produces a broken layout or a graceful placeholder. Related Rules: BR-007
GAP-03: Hidden Product Tag Value Not Documented
Type: Missing Rule Affected Domain: Content Visibility & Access Control Risk Level: Medium Description: The HIDDEN_PRODUCT_TAG constant is imported from lib/constants and used in BR-005, but its actual string value is not documented in the provided screen documentation. The documentation notes it is "typically 'nextjs-frontend-hidden' or similar." Without knowing the exact value, QA engineers cannot test the rule, and merchandising teams cannot correctly tag products in Shopify. Recommendation: Document the exact string value of HIDDEN_PRODUCT_TAG from lib/constants. [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 Validation Details of BR-005 and in the Glossary entry for HIDDEN_PRODUCT_TAG] Related Rules: BR-005, BR-006
GAP-04: Server-Locale Date Formatting May Mismatch User Locale
Type: Implicit Rule Affected Domain: Product Data Integrity Risk Level: Low Description: BR-027 uses undefined as the locale argument to Intl.DateTimeFormat, which resolves to the server's runtime locale rather than the user's browser locale. On a server deployed in a non-English locale (e.g., a Vercel edge node), the date may be formatted in an unexpected language for English-speaking users. The documentation acknowledges this as "a subtle but important distinction" but does not state whether it is intentional. Recommendation: Verify with the product owner whether the date should be formatted in the user's browser locale. If user-locale formatting is desired, the date formatting must be moved to a client component or the locale must be passed from the request context. [Not documented — WHO: Product owner; WHAT: Is server-locale date formatting intentional, or should the date be formatted in the user's browser locale?; WHERE: Insert in the Business Rationale field of BR-027] Related Rules: BR-027
GAP-05: Array-Valued Query Parameters Not Guarded
Type: Edge Case Affected Domain: Search & Filtering Risk Level: Medium Description: Both the Search and Search Detail pages cast searchParams as { [key: string]: string }, suppressing TypeScript's awareness that values could be string[] or undefined. If a URL contains repeated parameters (e.g., ?q=a&q=b or ?sort=price-asc&sort=title-asc), the runtime behavior is undefined — the cast does not protect against receiving an array where a string is expected. This could produce unexpected behavior in getProducts or getCollectionProducts. Recommendation: Add runtime validation to confirm that q and sort are scalar strings before use. Replace the unsafe type cast with explicit extraction logic (e.g., Array.isArray(q) ? q[0] : q). Related Rules: BR-021, BR-022
GAP-06: JSON-LD Script Tag XSS Not Fully Mitigated
Type: Missing Rule Affected Domain: Product Data Integrity Risk Level: High Description: The Product Detail page injects JSON-LD structured data via dangerouslySetInnerHTML={{ __html: JSON.stringify(productJsonLd) }}. While JSON.stringify escapes characters that break JSON string context, it does not escape </script> sequences, which could allow a Shopify-sourced product title or description containing </script><script> to break out of the script tag and inject arbitrary HTML. The documentation acknowledges this risk explicitly. Recommendation: Use a JSON serializer that escapes <, >, and & characters within string values (e.g., replacing < with \u003c, > with \u003e, & with \u0026) before injecting into the script tag. This is a known best practice for server-rendered JSON-LD. [Not documented — WHO: Development team; WHAT: Has a safe JSON serializer been implemented for JSON-LD injection, or is raw JSON.stringify still in use?; WHERE: Insert in the Validation Details of a new rule to be added for JSON-LD sanitization] Related Rules: BR-009, BR-010
GAP-07: Missing featuredImage Null Guard in JSON-LD
Type: Edge Case Affected Domain: Product Data Integrity Risk Level: High Description: The Product Detail page accesses product.featuredImage.url directly in the JSON-LD block without a null guard. The documentation notes: "if featuredImage is undefined, this would throw a runtime error." While BR-017 documents a null guard for the OpenGraph metadata block, no equivalent guard is documented for the JSON-LD block. A product without a featuredImage would cause a server-side runtime error, likely resulting in a 500 response. Recommendation: Add a null guard (product.featuredImage?.url) in the JSON-LD construction block, consistent with the OpenGraph metadata handling. This is a defect risk, not merely a gap. Related Rules: BR-017, BR-009
GAP-08: No Error Boundary on RelatedProducts or Gallery
Type: Missing Rule Affected Domain: Product Display Risk Level: Medium Description: Neither the RelatedProducts component nor the Gallery/ProductDescription Suspense wrappers include React Error Boundaries. An unhandled error in any of these components would propagate to the nearest Next.js error boundary (error.tsx), potentially taking down the entire product detail page rather than gracefully degrading the affected section. No rule governing partial failure behavior is documented. Recommendation: Define a business rule specifying the expected behavior when RelatedProducts or Gallery fails to render (e.g., "If the Related Products section fails to load, the main product card must still render"). Implement React Error Boundaries around these sections to enforce partial failure isolation. Related Rules: BR-008, BR-007
GAP-09: getPage Called Twice Per Request
Type: Implicit Rule Affected Domain: SEO & Metadata Risk Level: Low Description: On the Item Detail screen, getPage is called independently in both generateMetadata and the Page component, resulting in two API calls per request unless Next.js's fetch deduplication coalesces them. The documentation notes this behavior but does not confirm whether deduplication is active. If deduplication is not active (e.g., if getPage uses a non-fetch-based HTTP client), this doubles the Shopify API load per page request and could contribute to rate limiting. Recommendation: Confirm whether Next.js fetch deduplication is active for getPage calls. [Not documented — WHO: Development team; WHAT: Does lib/shopify's getPage use the native fetch API (which Next.js deduplicates) or a custom HTTP client? Is request deduplication confirmed to be active?; WHERE: Insert in the Integration Points section of the Item Detail screen documentation and note in GAP-09] Related Rules: BR-011, BR-012
GAP-10: Empty Page Title Not Guarded
Type: Edge Case Affected Domain: Content Visibility & Access Control Risk Level: Low Description: The Item Detail page renders page.title directly in an <h1> element without checking whether the title is empty. If a Shopify page has an empty title, an empty <h1> is rendered. No validation rule or fallback is documented for this case. Similarly, if page.title is empty and page.seo.title is also empty, the metadata title will be empty. Recommendation: Define a rule specifying the minimum content requirements for a renderable page (e.g., "A page must have a non-empty title to be rendered"). Add a guard or fallback for empty page.title values. Related Rules: BR-002, BR-011
9.1: Business Concepts
| Term | Definition in this context |
|---|---|
| Collection | A Shopify concept representing a named group of products (e.g., "Summer Sale", "Accessories"). Collections have a handle (URL-friendly slug), title, description, and SEO metadata. Used as the route parameter for the Search Detail screen. |
| Collection handle | The URL-safe identifier for a Shopify collection, used as the [collection] route parameter (e.g., "mens-shirts"). Must match an existing Shopify collection or the page returns 404. |
| Page handle | A URL-safe string identifier for a Shopify CMS page (e.g., about, privacy-policy). Used as both the URL segment and the Shopify API lookup key on the Item Detail screen. |
| Product handle | A URL-safe, human-readable string identifier for a Shopify product (e.g., classic-white-tee). Unique per product, set in Shopify admin. Used as the route parameter on the Product Detail screen. |
| availableForSale | A Shopify boolean field on a product indicating whether any variant of the product is currently purchasable. Used to determine the Schema.org availability value in JSON-LD structured data. |
| featuredImage | The primary/hero image of a Shopify product, used for OpenGraph metadata and JSON-LD. Distinct from the full images array. |
| HIDDEN_PRODUCT_TAG | A string constant (from lib/constants) used as a Shopify product tag to mark products that should not be indexed by search engines. Products with this tag receive noindex, nofollow metadata directives but remain accessible via direct URL. |
| priceRange | A Shopify object containing minVariantPrice and maxVariantPrice, each with amount (string) and currencyCode (ISO 4217 string). Represents the price spread across all variants of a product. |
| productRecommendations | Shopify's algorithmic related-product suggestions, returned by the productRecommendations Storefront API query given a product ID. Used to populate the Related Products section on the Product Detail screen. |
| page.body | The full HTML content of a Shopify CMS page, as returned by the Storefront API. Contains merchant-authored rich text rendered as HTML by the Prose component. |
| page.bodySummary | A plain-text excerpt or summary of the Shopify page body, used as a fallback for SEO meta description when no explicit SEO description is set. |
| page.seo | An optional Shopify object containing merchant-specified SEO overrides: title and description. Takes precedence over the page's default title and body summary in metadata generation. |
| SEO metadata | Shopify resources (pages, products, collections) can have dedicated SEO title and description fields separate from their display title/description. These are preferred for <head> metadata when present. |
9.2: Rule Terminology
| Term | Definition in this context |
|---|---|
| Structural rule | A BABOK rule category describing a constraint on data or state — what must always be true about an entity or relationship. Expressed as an invariant. |
| Operative rule | A BABOK rule category describing a constraint on process or action — what must happen or must not happen under specified conditions. Expressed as a behavioral requirement. |
| Decision rule | A BABOK rule category describing a condition-based outcome — if a set of conditions is true, then a specific result must follow. Expressed as a conditional. |
| Hit Policy | A DMN v1.5 concept defining how a decision table resolves when multiple rows match: Unique (only one row can match), First (first matching row wins), Any (all matching rows give the same result), Collect (all matching rows are applied). |
| Enforcement point | Where a rule is evaluated and enforced: Client (browser/UI), Server (API/backend), or Both. |
| Criticality | The severity of impact if a rule is violated: Critical (data corruption or security breach), High (broken workflow), Medium (degraded user experience), Low (cosmetic or minor). |
9.3: Domain Terms
| Term | Definition in this context |
|---|---|
| AggregateOffer | A Schema.org type used in JSON-LD to represent a product with multiple price points (variants), specifying both highPrice and lowPrice. Used in the Product Detail JSON-LD block. |
| defaultSort | A constant from lib/constants defining the fallback sort configuration used when no valid sort query parameter is present. Defines the default product ordering for Search and Search Detail screens. |
| generateMetadata | A Next.js App Router special export from a page file that runs server-side to produce <head> metadata (title, description, robots, OpenGraph tags) for a route. |
| getCollection | A function in lib/shopify that queries the Shopify Storefront GraphQL API for a single Collection resource by its handle. Called during metadata generation on the Search Detail screen. |
| getCollectionProducts | A function in lib/shopify that queries the Shopify Storefront GraphQL API for all products within a specified collection, accepting collection, sortKey, and reverse parameters. |
| getPage | A function in lib/shopify that queries the Shopify Storefront GraphQL API for a single Page resource by its handle. Called on the Item Detail screen. |
| getProduct | A function in lib/shopify that queries the Shopify Storefront GraphQL API for a single Product resource by its handle. Called on the Product Detail screen. |
| getProductRecommendations | A function in lib/shopify that queries the Shopify Storefront API for algorithmically generated product recommendations given a product ID. Called by the RelatedProducts component. |
| getProducts | A function in lib/shopify that queries the Shopify Storefront GraphQL API for a list of products, accepting sortKey, reverse, and query parameters. Called on the Search screen. |
| JSON-LD | JSON Linked Data — a structured data format embedded in a <script type="application/ld+json"> tag, used by search engines to understand page content for rich results (e.g., product price, availability, images in Google Shopping). |
| notFound() | A Next.js App Router utility function that, when called, halts rendering and triggers the nearest not-found.tsx boundary or the default 404 response. Used as the data-existence gate on Item Detail, Product Detail, and Search Detail screens. |
| Open Graph (og:) | A metadata protocol used by social platforms (Facebook, LinkedIn, Slack, etc.) to generate rich link previews. Set via openGraph in Next.js Metadata. |
| Prose | A custom component that applies typographic styling to raw HTML content. Renders page.body from Shopify using dangerouslySetInnerHTML on the Item Detail screen. |
| searchValue | The value of the q URL query parameter on the Search screen; the keyword string the user searched for. Drives both the product query and the results summary display. |
| Shopify Storefront API | The GraphQL API provided by Shopify that allows headless storefronts to query product, collection, cart, and checkout data. The underlying data source for all product and content data in this application. |
| sorting | An array of sort option objects defined in lib/constants, each containing a slug (URL-friendly identifier), sortKey (Shopify API enum), and reverse (boolean). Maps user-facing sort options to Shopify API parameters. |
| sortKey | A Shopify Storefront API enum value (e.g., PRICE, TITLE, BEST_SELLING) that determines the field by which products are sorted in a GraphQL query. |
| reverse | A boolean passed to the Shopify Storefront API indicating whether the sort order should be reversed (descending). Combined with sortKey to express full sort intent. |
| React Server Component (RSC) | A React component that renders exclusively on the server, sending HTML to the client with no client-side JavaScript bundle for the component itself. All five documented page components are RSCs. |
| Suspense | React's built-in component for declarative loading states, enabling streaming SSR in Next.js App Router. Used on the Product Detail screen to wrap Gallery and ProductDescription. |
Business Rules Assessment — Commerce Application — April 2026 Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.