Product Descriptions That Cannot Drift

Reading Time: 5 minutes
951 words

5 min read


5 parts, 20 paragraphs

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 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.

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.

Facts First, Prose Second

The fix is to stop writing the numbers. One script opens each board and schematic and writes JSON:

collect_product_facts.py  →  _product_facts.json  →  build_descriptions.py  →  HTML

It pulls the board outline, the physical stackup in order with each layer’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:

BGA-256 BGA-900 BGA-1536
Board size (mm) 182.65 × 70.42 172.48 × 69.78 256.68 × 112.00
Footprints / pads 523 / 2,492 523 / 3,136 525 / 3,804
Tracks 7,205 6,707 8,388
Vias 1,136 through 969 through 1,753 through, 222 micro, 22 blind
Nets 1,073 1,452 1,708
Schematic sheets 20 20 28
Copper layers / netclasses 12 / 11 12 / 11 12 / 11

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.

It Also Publishes The Bad News

The interesting side effect is that a generator does not know to be flattering.

All three boards carry 191 nets whose names begin with unconnected-. Those are placeholders, not connections. And all three carry 15 components marked do-not-populate — the power section, deliberately left unpopulated pending a design decision, along with a documented over-voltage condition on U1.

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.

Writing this by hand, the temptation to leave it out is real. Generating it, the temptation never arises.

The Icons Are Drawn, Not Generated

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.

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 — 8 × 6, 10 × 7, 14 × 9 — so the icons are informative as well as decorative.

Three Magento Traps, For Anyone Who Hits Them

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.

Gallery images must be staged inside pub/media

Magento’s PathValidator rejects a path in /tmp outright. Stage the file in pub/media/import and the identical call succeeds. The error does not mention the path.

A zero-priced downloadable link blocks every full save

The downloadable ContentValidator refuses to save a product whose link has a price of zero — even when links_purchased_separately is off, which means the link price is dead data that no customer is ever charged. It fires on any full save(), so it blocks attaching a gallery image, a completely unrelated operation:

Link price must have numeric positive value.

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.

Store-scope overrides silently swallow your writes

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 Product\Action::updateAttributes at every store scope that carries an override fixes it — and as a bonus, sidesteps the link validator above, because it does not perform a full product save.

The Principle

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 “kept in sync” — derived, every time the page is built.

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.

The AutoPhi V19 blueprints — fabrication packages and full editable design sources — are at the store. Every number on those pages was read out of the files you would be buying.