<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gabriel Richards</title>
    <description>The latest articles on DEV Community by Gabriel Richards (@endertech).</description>
    <link>https://dev.to/endertech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4213981%2F38e5b15c-3901-4418-a080-16af04a600a8.png</url>
      <title>DEV Community: Gabriel Richards</title>
      <link>https://dev.to/endertech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/endertech"/>
    <language>en</language>
    <item>
      <title>Is Your Hydrogen Product Page Readable by an AI Agent?</title>
      <dc:creator>Gabriel Richards</dc:creator>
      <pubDate>Thu, 29 Oct 2026 17:11:25 +0000</pubDate>
      <link>https://dev.to/endertech/is-your-hydrogen-product-page-readable-by-an-ai-agent-4a6j</link>
      <guid>https://dev.to/endertech/is-your-hydrogen-product-page-readable-by-an-ai-agent-4a6j</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://endertech.com/blog/hydrogen-product-page-readable-by-ai-agents" rel="noopener noreferrer"&gt;Endertech blog&lt;/a&gt; by Gabriel Richards, Founder &amp;amp; CEO. Republished here with a canonical link to the original.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI agents and crawlers read the first server response, not the finished page. Here are the three ways product pages hide content from them, how to check your own, and what we found on a production Hydrogen store.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Judge a product page by its first server response. Many agents don't run JavaScript or wait for streamed content.&lt;/li&gt;
&lt;li&gt;Pages hide content three ways: browser-fetched blocks, streamed placeholders and bot-detection gates.&lt;/li&gt;
&lt;li&gt;On the Hydrogen store we checked, price, specs and Product JSON-LD were in the initial HTML, but review text and delivery terms weren't for every reader.&lt;/li&gt;
&lt;li&gt;Render anything that affects a purchase on the server, for every user agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A product page can look complete in a browser and still be half empty to the software reading it. AI shopping agents, search crawlers and link previews often read the first HTML response, and many don't run JavaScript or wait for content that arrives later. The general lesson: judge a product page by what's in the first server response, not by what a person sees after everything has loaded.&lt;/p&gt;

&lt;p&gt;Modern storefronts make this easy to get wrong because they're good at loading things later. Headless stacks like Shopify Hydrogen stream the page, defer slow data behind placeholders and fetch some blocks in the browser. All of that helps shoppers. Some of it hides content from machines. This post covers the three ways that happens, how to check your own pages in an afternoon, and what we found when we ran the check on a production Hydrogen store in October 2026.&lt;/p&gt;

&lt;p&gt;Crawl signals like robots.txt, sitemaps and canonical tags are covered in &lt;a href="https://endertech.com/blog/headless-storefront-seo-own-the-crawl-signals-your-theme-used-to-send" rel="noopener noreferrer"&gt;Headless Storefront SEO: Own the Crawl Signals Your Theme Used to Send&lt;/a&gt;. How structured data fits in is covered in &lt;a href="https://endertech.com/blog/what-structured-data-does-for-seo-and-ai-search" rel="noopener noreferrer"&gt;What Structured Data Actually Does for SEO and AI Search&lt;/a&gt;. This post is about the product page itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the page still matters when Shopify sends a feed
&lt;/h2&gt;

&lt;p&gt;As of October 2026, Shopify sends eligible product data to AI channels through Shopify Catalog, so you might assume the page doesn't matter. Two things say otherwise.&lt;/p&gt;

&lt;p&gt;First, Shopify's own agentic storefronts documentation says AI channels can also reach products through the open web, by crawling and indexing. Catalog isn't the only reader.&lt;/p&gt;

