# Generate a Storefront Sign Layout Proof from JSON in Node.js

A sign brief can have approved copy and still leave reviewers guessing about the panel proportions. Generate a small SVG from the agreed copy and dimensions before making a photo mockup. The proof puts the exact text, a panel outline and an inset guide in one file that can be reviewed in a browser.

## 1. Write down the panel and copy

Save this example as brief.json. Width and height define the panel proportions. Inset, fontSize and lineStep are in the same SVG coordinate units. Enter each intended line as a separate string, including any punctuation that belongs on the sign.

```json
{
  "width": 720,
  "height": 260,
  "inset": 32,
  "fontSize": 44,
  "lineStep": 60,
  "lines": ["Fern & Field", "Coffee"]
}
```

## 2. Generate the proof

Save this function as sign-proof.mjs. The SVG viewBox defines the panel coordinate system. The inset rectangle shows the space set aside for copy. Each text line starts at the left inset and uses its own baseline.

```javascript
function xmlText(value) {
  if (typeof value !== "string" || !value.trim() || value.length > 120) {
    throw new TypeError("Each sign line must contain 1–120 UTF-16 code units");
  }
  for (const character of value) {
    const point = character.codePointAt(0);
    const valid = (point >= 0x20 && point <= 0xD7FF) ||
      (point >= 0xE000 && point <= 0xFFFD) ||
      (point >= 0x10000 && point <= 0x10FFFF);
    if (!valid) throw new TypeError("Use one line of valid XML text");
  }
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

export function buildSignProof(brief) {
  const { width, height, inset, fontSize, lineStep, lines } = brief ?? {};
  for (const value of [width, height, inset, fontSize, lineStep]) {
    if (!Number.isInteger(value) || value < 1 || value > 10000) {
      throw new RangeError("Dimensions must be integers from 1 to 10000");
    }
  }
  if (2 * inset >= width || 2 * inset >= height || lineStep < fontSize ||
      !Array.isArray(lines) || lines.length < 1 || lines.length > 4) {
    throw new RangeError("Check inset, line spacing, and number of lines");
  }
  const lastBaseline = inset + fontSize + (lines.length - 1) * lineStep;
  if (lastBaseline >= height - inset) {
    throw new RangeError("Last baseline reaches the lower guide");
  }

  const labels = Array.from(lines, (line, index) => {
    const baseline = inset + fontSize + index * lineStep;
    return '  <text x="' + inset + '" y="' + baseline + '" font-size="' +
      fontSize + '" fill="#ffffff" font-family="sans-serif">' + xmlText(line) + '</text>';
  });
  const guide = '  <rect x="' + inset + '" y="' + inset + '" width="' +
    (width - 2 * inset) + '" height="' + (height - 2 * inset) +
    '" fill="none" stroke="#98a7b6" stroke-dasharray="8 6"/>';
  return [
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + width + ' ' + height + '">',
    '  <rect width="100%" height="100%" fill="#243449"/>',
    guide,
    ...labels,
    '</svg>',
  ].join("\n");
}
```

The function escapes ampersands and angle brackets before inserting the text. It rejects malformed text and obvious vertical layout errors, including a final baseline that reaches the lower guide. A baseline is a position for drawing text. Glyphs can extend below it or beyond the right guide, so inspect the rendered proof.

## 3. Write a new SVG file

Save the command as make-proof.mjs. It reads the brief and writes a new SVG. The exclusive file flag keeps a rerun from replacing an existing proof.

```javascript
import { readFileSync, writeFileSync } from "node:fs";
import { buildSignProof } from "./sign-proof.mjs";

const [input, output] = process.argv.slice(2);
if (!input || !output || !output.toLowerCase().endsWith(".svg")) {
  console.error("Usage: node make-proof.mjs brief.json proof.svg");
  process.exitCode = 2;
} else {
  try {
    const brief = JSON.parse(readFileSync(input, "utf8"));
    writeFileSync(output, buildSignProof(brief), { flag: "wx" });
  } catch (error) {
    console.error(error.message);
    process.exitCode = 2;
  }
}
```

## 4. Run the example and inspect the panel

Run node make-proof.mjs brief.json proof.svg, then open proof.svg in a browser. For this example, the guide is 656 by 196 units and the baselines are at 76 and 136. Check the exact shop name, then look for text that extends across the guide. Font substitution can change how much space a line occupies.

## 5. Test geometry and text handling

Save these assertions as test.mjs and run node test.mjs. They cover the sample geometry, XML escaping, invalid characters and the boundary where the last baseline meets the guide.

```javascript
import assert from "node:assert/strict";
import { buildSignProof } from "./sign-proof.mjs";
const base = { width: 720, height: 260, inset: 32, fontSize: 44, lineStep: 60,
  lines: ["Fern & Field", "Coffee"] };
const svg = buildSignProof(base);
assert.match(svg, /viewBox="0 0 720 260"/);
assert.match(svg, /width="656" height="196"/);
assert.match(svg, /y="76".*Fern &amp; Field/);
assert.match(svg, /y="136".*Coffee/);
assert.match(buildSignProof({ ...base, lines: ["<Coffee> 咖啡"] }), /&lt;Coffee&gt; 咖啡/);
assert.throws(() => buildSignProof({ ...base, lines: ["bad\nline"] }), TypeError);
assert.throws(() => buildSignProof({ ...base, lines: ["\uD800"] }), TypeError);
assert.throws(() => buildSignProof({ ...base, lines: Array(1) }), TypeError);
assert.throws(() => buildSignProof({ ...base, width: "720" }), RangeError);
assert.throws(() => buildSignProof({ ...base, inset: 130 }), RangeError);
assert.throws(() => buildSignProof({ ...base, lineStep: 43 }), RangeError);
assert.throws(() => buildSignProof({ ...base, height: 168 }), RangeError);
console.log("12 checks passed");
```

## 6. Keep the approved brief with the proof

Save the input JSON beside each version of the SVG. When someone requests new wording or an extra line, change the brief and generate a new file. Use the proof to confirm the flat panel layout before checking perspective and legibility in a separate storefront mockup.

The example uses fictional copy and a generic font. Drafted with AI assistance.

PixMind source: [How to Write AI Storefront Sign Briefs](https://www.pixmind.io/posts/ai-sign-design-prompts).

Technical reference: [W3C SVG viewBox](https://www.w3.org/TR/SVG2/coords.html#ViewBoxAttribute).
