WordPress EngineeringNamed client

GS1 Digital Link Resolver on WooCommerce

Packaging QR codes resolved on the manufacturer's own site, with validated GTINs, batch data and a first-party scan log

By Lotfi Otmanine

Client
QMagnets, Australian magnetic therapy devices
Engagement
Standalone fixed-scope plugin build, in progress
Stack
WordPressWooCommercePHPGS1 Digital LinkCustom DB TablesCustom Post TypesSettings APIoEmbed

Key result

170 / 0

test assertions / failures

01

Context

QMagnets prints GS1 QR codes on its device packaging. Each code carries a GS1 Digital Link URI on the company's own domain: the GTIN under application identifier (01) and, for most product lines, the batch or lot under (10). Nothing answered those paths. The store returned its standard 404, and GS1's global and Australian resolvers had no links registered for the GTINs either, so every scan dead-ended.

The client specified a dedicated identification and support page rather than a jump to the shop: one fixed layout, with the manufacturer block and the instructions-for-use booklet link in the same place on every page, and dynamic data drawn from the GTIN and from a batch table maintained by hand in WordPress, including manufacture date, batch status and, for white-labelled batches, the distributor. The packaging print run was imminent, so the GTIN data had to be checked before any plugin code was written.

02

The Challenge

  • The source spreadsheet appeared to mix 13 and 14 digit GTINs, while the Digital Link URI syntax requires every GTIN in the path zero-padded to 14 digits, and the packaging was about to print.
  • Spreadsheet SKUs did not always match store SKUs: some products matched only by name, two were missing from the staging copy, and one GTIN was ambiguous across four products.
  • In a traceability system the batch code is identity. Any silent change in decoding, trimming or sanitizing would display one batch as another, with nothing to show it had happened.
  • GTINs under one company prefix are sequential and the check digit is public, so any lookup that reached unreleased products would let anyone enumerate them.
  • The page had to look identical on every scan and survive a theme change, and the shop manager had to maintain mappings, batches and distributors without a developer.

03

Scope and Approach

Data pass ahead of the print run

Every GTIN in the catalogue cleaned, padded to 14 digits and check-digit validated, with a per-product URL template in the /01/{GTIN}/10/{batch} form ready for the GS1 code generator, and batch-free URLs for point-of-sale lines. The apparent 13 digit values turned out to be Excel dropping leading zeros, plus 17 tab-polluted cells. Products mapped to the store: most by exact SKU, the rest by name, and a few flagged for the client.

Routing and validation

A standalone plugin intercepts requests at parse_request, priority 1, for GET and HEAD only, before WordPress resolves any query. Two path shapes are accepted: /01/{GTIN} and /01/{GTIN}/10/{batch}. GTIN-8, 12, 13 and 14 input is padded to 14 digits and the modulo 10 check digit verified; the batch is checked against GS1 character set 82, 20 characters at most. Anything else, including /21/ serials, gets a branded 404.

Identification page and batch register

The page renders as a self-contained document outside the theme, with header, device card, booklet link and manufacturer footer in fixed positions. Device name, model, image and store link come from the GTIN; manufacture date, status and distributor come from a custom batch table, unique on GTIN and batch, with a searchable admin screen. Distributor profiles and per-family instructions, with category defaults, fill the optional sections.

Scan log and editor access

Each resolved or unmatched scan is logged with GTIN, batch, product, outcome and time. No IP address or location is stored. A repeat of the same code and outcome within 60 seconds is deduplicated against the log table itself, and malformed requests are never written. Settings save under the shop manager capability, so page content stays editable without administrator access.

Adversarial verification

Four finder passes (scope, security, WordPress and WooCommerce correctness, data reality) were each challenged by an independently prompted refuter, and no finding was accepted until a refuter had tried to disprove it. Claims were traced into WordPress 6.9 and WooCommerce 10.7 source and reproduced in isolated harnesses. 18 findings were confirmed and fixed; the rest were dismissed with evidence.

04

Architecture flow

Printed GS1 QR code
Digital Link URI, own domain
parse_request intercept
/01/{GTIN}[/10/{batch}]
Validation gate
14 digits, check digit, CSET 82
valid
Published product lookup
native GTIN field
invalid or unknown
Branded 404
noindex
Batch table match
date, status, distributor
Scan log
no IP, 60 s dedupe
Identification page
fixed layout, theme-independent

05

Technical Deep Dive

A check digit that failed almost every real GTIN

The first check digit routine applied GS1's alternating weights from the wrong end and rejected nearly all of the client's genuine GTINs. Testing against the real spreadsheet, not synthetic examples, exposed it at once: weight 3 belongs on the leftmost of the 13 data digits. The same harness showed that PHP's trim() strips null bytes, so a batch QB%00 was accepted as QB. Control characters are now rejected outright.

Batch codes that changed in transit

Under plain permalinks the router falls back to the request URI, and sanitize_text_field() strips percent-encoded octets, so /10/QA%2D0010 silently became QA0010, a different batch. The fallback now reads the raw path and validates each segment instead. A second asymmetry decoded admin-typed codes as well as scanned ones, so a stored code containing % could never match; only URI values are decoded now.

Unreleased products were enumerable

WooCommerce's GTIN lookup excludes only trashed products, and the plugin's own fallback query had no status filter, so a draft's name, image and content would render publicly for anyone requesting its GTIN. Lookups now resolve only published, non-password products, with a published parent for variations, and an unpublished distributor no longer appears on scanned devices.

A scan log without personal data

The first throttle stored a transient per IP, code and outcome: extra options rows for every distinct request, and a second person scanning the same code behind a shared network was silently dropped. It became a 60 second dedupe query on the scans table's existing indexes. No IP is read anywhere, invalid requests are not logged, and retention is left as a client decision.

06

Engineering Decisions

Parse the URI on site, no hosted redirect

The codes already point at the client's own domain, and the page needs batch data and a first-party scan log, which a hosted redirect service cannot supply. Requests to GS1's global and Australian resolvers returned 404 for the client's GTINs, confirming nothing upstream sat in the path. The site parses the URI and renders in place, with no redirect.

The store's own GTIN field is the mapping

GTINs live in WooCommerce's native GTIN field on each product or variation, edited from the normal product screen. Lookups try the 14 digit form and each shorter stored form, and ignore hyphens, because WooCommerce keeps whatever the operator typed. There is no parallel mapping table to drift out of sync.

A page, not a full resolver platform

Version 1 handles the GTIN and batch path forms only. Serial numbers under (21), linkset and JSON responses, and recall workflows are outside the client's brief, so /21/ paths fall to the branded error page rather than being half supported. A batch status of Withdrawn is a label, not recall support, and the documentation says so.

07

Results

  • A 14 digit Digital Link URL list for every GTIN, every check digit valid, produced ahead of the packaging print run together with the URL template for the GS1 code generator.
  • A self-contained resolver plugin covering all 18 elements of the client's page specification, each checked against the spec table before installation; artwork and document slots fill once the client supplies them.
  • A test harness of 170 assertions with 0 failures, covering every real GTIN, the spreadsheet's tab-polluted values, legacy 13 digit forms and hostile input such as injection strings, path traversal, null bytes and oversize values.
  • 18 review findings confirmed and fixed before installation, including a high-severity exposure of unreleased products, with every claim tested by an independent refuter.

08

Planned next

  • Staging installation: activation and table creation, end-to-end scans of generated QR codes across product families, and checks against the store's theme and page cache.
  • Loading the GTINs onto store products and adding the client's content: instructions-for-use booklet, manufacturer symbol, per-family instructions and distributor records.
  • Production deployment from a packaged plugin with exact install steps, followed by live scan verification.

Have a site that needs this kind of care?

Tell me what is breaking or what you need built. I will reply with how I would approach it and what I would check first.

Custom quote within 24 hours

Chat on WhatsApp