{"id":912038,"date":"2026-07-27T00:17:35","date_gmt":"2026-07-27T00:17:35","guid":{"rendered":"https:\/\/cri-one.com\/blog\/?p=912038"},"modified":"2026-07-27T00:17:35","modified_gmt":"2026-07-27T00:17:35","slug":"product-descriptions-that-cannot-drift","status":"publish","type":"post","link":"https:\/\/cri-one.com\/blog\/2026\/07\/27\/product-descriptions-that-cannot-drift\/","title":{"rendered":"Product Descriptions That Cannot Drift"},"content":{"rendered":"<p style=\"font-size:18px;font-weight:bold;color:#1a1a2e;background:#f0f0ff;padding:16px 20px;border-left:4px solid #302b63;border-radius:4px;margin:0 0 1.5em\">Three product pages, about 1,800 words each, and not one number typed by hand. Every figure is read out of the design files at build time, so the copy cannot quietly stop being true.<\/p>\n<p>Selling a PCB design is an unusual retail problem. The buyer cannot open the box. They are paying for schematics, a routed board, libraries and 3D models, and the only way to judge any of it before purchase is the description. So the description has to be specific, and specific claims rot.<\/p>\n<p>Change a netclass, re-route a region, add a decoupling cap, and every hand-written sentence about layer counts and via geometry becomes a small lie. Not deliberately. Just because the page and the board are two copies of the same facts and only one of them got updated.<\/p>\n<h2>Facts First, Prose Second<\/h2>\n<p>The fix is to stop writing the numbers. One script opens each board and schematic and writes JSON:<\/p>\n<pre style=\"background:#1a1a2e;color:#e0e0f0;padding:14px 18px;border-radius:4px;overflow-x:auto;font-size:13px\">collect_product_facts.py  &rarr;  _product_facts.json  &rarr;  build_descriptions.py  &rarr;  HTML<\/pre>\n<p>It pulls the board outline, the physical stackup in order with each layer&#8217;s type, per-layer track counts, a via census by type and by pad\/drill geometry, the component and pad totals, every netclass with its clearance and widths, the net count, the schematic sheet list, and the do-not-populate list. What comes out:<\/p>\n<table style=\"width:100%;border-collapse:collapse;margin:1em 0\">\n<tbody>\n<tr style=\"background:#f0f0ff\">\n<td style=\"padding:6px 12px;border:1px solid #ddd;font-weight:bold\"><\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd;font-weight:bold\">BGA-256<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd;font-weight:bold\">BGA-900<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd;font-weight:bold\">BGA-1536<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Board size (mm)<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">182.65 &times; 70.42<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">172.48 &times; 69.78<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">256.68 &times; 112.00<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Footprints \/ pads<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">523 \/ 2,492<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">523 \/ 3,136<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">525 \/ 3,804<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Tracks<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">7,205<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">6,707<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">8,388<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Vias<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">1,136 through<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">969 through<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">1,753 through, 222 micro, 22 blind<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Nets<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">1,073<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">1,452<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">1,708<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Schematic sheets<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">20<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">20<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">28<\/td>\n<\/tr>\n<tr>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">Copper layers \/ netclasses<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">12 \/ 11<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">12 \/ 11<\/td>\n<td style=\"padding:6px 12px;border:1px solid #ddd\">12 \/ 11<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Re-run it after a board change and the pages update themselves. The generator is a hundred and forty lines. It has paid for itself several times over already, mostly in claims it stopped me making.<\/p>\n<h2>It Also Publishes The Bad News<\/h2>\n<p>The interesting side effect is that a generator does not know to be flattering.<\/p>\n<p>All three boards carry 191 nets whose names begin with <code>unconnected-<\/code>. Those are placeholders, not connections. And all three carry 15 components marked do-not-populate &mdash; the power section, deliberately left unpopulated pending a design decision, along with a documented over-voltage condition on U1.<\/p>\n<p>None of that is flattering, and all of it is in the description, in its own section, because the script that counts tracks also counts DNP parts and does not have an opinion about which numbers are good for business. A buyer finding out after purchase that a section is unpopulated is a refund and a bad reputation. A buyer reading it on the product page before they click is an informed buyer.<\/p>\n<p>Writing this by hand, the temptation to leave it out is real. Generating it, the temptation never arises.<\/p>\n<h2>The Icons Are Drawn, Not Generated<\/h2>\n<p>Four products needed images that read as a family. Photographs of bare PCBs all look alike and none of them looks like a blueprint, so the icons are drawn: a flat PCIe card in a minimal style, board outline, gold finger comb split at the PCIe key, the BGA as a ball grid, QSFP cages on the I\/O edge, bracket down the side.<\/p>\n<p>Two details do most of the work. Every shape carries a thin dark outline, which is most of why the style reads the way it does. And every icon is drawn on one shared coordinate grid, so the four line up as a set instead of four separate pictures that happen to use the same colours. The ball grid density encodes the part &mdash; 8&nbsp;&times;&nbsp;6, 10&nbsp;&times;&nbsp;7, 14&nbsp;&times;&nbsp;9 &mdash; so the icons are informative as well as decorative.<\/p>\n<h2>Three Magento Traps, For Anyone Who Hits Them<\/h2>\n<p>Getting this onto a Magento 2 storefront cost more debugging than generating it did. All three of these fail in ways that do not name the real cause.<\/p>\n<h3>Gallery images must be staged inside pub\/media<\/h3>\n<p>Magento&#8217;s <code>PathValidator<\/code> rejects a path in <code>\/tmp<\/code> outright. Stage the file in <code>pub\/media\/import<\/code> and the identical call succeeds. The error does not mention the path.<\/p>\n<h3>A zero-priced downloadable link blocks every full save<\/h3>\n<p>The downloadable <code>ContentValidator<\/code> refuses to save a product whose link has a price of zero &mdash; even when <code>links_purchased_separately<\/code> is off, which means the link price is dead data that no customer is ever charged. It fires on <em>any<\/em> full <code>save()<\/code>, so it blocks attaching a gallery image, a completely unrelated operation:<\/p>\n<pre style=\"background:#1a1a2e;color:#e0e0f0;padding:14px 18px;border-radius:4px;overflow-x:auto;font-size:13px\">Link price must have numeric positive value.<\/pre>\n<p>Check the flag first. If links are genuinely sold separately, changing the price changes what buyers pay and you must not touch it. If they are not, set it to 1.00 and move on.<\/p>\n<h3>Store-scope overrides silently swallow your writes<\/h3>\n<p>Write a description at the default scope on a product that carries a store-level override, and the save succeeds, reports no error, and changes nothing a visitor sees. Writing through <code>Product\\Action::updateAttributes<\/code> at every store scope that carries an override fixes it &mdash; and as a bonus, sidesteps the link validator above, because it does not perform a full product save.<\/p>\n<h2>The Principle<\/h2>\n<p>Anything true of both the artifact and its description should be read from the artifact. Not copied, not summarised at the time of writing, not &#8220;kept in sync&#8221; &mdash; derived, every time the page is built.<\/p>\n<p>It is the same argument as generated API documentation, and it holds for the same reason: two hand-maintained copies of one fact are one fact and one future bug. The difference here is that the copy customers see is the one that gets out of date, because nobody runs a test suite against a product page.<\/p>\n<p>The AutoPhi V19 blueprints &mdash; fabrication packages and full editable design sources &mdash; are at <a href=\"https:\/\/cri-one.com\/store\/\">the store<\/a>. Every number on those pages was read out of the files you would be buying.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Three product pages, about 1,800 words each, and not one number typed by hand. Every figure is read out of the design files at build time, so the copy cannot quietly stop being true. Selling a PCB design is an unusual retail problem. The buyer cannot open the box. They are paying for schematics, a [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[593,580],"tags":[],"class_list":["post-912038","post","type-post","status-publish","format-standard","hentry","category-about-the-work","category-autophi"],"_links":{"self":[{"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/posts\/912038","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/comments?post=912038"}],"version-history":[{"count":1,"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/posts\/912038\/revisions"}],"predecessor-version":[{"id":912041,"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/posts\/912038\/revisions\/912041"}],"wp:attachment":[{"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/media?parent=912038"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/categories?post=912038"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cri-one.com\/blog\/wp-json\/wp\/v2\/tags?post=912038"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}