&lt;p&gt;Second, the shoppers those agents send land on product pages. Shopify's GEO Playbook reports that in Q1 2026, more than half of AI-referred sessions started on a product page, compared with about 20% for organic search. The page is the first thing an agent quotes and the first thing a referred shopper sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways a product page hides content from machines
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content fetched in the browser.&lt;/strong&gt; Reviews, delivery estimates and recommendation widgets often load from a third-party service after the page renders. A reader that doesn't run JavaScript never sees them. In a searchVIU test from October 2025, ChatGPT and Claude didn't pick up a price inserted by JavaScript when fetching the page live, while ChatGPT did read prices in the visible HTML.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content streamed behind a placeholder.&lt;/strong&gt; Streaming frameworks send a "Loading..." fallback first and the real content later in the same response. A browser swaps it in. A simple parser may stop at the placeholder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content gated by bot detection.&lt;/strong&gt; Some storefronts wait for all deferred data only when the user agent looks like a known bot. Known crawlers get the full page. A new AI agent the detection library doesn't recognize gets the placeholder version.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My opinion: anything that affects a buying decision shouldn't depend on bot detection or JavaScript. Price, availability, specifications, delivery promises, warranty terms and some review text belong in the initial render for everyone. Only truly slow, nonessential blocks should be deferred.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own product pages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the page without JavaScript. Request a product URL with a plain HTTP client, once with a browser user agent, once with a known crawler user agent and once with a made-up agent string, and save all three responses.&lt;/li&gt;
&lt;li&gt;Search the raw HTML for the facts that sell the product. Price, availability, dimensions, materials, key specifications, review rating and count, review text, delivery and warranty terms.&lt;/li&gt;
&lt;li&gt;Trace anything missing. For each missing fact, find whether it's deferred on the server, fetched in the browser or never rendered.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we found on one Hydrogen store
&lt;/h2&gt;

&lt;p&gt;We ran that check on a Hydrogen storefront we build and maintain for a furniture retailer. The pattern is common, so it's a useful example.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Present for everyone:&lt;/strong&gt; title, price, description and specifications rendered as plain HTML from metafields, plus server-generated Product JSON-LD with one offer per variant and a rating only when real reviews exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependent on who was asking:&lt;/strong&gt; delivery, warranty and brand blocks were streamed behind a placeholder, and the server waited for them only for recognized bots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing for everyone:&lt;/strong&gt; review text, which loaded in the browser from a reviews service. The rating and count were in the HTML. The reviews themselves weren't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixes follow directly: render purchase-critical blocks for every user agent, and render at least a few recent reviews on the server if your reviews provider offers a server-side API. Shopify's Agentic admin lists reviews among its listing quality signals, and reviews are often the most persuasive content on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for any product page
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Compare raw HTML for a browser, a known crawler and an unknown user agent.&lt;/li&gt;
&lt;li&gt;List every deferred or browser-fetched block and decide whether it affects a purchase. If it does, render it on the server.&lt;/li&gt;
&lt;li&gt;Keep specifications in the HTML, even when they're inside a collapsed section.&lt;/li&gt;
&lt;li&gt;Render some review text on the server, not only the rating.&lt;/li&gt;
&lt;li&gt;Generate Product JSON-LD from the same data the page renders, with one offer per variant and ratings only when they exist.&lt;/li&gt;
&lt;li&gt;Check headings. One H1, sensible levels and no hidden headings that also hide from assistive technology.&lt;/li&gt;
&lt;li&gt;Test after every release. Deferred loading is easy to add in a refactor and hard to notice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're planning a new build or a rebuild, our &lt;a href="https://endertech.com/solutions/shopify-hydrogen" rel="noopener noreferrer"&gt;Shopify Hydrogen&lt;/a&gt; work starts from these checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we know
&lt;/h2&gt;

&lt;p&gt;We reviewed the product route of a production Hydrogen storefront, including its server entry, product loader and structured data module, on October 22, 2026. Shopify claims come from Shopify's Help Center and GEO Playbook, and the JavaScript fetch behavior from searchVIU's published test, all accessed the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;This is one storefront's route, and your stack may defer different blocks or detect bots differently. AI agents don't publish whether they run JavaScript or how long they wait, and the searchVIU result comes from a single test page, so we treat the first server response as the safe baseline rather than predicting each agent's behavior.&lt;/p&gt;

