# Zactonz Number Plates for WooCommerce

> Sell made-to-order number plates. Shoppers configure a registration, see it drawn to scale as they type, and the order carries production-ready SVG artwork from the same renderer.

- **Type:** Plugin (WordPress plugins)
- **Version:** 1.0.0, released 14 Oct 2026
- **Requires:** WordPress 7.0+, WooCommerce 9.6+, PHP 7.4+
- **Tested with:** WordPress 7.1, WooCommerce 11.2
- **Licence:** GPL-2.0-or-later
- **Documentation:** https://developers.zactonz.com/wordpress/plugins/zactonz-number-plates-for-woocommerce/

## Overview

Zactonz Number Plates for WooCommerce turns a product into a number plate a shopper configures themselves. They type a registration, pick a plate type and size, and see the plate drawn at its true dimensions as they type. The order that reaches the merchant carries the same drawing as a scalable SVG, measured in millimetres, ready to send to a plotter or an engraver.

The drawing a shopper approves and the file the merchant manufactures from come out of one renderer, so they cannot disagree. Nothing is stored as an image: the order holds the registration and the choices behind it, and the artwork is rebuilt from them whenever it is asked for.

**Germany is the country profile that ships.** Plates follow Anlage 4 of the Fahrzeug-Zulassungsverordnung: the sizes it lists, its cap heights, the Euro field, and the FE-Mittelschrift typeface it prescribes, which is bundled. All 733 district codes are included, so a registration is checked against the real list rather than a pattern. Four plate types are offered: Standard, Motorcycle, Electric and Historic, with the E and H markings drawn on the artwork.

### What the shopper gets

- A live preview at the plate's real proportions, drawn on the server by the same code that produces the artwork.
- A picture of the plate they configured wherever the order is shown to them: the cart, the checkout, the confirmation email and their account, each one clicking through to the full-size plate.
- Immediate, specific feedback: an unknown district code, a registration too long for the plate type, a size that cannot fit what they typed.
- The price for the exact configuration, updated as they change it.

### What the merchant gets

- A per-product switch, so only part of a catalogue is configurable.
- A choice of three configurator layouts, Stacked, Studio and Guided, to suit a narrow product column, a wide one, or a catalogue where shoppers need leading through the choices.
- Optional surcharges per size and per plate type, on top of the product's own price. Leave them blank for one flat price.
- The plate drawn on each order line, beside a **Download plate artwork (SVG)** button that gives the vector to manufacture from. The registration is drawn as shapes, not as text, so the file needs no font installed and cannot come out in the wrong typeface whatever opens it.
- Validation that runs on the server. A plate that would be impossible to manufacture does not reach the cart.
- Compatibility with High-Performance Order Storage and with the WooCommerce Cart and Checkout blocks.

### No external services

The plugin makes no outbound requests, loads no fonts or scripts from a third party, and sends nothing anywhere. The preview is produced by your own site.

### What it does not do

It does not make you compliant with anything. Whether you may manufacture plates, and what you must record about who buys them, is between you and your regulator. This plugin draws plates; it makes no claim about your entitlement to sell them.

It does not offer Versicherungskennzeichen (moped plates) or Saisonkennzeichen (seasonal plates). Both need DIN 1451-2 for part of their lettering rather than FE-Schrift, the whole of a moped plate and the two months in a seasonal plate's operating-period field, and no DIN 1451 face may be redistributed. Offering either would produce a plate that cannot legally be used.

The longest registrations, eight positions on a plate that cannot fit them in Mittelschrift, are declined rather than drawn badly. The regulation's answer is the narrower FE-Engschrift, and no Engschrift face was found under a licence that permits redistribution. A merchant licensed to use one adds it with a filter; see [Developers](https://developers.zactonz.com/wordpress/plugins/zactonz-number-plates-for-woocommerce/developers/).

### Bundled font

