Skip to content

Conversation

@juanpprieto
Copy link
Contributor

@juanpprieto juanpprieto commented Oct 3, 2025

Fixes: https://github.com/Shopify/hydrogen-internal/issues/254

Summary

Adds /.well-known/shopify/monorail/unstable/produce_batch as a virtual resource route that proxies Shopify analytics requests to the configured Shopify store domain, eliminating runtime errors in Multipass/Hybrid setups where users navigate between Hydrogen and Shopify theme subdomains.

Why

Problem: In Multipass/Hybrid setups, Shopify theme analytics scripts persist after subdomain navigation and POST to the Hydrogen domain, causing runtime errors:

POST /.well-known/shopify/monorail/unstable/produce_batch → 404 Not Found
Error: "did not provide an action for route routes/($locale).$"

Affected setups: Any Hydrogen + Multipass configuration with subdomain redirects to Shopify themes (e.g., xyz.com)

Impact:

  • Runtime error noise in logs
  • Potential analytics data loss
  • User confusion from errors

What

Implementation

Virtual Resource Route:

Request Flow:
┌──────────────────┐
│  Theme Scripts   │ (trekkie.js, consent-tracking-api.js)
│  POST analytics  │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Hydrogen Domain  │ 
│ /.well-known/... │ (xyz.com)
└────────┬─────────┘
         │
         ▼ (proxy)
┌──────────────────┐
│ Shopify Theme    │ (xyz.myshopify.com)
│ /.well-known/... │
└────────┬─────────┘
         │
         ▼
    200 OK / 400
Implementation Details

Resource Route (no default export):

  • Returns raw responses without React Router layout wrapping
  • Uses response.body stream to preserve Shopify's exact response
  • Forwards all headers (CORS, x-request-id, x-robots-tag, etc.)
export async function action({request, context}: ActionFunctionArgs) {
  const shopifyStoreDomain = context?.env?.SHOPIFY_STORE_DOMAIN;

  if (!shopifyStoreDomain) {
    return new Response('', {status: 204});
  }

  const response = await fetch(
    `https://${shopifyStoreDomain}${url.pathname}`,
    {method: 'POST', headers: {...}, body}
  );

  return new Response(response.body, {
    status: response.status,
    headers: respHeaders,
  });
}

Registration:

// packages/hydrogen/src/vite/get-virtual-routes.ts
{
  id: 'vite/virtual-routes/routes/[.]well-known.shopify.monorail.unstable.produce_batch',
  path: '.well-known/shopify/monorail/unstable/produce_batch',
  file: '[.]well-known.shopify.monorail.unstable.produce_batch.jsx',
  index: false,
}

Key Features

Feature Implementation Benefit
Resource Route No default export Returns raw responses (no HTML layout)
Conditional Proxy Checks SHOPIFY_STORE_DOMAIN env var Works out-of-box when domain set, silent fallback when not
Header Forwarding Copies all Shopify response headers Preserves CORS, validation messages, Shopify metadata
Stream Response Uses response.body not await response.text() Avoids serialization, exact Shopify response
Error Handling Try-catch with 204 fallback Graceful degradation if proxy fails

robots.txt SEO Fix

Removed: Disallow: /.well-known/shopify/monorail

Rationale:

  • Shopify's endpoint returns x-robots-tag: noindex header
  • Google best practice: Don't use both robots.txt Disallow AND noindex headers
  • With robots.txt blocking, crawlers never see the noindex header
  • Proper approach: Let x-robots-tag: noindex header prevent indexing

Reference: Google - Block Search Indexing with noindex

"For the noindex rule to be effective, the page or resource must not be blocked by a robots.txt file."

🎩 Top Hat

Prerequisites

  • Hydrogen project (2025.7.0+)
  • Multipass setup with theme subdomain (optional - route works without)
  • SHOPIFY_STORE_DOMAIN environment variable (optional)

Testing Steps

Test 1: Verify Route Exists and Returns Raw Response

# Start dev server
cd templates/skeleton
npm run dev

# In another terminal - test POST
curl -X POST http://localhost:3000/.well-known/shopify/monorail/unstable/produce_batch \
  -H "Content-Type: text/plain" \
  -d '{"events":[]}' \
  -i

# Expected: HTTP 200 or 400, NO HTML in response body
# With SHOPIFY_STORE_DOMAIN set: Proxies to Shopify, returns their response
# Without SHOPIFY_STORE_DOMAIN: Returns 204 No Content

# Test GET (should reject)
curl http://localhost:3000/.well-known/shopify/monorail/unstable/produce_batch

# Expected: 405 Method Not Allowed

Test 2: Verify Proxy Functionality with Valid Payload

# Set environment variable
export SHOPIFY_STORE_DOMAIN=hydrogen-preview.myshopify.com