&lt;p&gt;Last reviewed: October 22, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.shopify.com/" rel="noopener noreferrer"&gt;The GEO Playbook: How (and Why) to Optimize for AI Discovery&lt;/a&gt;, Shopify (accessed October 22, 2026).&lt;/li&gt;
&lt;li&gt;Shopify Catalog and product discovery for agentic storefronts, Shopify Help Center (accessed October 22, 2026).&lt;/li&gt;
&lt;li&gt;Data fetching in Hydrogen, Shopify developer documentation (accessed October 22, 2026).&lt;/li&gt;
&lt;li&gt;Schema Markup and AI in 2025: What ChatGPT, Claude, Perplexity and Gemini Really See, searchVIU (accessed October 22, 2026).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make your product pages readable to every reader
&lt;/h2&gt;

&lt;p&gt;This is part of our series on Shopify's agentic storefronts. If you want a readability audit of your product pages, &lt;a href="https://endertech.com/contact" rel="noopener noreferrer"&gt;talk with Endertech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>hydrogen</category>
      <category>seo</category>
      <category>ai</category>
    </item>
    <item>
      <title>Isolate Shared State Before Adding Parallel Test Workers</title>
      <dc:creator>Gabriel Richards</dc:creator>
      <pubDate>Wed, 28 Oct 2026 23:37:59 +0000</pubDate>
      <link>https://dev.to/endertech/isolate-shared-state-before-adding-parallel-test-workers-pa6</link>
      <guid>https://dev.to/endertech/isolate-shared-state-before-adding-parallel-test-workers-pa6</guid>
      <description>&lt;p&gt;Parallel test runs break on shared state, not CPU count. Here is how we isolated databases, temp files, locks, and reports in a Symfony coverage run, locked only the truly shared fixtures, and merged coverage without hiding failures.&lt;/p&gt;

&lt;p&gt;TLDR&lt;br&gt;
Parallel test failures usually come from shared state, not too few CPUs.&lt;br&gt;
Give each process its own database suffix, and each queued test its own TMPDIR, lock store, JUnit file, and coverage file.&lt;br&gt;
Lock only the fixtures that must stay shared, and keep those locks outside the per-test TMPDIR.&lt;br&gt;
Warm one-time installs first, build the queue from phpunit --list-tests, and merge coverage with phpcov after the run.&lt;br&gt;
Let the test exit code win, and treat any test that passes only on rerun as a bug report.&lt;br&gt;
When a mature Symfony test suite gets slow, adding workers looks like the obvious fix. It usually isn't the first fix. Parallel test runs break on shared state, not on CPU count: two processes that both think they own the same database, temp file, lock, or report will produce failures that have nothing to do with the code under test.&lt;/p&gt;

&lt;p&gt;The rule we follow is simple. Give every worker, and ideally every queued test, its own database, temporary directory, lock store, report file, and coverage file. Serialize the few resources that truly must stay shared with a narrow, explicit lock. Warm one-time installs before you fan out. Then merge the outputs and keep the test exit code in charge.&lt;/p&gt;

&lt;p&gt;In October 2026 we applied this to the coverage run of a retail ecommerce integration middleware: a Symfony and Doctrine application whose suite exercises CSV imports, queues, and order and inventory flows. The client isn't named here, and the code below is trimmed from the real script and config. The lesson carries over to most PHP suites.&lt;/p&gt;

&lt;p&gt;Start with a shared-state inventory&lt;br&gt;
Before touching the test command, list every resource the suite creates, reads, overwrites, or reports into. For each one, decide whether it can be unique per process or has to stay shared.&lt;/p&gt;

&lt;p&gt;Resource    What we did Why it matters&lt;br&gt;
Doctrine test database  Appended the runner's process (channel) number to the test database name    Fixtures and test data can't cross process boundaries&lt;br&gt;
Temporary files Set TMPDIR per queued test (coverage/tmp/{n})   Generated and cached files don't collide&lt;br&gt;
Symfony Lock store  Pointed LOCK_DSN at a flock directory per queued test   The lock backend stops being accidental shared infrastructure&lt;br&gt;
JUnit reports   One XML file per queued test    No process overwrites another's results&lt;br&gt;
Coverage data   One .cov file per queued test, merged at the end    Each process records coverage independently&lt;br&gt;
CSV import fixtures in a shared directory   An exclusive cross-process flock per fixture file   Tests that must modify and restore the same file take turns&lt;br&gt;
An inventory like this is worth doing even if you never parallelize. Flaky tests usually point at unclear ownership of files and data. Finding them early is a normal part of our custom software development work.&lt;/p&gt;

&lt;p&gt;Give every process its own environment&lt;br&gt;
The runner here is liuggio/fastest, which reads a queue of tests from standard input and runs them across N processes. Its command template has two placeholders: {p} is the process (channel) number and {n} is a unique number for each queued test.&lt;/p&gt;

&lt;p&gt;For the database, the Doctrine test config appends both the ParaTest convention and the fastest channel to the database name:&lt;/p&gt;

&lt;p&gt;when@test:&lt;br&gt;
    doctrine:&lt;br&gt;
        dbal:&lt;br&gt;
            # "TEST_TOKEN" is typically set by ParaTest&lt;br&gt;
            # "ENV_TEST_CHANNEL" is typically set by liuggio/fastest ({p})&lt;br&gt;
            dbname_suffix: '_test%env(default::TEST_TOKEN)%%env(default::ENV_TEST_CHANNEL)%'&lt;br&gt;
For files, locks, and reports, the script keys everything on {n}, so each queued test gets its own directory and output files:&lt;/p&gt;

&lt;p&gt;FASTEST_ISOLATION_ENV="TMPDIR=\"$COVERAGE_BASE_DIR/coverage/tmp/{n}\" LOCK_DSN=\"flock://$COVERAGE_BASE_DIR/coverage/tmp/{n}/locks\"";&lt;br&gt;
FASTEST_COMMAND="$COVERAGE_COMMAND_PREFIX ./bin/phpunit -c \"$PHPUNIT_CONFIG\" {} --colors=never --coverage-php $COVERAGE_BASE_DIR/coverage/php/{n}.cov --log-junit $COVERAGE_BASE_DIR/coverage/junit/{n}.xml";&lt;br&gt;
FASTEST_EXECUTE="mkdir -p \"$COVERAGE_BASE_DIR/coverage/tmp/{n}/locks\" &amp;amp;&amp;amp; $FASTEST_ISOLATION_ENV $FASTEST_COMMAND";&lt;br&gt;
Lock only what has to stay shared&lt;br&gt;
Some state can't be split without changing what a test verifies. Import tests write CSV fixtures into the application's var/import directory. Two processes writing the same fixture file at once would corrupt each other's input.&lt;/p&gt;

&lt;p&gt;The fix was a narrow lock per fixture file, kept outside TMPDIR so every process can see it:&lt;/p&gt;

&lt;p&gt;// Shared across workers (do not use TMPDIR — coverage runner sets a per-method TMPDIR).&lt;br&gt;
$lockDir = '/tmp/phpunit-import-fixture-locks';&lt;br&gt;
if (!is_dir($lockDir)) {&lt;br&gt;
    mkdir($lockDir, 0777, true);&lt;br&gt;
}&lt;br&gt;
$lockPath = $lockDir.'/'.preg_replace('/[^A-Za-z0-9.&lt;em&gt;-]/', '&lt;/em&gt;', $filename).'.lock';&lt;br&gt;
$handle = fopen($lockPath, 'c+');&lt;br&gt;
if (!flock($handle, LOCK_EX)) {&lt;br&gt;
    fclose($handle);&lt;br&gt;
    throw new \RuntimeException(sprintf('Unable to acquire import fixture lock "%s"', $lockPath));&lt;br&gt;
}&lt;br&gt;
$this-&amp;gt;importCommandFixtureLocks[$filename] = $handle;&lt;br&gt;
Isolate by default, then lock the smallest shared thing that can't be isolated, and name the lock after it.&lt;/p&gt;

&lt;p&gt;Warm one-time setup before you fan out&lt;/p&gt;

&lt;h1&gt;
  
  
  Warm the Symfony PHPUnit bridge install once so parallel workers do not race on first boot.
&lt;/h1&gt;

&lt;p&gt;$PHP_BINARY ./bin/phpunit -c "$PHPUNIT_CONFIG" --version &amp;gt;/dev/null;&lt;br&gt;
Let PHPUnit build the queue&lt;br&gt;
Build the queue from bin/phpunit --list-tests, not find tests/ -name "*Test.php". That skips abstract base classes. Process count comes from PARALLEL_PROCESSES, then nproc / sysctl, then a fallback of 4.&lt;/p&gt;

&lt;p&gt;Treat reruns as bug reports&lt;br&gt;
--rerun-failed can keep one unlucky collision from failing a pipeline — and hide a race inside a green build. A test that passes only on rerun is a bug report, not a pass.&lt;/p&gt;

&lt;p&gt;Merge coverage without letting it mask failures&lt;/p&gt;

&lt;p&gt;./vendor/bin/fastest -vvv --ansi --process "$PROCESS_COUNT" --preserve-order --rerun-failed --no-errors-summary "$FASTEST_EXECUTE" &amp;lt; "$QUEUE_FILE";&lt;br&gt;
FASTEST_EXIT=$?;&lt;br&gt;
$PHP_BINARY "$PHPCOV_PHAR" merge \&lt;br&gt;
  --html "$COVERAGE_BASE_DIR/coverage/html" \&lt;br&gt;
  --cobertura "$COVERAGE_BASE_DIR/coverage/cobertura.xml" \&lt;br&gt;
  --text "$COVERAGE_BASE_DIR/coverage/coverage.txt" \&lt;br&gt;
  "$COVERAGE_BASE_DIR/coverage/php";&lt;br&gt;
PHPCOV_EXIT=$?;&lt;br&gt;
if [ "$FASTEST_EXIT" -ne 0 ]; then&lt;br&gt;
  exit "$FASTEST_EXIT";&lt;br&gt;
fi&lt;br&gt;
exit "$PHPCOV_EXIT";&lt;br&gt;
Coverage still merges after a failed run, but the test result wins.&lt;/p&gt;

&lt;p&gt;A practical sequence&lt;br&gt;
Inventory every database, directory, lock backend, fixture, report, and coverage file the suite touches.&lt;br&gt;
Make each one per process or per test.&lt;br&gt;
Put narrow cross-process locks on what must stay shared.&lt;br&gt;
Run one-time installs before starting workers.&lt;br&gt;
Build the queue from phpunit --list-tests.&lt;br&gt;
Merge coverage after the run, and exit with the test runner's status first.&lt;br&gt;
Log every rerun and fix the test behind it.&lt;br&gt;
If CI is slow and parallel runs feel risky, start with the shared-state inventory. For more on how we approach this kind of engineering work, see our &lt;a href="https://endertech.com/services/software-development" rel="noopener noreferrer"&gt;custom software practice&lt;/a&gt; and &lt;a href="https://endertech.com/" rel="noopener noreferrer"&gt;engineering strategy&lt;/a&gt; pages — or read &lt;a href="https://endertech.com/blog/isolate-shared-state-before-adding-parallel-test-workers" rel="noopener noreferrer"&gt;the full original with sources&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Willem Luijt at &lt;a href="https://endertech.com/" rel="noopener noreferrer"&gt;Endertech&lt;/a&gt;. Originally published at &lt;a href="https://endertech.com/blog/isolate-shared-state-before-adding-parallel-test-workers" rel="noopener noreferrer"&gt;https://endertech.com/blog/isolate-shared-state-before-adding-parallel-test-workers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>testing</category>
      <category>phpunit</category>
    </item>
  </channel>
</rss>