One font is bundled, because a German plate has to be set in FE-Schrift: the typeface is part of the legal specification, not a styling choice. **GL-Nummernschild-Mtl**, version 2.018, by [Gutenberg Labo](http://gutenberg.osdn.jp/), is derived from Karlgeorg Hoefer's FE-Mittelschrift. Its own name table grants unlimited permission to use, copy and distribute it, with or without modification, commercially or not, as is and without warranty. That all-permissive grant is compatible with the plugin's GPL-2.0-or-later licence; the font remains the copyright of Gutenberg Labo and is not covered by that licence. The full text ships beside the font as `assets/fonts/LICENSE-GL-Nummernschild.txt`.

No other font is bundled. Faces for other countries, and FE-Engschrift for the longest German registrations, are added by the merchant from a file they are licensed to use.

### Accessibility

Plate type and size are radio groups rather than dropdowns, so they keyboard and announce correctly without help, and the Guided layout uses the browser's own accordion. All three layouts report zero violations under axe-core 4.10.2 at desktop and at 375 pixels wide.

### Not affiliated

This plugin is developed by Zactonz Technologies and is not affiliated with WooCommerce, Automattic, or any German registration authority.

## Installation & setup

### Requirements

- WordPress 7.0 or newer and PHP 7.4 or newer.
- WooCommerce 9.6 or newer, installed and active. The plugin declares WooCommerce as a required plugin, so WordPress will not activate it without WooCommerce.
- GD with FreeType, which almost every host has, for the PNG picture of the plate in the cart, checkout and email. Without it the plugin shows the vector on the web and leaves the picture out of the email rather than sending a broken one.

### Install the plugin

This plugin is awaiting review for the WordPress plugin directory. The directory steps below are the route once it is listed; until then, install it from the plugin zip.

From the dashboard, go to **Plugins › Add New**, search for **Zactonz Number Plates for WooCommerce** and click **Install Now**, then **Activate**.

With WP-CLI:

```bash
wp plugin install zactonz-number-plates-for-woocommerce --activate
```

To install manually, upload the folder to `/wp-content/plugins/zactonz-number-plates-for-woocommerce/` and activate it from the **Plugins** menu.

### Make a product a number plate

1. Edit a product and open the **Number plate** tab in **Product data**.
2. Tick **Sell as a number plate**.
3. Choose the country. Germany is the profile that ships.
4. Choose a layout for the configurator: Stacked, Studio or Guided. See [Layouts & artwork](https://developers.zactonz.com/wordpress/plugins/zactonz-number-plates-for-woocommerce/layouts/).
5. Set surcharges per size or per plate type if you want them. Leave them blank for one flat price.
6. Publish. The configurator appears on the product page.

Nothing else is configured. Plate sizes, cap heights and the typeface come from the regulation, not from a settings screen, so there is no size to type in and nothing to get wrong.

### What a shopper sees

The product page shows the plate drawn at its real proportions, redrawn as the shopper types. Plate type and size are radio groups. An unknown district code, a registration too long for the plate type or a size that cannot fit what was typed is reported at once, and the price for the exact configuration updates with each change. A plate that would be impossible to manufacture cannot be added to the cart, because the same validation runs again on the server.

On a block theme, WooCommerce's Add to Cart block sees the configurator's form fields and renders an ordinary form instead of its no-reload path. That is WooCommerce's own fallback and is what makes the same fields work on classic and block themes alike.

### Where the plate appears

Everywhere the order is shown: the cart, the checkout, the order confirmation, the order email, the customer's account page and the order screen in your admin. It is a PNG, because no mail client of consequence renders SVG, and the link beside it always opens the full-size vector. On the Cart and Checkout blocks the plate takes the place of the product photograph, because those blocks render line item details through a component that allows no images.

### Download the artwork

Open the order and find the plate line. Beside the drawing is **Download plate artwork (SVG)**. The file is measured in millimetres and every character is a filled shape taken from the bundled font, so it renders identically in a cutter, a plotter or a browser and needs no font installed.

### Remove the plugin

Deleting the plugin leaves your product settings alone, because deleting a plugin is usually how it gets replaced. To remove them as well, define `ZCTZ_NP_REMOVE_DATA` as true in `wp-config.php` before you delete:

```php
define( 'ZCTZ_NP_REMOVE_DATA', true );
```

Orders are never touched either way. Each order line records the rules it was configured under, so its artwork stays available after the product changes or is deleted.

## Layouts & artwork

### The three layouts

Pick one per product, in the Number plate tab. All three collect the same values and are driven by the same script, so nothing about validation, pricing or artwork changes with the choice.

- **Stacked**: the plate above, the fields in one column under it. Fits any theme and any column width; the safe default.
- **Studio**: the plate held beside the controls, on a tinted stage. It measures its own column rather than the window, so in a narrow theme it folds back to a single column by itself instead of squeezing.
- **Guided**: three numbered steps, one open at a time, built on the browser's own accordion so it needs no script to collapse. Where the browser cannot keep one step open at a time, all three open, which is the Stacked layout again.

Plate type and size are radio groups rather than dropdowns: a shopper comparing four plate types should see four plate types, and a set of mutually exclusive choices is what a radio group is, so it keyboards and announces correctly without help.

### Your theme styles the controls

The plugin sets no colours, no borders on inputs, no button look and no type sizes. It arranges the fields and decides how wide the plate may be, and leaves the rest to your theme, so the configurator matches your shop on the day you install it and still matches it after a redesign. The one edge it draws, the Guided step boxes, is taken from the current text colour at low opacity rather than a grey of its own, so it reads correctly on a dark theme too.

Developers can set the layout for every product that names none with `zctz_np_default_template`, and add a layout of their own with `zctz_np_templates` and `zctz_np_render_template`. See [Developers](https://developers.zactonz.com/wordpress/plugins/zactonz-number-plates-for-woocommerce/developers/).

### One renderer for preview, email and production

There is a single layout engine. The live preview, the file the merchant manufactures from and the thumbnail in the order email all come from the same computation of every position in millimetres. A JavaScript preview would have been a second layout engine free to drift from the first, and a plate that looks right in a browser and wrong at 520 mm is the failure this product exists to avoid.

Lettering is centred between the Euro field and the far border, never stretched to fill the width, because Anlage 4 requires the distance from the lettering to each edge to be equal. The gaps inside the registration have the ranges the regulation gives and stay inside them, which is why a long registration is set a little tighter than a short one and a short registration has wide, equal margins. Everything is measured to the ink rather than to a typesetter's box, so the 8 mm minimum clearance is a distance to the letters themselves.

### Why the artwork contains no text

An SVG that sets text depends on whatever opens it honouring the embedded font, and production software often does not: a plate opened in a cutter or a plotter can come out in a substituted typeface with the umlauts missing, which is both wrong and not registrable. Every character in the download is therefore a filled shape taken from the same font, so the file renders identically everywhere and needs nothing installed. It is also about a fifth of the size, and a product page fetches no font file at all.

### The picture in the cart and the email

The picture shown on the cart, the checkout, the email and the account page is a PNG drawn from the same layout, because mail clients do not render SVG. It is drawn larger than needed and scaled down so curves are smooth, with the working size chosen per request so that a small thumbnail gets full smoothing and a large one does not cost many times more. The largest size the image endpoint will draw is capped. Each picture is a pure function of the registration, the choices, the format and the height, so it carries an ETag and a caller that already holds it is answered without drawing anything.

## FAQ

### Is the preview the same thing I manufacture?

Yes, and that is the point of the design. One layout computes every position, in millimetres, and everything is drawn from it: the preview on the product page, the picture in the email, and the artwork you download. All three draw the registration as shapes rather than setting it as text, so none of them depends on a font arriving and being honoured. A shopper cannot be shown their plate in a substituted typeface and approve it, and the file you manufacture from needs nothing installed.

### Why does a short registration leave so much white space?

Because the regulation asks for it. Anlage 4 requires the horizontal distance from the lettering to the border and to the Euro field to be the same on both sides, so the characters are centred rather than stretched across the plate. A three-character registration on a 520 mm plate genuinely has wide, equal margins. In practice a registration that short is ordered on a narrower plate, and the plate sizes a shopper can pick come from Anlage 4 rather than from this plugin.

The gaps inside the registration are not stretched either. They have the ranges the regulation gives and stay inside them, which is why a long registration is set a little tighter than a short one.

### Which plate types are offered?

Standard, Motorcycle, Electric and Historic. The E and H markings on Electric and Historic plates are drawn by the renderer in the same typeface, as the regulation requires, and do not count towards the position limit.

Moped plates (Versicherungskennzeichen) and seasonal plates (Saisonkennzeichen) are deliberately not offered. Both need DIN 1451-2 for part of their lettering, and no DIN 1451 face may be redistributed, so drawing them would mean setting a regulated plate in the wrong typeface.

### Why is a long registration refused?

Eight-position registrations on a plate that cannot fit them in FE-Mittelschrift need the narrower FE-Engschrift, which is not bundled because no redistributable Engschrift face was found. Rather than draw the plate badly, the plugin declines it with a reason. A merchant licensed to use an Engschrift face adds it with the `zctz_np_profile_faces` filter and those registrations start rendering.

### Where do the forbidden letter combinations come from?

The regulation says a registration must not offend against good morals; it does not publish a list. Germany's registration offices maintain their own, and they differ. The plugin therefore ships a short, documented default and exposes `zctz_np_blocked_sequences` so you can match the office you actually deal with. Treat the default as a starting point, not as law.

### Can I add another country?

Yes, with the `zctz_np_profiles` filter. The engine already treats a country as data: ordered fields with character classes, sizes in millimetres, colours, geometry and typefaces. What a new country needs is that data plus a typeface you are licensed to use, because the face is usually part of the specification. Germany ships complete; no other profile does yet.

Register it from your own plugin and keep its files there, because anything inside this plugin's directory is replaced when it updates.

### Can a shop owner add a plate size?

Not from the admin, by design. German plate sizes are fixed by Anlage 4; a size typed into a settings screen would produce a plate that cannot be registered, which is the failure this plugin exists to prevent. A developer who needs different sizes registers a profile of their own with the filter above.

### Does it work with the block Cart and Checkout?

Yes. The configured plate is attached through WooCommerce's own cart item data, so the classic cart and the block cart carry the same plate, and the plugin declares compatibility with High-Performance Order Storage and with the Cart and Checkout blocks. On the blocks the plate takes the place of the product photograph, because those blocks render line item details through a component that allows no images.

One limit for headless integrators: the Store API's add-item route carries the plate only when the request is form-encoded, as WooCommerce's own add-to-cart form sends it. A JSON body is refused with "Choose a plate type". Shoppers are unaffected, because on a block theme the product page renders the classic form and the blocks only read the cart.

### Why does my block theme's Add to Cart reload the page?

Because the configurator puts real form fields on the product page. WooCommerce's Add to Cart + Options block looks for form elements from extensions and, finding them, renders an ordinary form instead of its no-reload path. That fallback is WooCommerce's own behaviour, and it is what makes the same fields work on classic and block themes alike.

### Can a customer order the same plate again?

Yes. WooCommerce's **Order again** carries the configured plate forward into a new cart. WooCommerce shows that button only on completed orders, so a shop that leaves orders in the processing state will not see it until the order is completed.

### Are any custom database tables created?

No. Profiles are code and data, the product binding and surcharges are product meta, and the configured plate lives on the cart item and then the order line. There is nothing to migrate.

### What happens to an old order if I change the product?

Nothing. Each order line records the country rules it was configured under, so the artwork is still there after you switch the product to another country, turn plate mode off, or delete the product outright. An order you took is a record of what someone bought, and it stays manufacturable.

### Does it connect to anything outside my site?

No. The plugin makes no outbound requests, loads no fonts or scripts from a third party, and sends nothing anywhere.

## Developers

Everything below is a WordPress filter. Register your callbacks from your own plugin and keep any files they need there, because anything inside this plugin's directory is replaced when it updates.

### Typefaces

`zctz_np_profile_faces` gives the typefaces a country profile may set a plate in, in preference order. The first face that measures every character and fits the plate wins, which reproduces the regulation's own Mittelschrift-then-Engschrift rule without the renderer knowing anything about Germany. A face that cannot measure a character is passed over for plates containing it rather than drawn with a glyph of zero width.

The usual reason to use it is FE-Engschrift for the longest German registrations, which is not bundled. A merchant licensed to use one adds it like this:

```php
add_filter(
	'zctz_np_profile_faces',
	function ( array $faces, string $profile_id ): array {
		if ( 'de' !== $profile_id ) {
			return $faces;
		}

		$faces[] = array(
			'family'  => 'FE-Engschrift',
			'path'    => '/absolute/path/to/engschrift.woff',
			'url'     => 'https://example.com/fonts/engschrift.woff',
			'metrics' => array( /* advance widths in font units */ ),
		);

		return $faces;
	},
	10,
	2
);
```

The metrics are the face's advance widths in font units. The plugin's source ships `tools/font-metrics.php`, which measures a WOFF or TrueType file and prints the array ready to paste. WOFF2 is refused, because its table stream is Brotli-compressed and PHP bundles no Brotli.

### Country profiles

`zctz_np_profiles` maps a profile identifier to its specification array, or to the path of a PHP file that returns one:

```php
add_filter( 'zctz_np_profiles', function ( array $registered ): array {
	$registered['at'] = __DIR__ . '/profiles/at-profile.php';

	return $registered;
} );
```

A profile is data: ordered fields with character classes, sizes in millimetres, colours, geometry and typefaces. One registry answers both which countries a merchant may pick and which one to load, so a country you add is genuinely usable rather than merely listed. Give the profile its font metrics and glyph outlines inline as arrays rather than naming a data file. A callback may build on a shipped profile by calling `Profile::get( 'de' )` inside the filter; the call is re-entrant and the filter runs once per request.

### Blocked letter sequences

`zctz_np_blocked_sequences` is the list of letter sequences refused on the Erkennungsnummer. The shipped list is a convention rather than a statute, because Germany's registration offices keep their own lists and they differ. Replace or extend it to match the office you deal with.

### Layouts

- `zctz_np_default_template` sets the layout for every product that names none, which saves setting it on each one.
- `zctz_np_templates` registers a layout of your own, and `zctz_np_render_template` renders it. All layouts collect the same fields with the same names and data attributes and share one stylesheet and one script, so a layout owns neither validation nor pricing and cannot drift from the others on either.

### Switching parts off

Six filters each switch off one part of the plugin: `zctz_np_load_admin`, `zctz_np_load_artwork`, `zctz_np_load_configurator`, `zctz_np_load_cart`, `zctz_np_load_pricing` and `zctz_np_load_rest`. Return false from one to leave that part unloaded.

### Removal

Define `ZCTZ_NP_REMOVE_DATA` as true in `wp-config.php` before deleting the plugin to remove the product settings as well. Orders are never touched.

### Store API

The configured plate is attached through `woocommerce_add_cart_item_data`, which WooCommerce's Store API applies as well. The add-item route carries the plate only for a form-encoded request; a JSON body is refused with "Choose a plate type". The cart and checkout blocks are unaffected because they only read the cart.

## Changelog

### 1.0.0 — 14 October 2026

- First release.
- Three configurator layouts, Stacked, Studio and Guided, chosen per product.
- German profile complete: Anlage 4 sizes and cap heights, four plate types with the E and H markings drawn, all 733 district codes, bundled FE-Mittelschrift.
- Server-rendered live preview at true millimetre scale, shared with the production artwork.
- Server-side validation of district code, structure, length and per-type position limits.
- Optional surcharges per size and per plate type.
- Production SVG download on the order screen, with every character drawn as a shape so the file needs no font installed.
- Compatible with High-Performance Order Storage and the cart and checkout blocks.
- The configured plate shown as a picture on the cart, the checkout, the order, the email and the account, each linking to the full-size vector.
- No external services: no outbound requests, no third-party fonts or scripts.