# Post valid analytics payload
curl -X POST http://localhost:3000/.well-known/shopify/monorail/unstable/produce_batch \
  -H "Content-Type: text/plain" \
  -d '{
  "events": [{
    "schema_id": "trekkie_storefront_page_view/1.4",
    "payload": {
      "shopId": 1,
      "currency": "USD",
      "uniqToken": "test-token",
      "visitToken": "test-visit",
      "microSessionId": "test-session",
      "microSessionCount": 1,
      "url": "https://shop.com",
      "path": "/",
      "search": "",
      "referrer": "",
      "title": "Home",
      "appClientId": "12875497473",
      "isMerchantRequest": false,
      "hydrogenSubchannelId": "0",
      "isPersistentCookie": true,
      "contentLanguage": "en"
    },
    "metadata": {"event_created_at_ms": 1759436400000}
  }],
  "metadata": {"event_sent_at_ms": 1759436400000}
}' -i

# Expected: HTTP 200 OK with Shopify analytics response
# Should see CORS headers from Shopify

Test 3: Verify robots.txt Fix

curl http://localhost:3000/robots.txt | grep "monorail"

# Expected: NO output (Disallow line removed)
# Allows crawlers to see x-robots-tag: noindex header

Test 4: Run Unit Tests

cd packages/hydrogen
npm test -- "monorail"

# Expected: 7/7 tests passing
# - Proxies POST to Shopify domain
# - Returns 405 for GET
# - Returns 204 fallback when no domain
# - Handles proxy errors
# - Forwards headers correctly
# - Handles large payloads
# - Proxies valid Hydrogen analytics payloads

Edge Cases to Test

  • Route works without SHOPIFY_STORE_DOMAIN (returns 204)
  • Route works with SHOPIFY_STORE_DOMAIN set (proxies correctly)
  • Invalid analytics payloads return 400 from Shopify
  • Valid analytics payloads return 200 from Shopify
  • Network errors to Shopify gracefully fallback to 204
  • GET requests properly rejected with 405
  • Large analytics payloads handled without issues
  • No HTML layout wrapping in responses
  • All Shopify headers preserved (CORS, x-*, etc.)

Validation Checklist

  • All tests pass: npm test (445/445)
  • No TypeScript errors: npm run typecheck
  • Monorail route returns raw responses (no HTML)
  • Proxy forwards to correct Shopify domain
  • robots.txt no longer blocks monorail endpoint
  • Works in both dev and production (tested on skeleton.hydrogen.shop)
  • No breaking changes to existing analytics (monorail-edge.shopifysvc.com still works)

@juanpprieto juanpprieto requested a review from a team as a code owner October 3, 2025 15:41
@juanpprieto juanpprieto marked this pull request as draft October 3, 2025 15:41
@shopify
Copy link
Contributor

shopify bot commented Oct 3, 2025

Oxygen deployed a preview of your wellknown-monorail branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment October 3, 2025 4:23 PM
sitemap ✅ Successful (Logs) Preview deployment Inspect deployment October 3, 2025 4:23 PM
custom-cart-method ✅ Successful (Logs) Preview deployment Inspect deployment October 3, 2025 4:23 PM
metaobjects ✅ Successful (Logs) Preview deployment Inspect deployment October 3, 2025 4:23 PM
third-party-queries-caching ✅ Successful (Logs) Preview deployment Inspect deployment October 3, 2025 4:23 PM

Learn more about Hydrogen's GitHub integration.

Adds /.well-known/shopify/monorail/unstable/produce_batch as a virtual
resource route to resolve runtime errors in Multipass setups where users
navigate between Hydrogen and Shopify theme subdomains.

Root Cause:
- Shopify theme analytics scripts (trekkie.js, consent-tracking-api.js)
  persist after subdomain navigation
- Scripts POST analytics to current domain (Hydrogen)
- Hydrogen lacked the route Shopify themes provide
- Result: 404 errors and "did not provide an action" React Router errors

Solution:
- Virtual resource route proxies to SHOPIFY_STORE_DOMAIN when configured
- Returns raw responses (no layout wrapping) via React Router resource route pattern
- Forwards all Shopify headers and status codes (200, 400, etc.)
- Falls back to 204 No Content when domain not set
- Preserves analytics data from theme subdomain visits

Implementation:
- Resource route (no default export) for raw responses
- Uses response.body stream to avoid HTML serialization
- 7 comprehensive unit tests (proxy, headers, fallback, errors)
- All existing tests pass (445/445)

SEO Fix:
- Removed Disallow: /.well-known/shopify/monorail from robots.txt
- Rationale: Shopify returns x-robots-tag: noindex header
- Google best practice: Don't use both Disallow AND noindex
- robots.txt blocking prevents crawlers from seeing noindex header
- Let x-robots-tag header (forwarded from Shopify) prevent indexing
- Reference: https://developers.google.com/search/docs/crawling-indexing/block-indexing

Testing:
- Unit: 7/7 passing
- Integration: Validated on localhost and production (skylar.com investigation)
- Confirmed: Proxies to hydrogen-preview.myshopify.com successfully

Fixes runtime errors reported in:
https://community.shopify.dev/t/hydrogen-errors/22604

Changes:
- packages/hydrogen/src/vite/get-virtual-routes.ts
- packages/hydrogen/src/vite/virtual-routes/routes/[.]well-known.shopify.monorail.unstable.produce_batch.tsx
- packages/hydrogen/src/vite/virtual-routes/routes/[.]well-known.shopify.monorail.unstable.produce_batch.test.tsx
- templates/skeleton/app/routes/[robots.txt].tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant