DEV Community

Cover image for Generating a Parametric SVG Logo with Pug
Joan Miquel Torres
Joan Miquel Torres

Posted on

Generating a Parametric SVG Logo with Pug

Designing a logo is usually treated as a one-off, visual task.
Designing a logo system—with variants, themes, sizes, and guarantees of consistency—is a very different problem.

New SmarkForm logo

In this article I’ll show how I ended up generating real SVG logo files, fully parametric, using Pug, embedded and subsetted fonts, and a CLI-driven pipeline.

This approach has been used to create a new SmarkForm logo, which will replace the current one starting from upcoming version 0.13.0.
The logo is not just an asset anymore: it is generated, reproducible, and version-controlled.

SmarkForm is a free and open-source toolkit for declarative, markup-driven form generation — from simple inputs to complex nested forms and dynamic lists, with first-class JSON import/export. It is framework-agnostic and styling-agnostic by design, and that same philosophy now applies to its branding.


The Goal

I wanted:

  • A single logo definition
  • Variants for:

    • light / dark
    • monochrome
    • compact / full
  • Fonts embedded and subsetted

  • Output as real .svg files

  • Generated via CLI, not a browser

  • Reproducible and scriptable

In short: branding as code.


Why Pug?

Pug gives you three things that are surprisingly powerful for SVG work:

  1. Logic (conditions, defaults, variants)
  2. Parametrization (options passed from CLI)
  3. Readable structure for complex SVG trees

SVG is XML.
Pug is extremely good at generating structured XML.

That combination is criminally underrated.


Turning SVG into a First-Class Template

The key decision was this:

Don’t generate HTML that contains SVG.
Generate SVG directly so that it can be linked everywhere.

That means:

  • doctype svg instead of (default in Pug) doctype html
  • The root node is <svg>
  • Follow svg specs

A Pug mixin as the logo engine

Implementing the svg as a Pug mixin let me actually start with an HTML document showcasing different parameters combinations.

Even colors and fonts were originally parametyzed allowing to examine several configurations side by side until the winner was choosen.

At the end of the process only functional parameters have been kept.

mixin smarkformLogo(options = {})
  -
    const {
      mode = 'light',
      compact = false,
      monochrome = false,
      size = 100
    } = options;

    const height = size;
    const width  = compact
      ? Math.round(height * 1.105)
      : Math.round(height * 4.1);

  svg(
    xmlns="http://www.w3.org/2000/svg"
    width=width
    height=height
    viewBox=`0 0 ${width} ${height}`
    role="img"
    aria-label="SmarkForm logo"
  )
    // SVG content here
Enter fullscreen mode Exit fullscreen mode

At this point we already have something valuable:

  • Dimensions are computed
  • Variants are driven by data
  • The SVG is no longer static

Choosing and Subsetting Fonts

External fonts are fragile:

  • Network-dependent
  • Inconsistent
  • Sometimes forbidden in branding assets

So the decision was to embed the font directly in the SVG.

Picking a font with the right license

For SmarkForm I used Work Sans, available from
👉 https://fonts.google.com

Google Fonts is particularly useful because you can filter by:

  • Open-source licenses (SIL Open Font License, Apache 2.0, etc.)
  • Font weights
  • Variable fonts

This makes it easy to ensure your branding assets are legally safe to embed and redistribute.

Subsetting the font

Instead of embedding a full font file, I generated subsetted WOFF2 files containing only the glyphs actually used in the logo.

This keeps the SVG:

  • Smaller
  • Faster to load
  • More intentional

Tools like pyftsubset (from fonttools) are perfect for this step.

Examnple:

pyftsubset static/WorkSans-Regular.ttf \
    --text="<SmarkForm}" \
    --flavor=woff2 \
    --output-file=WorkSans-SmarkForm-Regular.woff2
Enter fullscreen mode Exit fullscreen mode

In my case, for stylistic reasons, I used two different faces of the font so I had to do this twice (once for WorkSans-Regular and one for WorkSans-SemiBold).

You can stick with one or pick for even different fonts. I reckon keeping things simpler is better for a logo though.


Embedding Fonts via Base64

Once you have your subsetted .woff2, embedding it is straightforward.

From the shell:

base64 < WorkSans-SmarkForm-Regular.woff2
Enter fullscreen mode Exit fullscreen mode

Then inline it into the template.

A small stylistic trick I used:

const WorkSans_regular_b64 = (`
  base64 < WorkSans-SmarkForm-Regular.woff2
`).replace(/\s+/g, '');
Enter fullscreen mode Exit fullscreen mode

Notes:

  • Declaring binary (base64) data in a variable at the beginning keeps the actual code cleaner afterwards.
  • The backticks sit outside the base64 block, so alignment stays clean.
  • Newlines and spaces are stripped afterwards (avoiding css issues).
  • Assuming the file is in the current directory, in Vim you can simply type :vip!bash to insert in place the file contents encoded in base64.

A subtle but important detail

Base64 itself allows arbitrary whitespace and newlines.
CSS does not.

If you forget to strip whitespace, your font may silently fail to load.
This is one of those details that’s obvious after you hit it once.


Wiring Fonts to SVG Text via CSS

Fonts are referenced via CSS inside <defs>:

defs
  style.
    @font-face {
      font-family: 'WS-Regular';
      src: url('data:font/woff2;base64,#{WorkSans_regular_b64}') format('woff2');
    }

    @font-face {
      font-family: 'WS-SemiBold';
      src: url('data:font/woff2;base64,#{WorkSans_SemiBold_b64}') format('woff2');
    }

    text.regular {
      font-family: 'WS-Regular';
      dominant-baseline: middle;
    }

    text.semibold {
      font-family: 'WS-SemiBold';
      dominant-baseline: middle;
    }
Enter fullscreen mode Exit fullscreen mode

The .regular / .semibold classes don’t mean anything special by themselves —
they’re simply a clean way to bind specific font weights and behaviors to specific parts of the logo.


Rendering Text Safely in SVG

One small but important gotcha when generating SVG with Pug:

text.regular(
  x=lmargin
  y=height / 2
  font-size=height * 0.6
  fill=primaryColor
)='<'
Enter fullscreen mode Exit fullscreen mode

The < must be passed as a string.

Otherwise it’s interpreted as markup and breaks the SVG.
Once you do this explicitly, Pug behaves exactly as expected
by replacing them with their correspondent HTML entities.


Compact vs Full Variants (Same Source)

With logic in place, variants are trivial:

if compact
  text.semibold(...) 'S'
  text.semibold(...) '}'
else
  text.regular(...) '<'
  text.semibold(...) 'Smark'
  text.semibold(...) 'Form'
  text.semibold(...) '}'
Enter fullscreen mode Exit fullscreen mode

No duplication.
No parallel files.
One definition, many outcomes.


Generating Real SVG Files from the CLI

This is the payoff.

Once the template outputs only SVG, you can do this:

npx pug-cli \
  -O '{ compact: true, mode: "dark" }' \
  < smarkform_logo.pug \
  > smarkform_compact_dark.svg
Enter fullscreen mode Exit fullscreen mode

And that file is:

  • A valid SVG
  • Fully self-contained
  • Embeddable anywhere
  • Diff-friendly
  • Reproducible

You can script all variants in seconds.

SmarkForm logo variants


Integration into the SmarkForm project

Integrating the new logo into the SmarkForm codebase turned out to be refreshingly simple.

The existing assets already lived under /docs/assets, so I introduced a new logo directory for better organization, with a src subdirectory to hold the source files.

The Pug template that generates the logo variants now lives in /docs/assets/logo/src, alongside a small Bash script responsible for rendering the desired SVG outputs. The script is intentionally minimal and largely self-documenting, making it easy to adjust or extend if new variants are needed in the future.

Importantly, this script is not part of the build process. The generated SVG files are stable, self-contained assets and remain valid until the logo source itself changes. In practice, this means the script only needs to be executed when the Pug template is modified—keeping the build pipeline clean and avoiding unnecessary regeneration work.

You can find both the generator template and the accompanying script in the aforementioned /docs/assets/logo/src directory in the SmarkForm GitHub repository.


Why This Scales

This approach scales because:

  • Logos become data-driven
  • Branding lives inside the repository
  • Changes are auditable
  • Variants never drift out of sync

Just as importantly, it respects SmarkForm’s philosophy:

SmarkForm is markup-driven and styling-agnostic.

The branding system does not impose a visual identity on user forms.
Instead, it provides optional, lightweight visual cues — icons or small banners — so end users may recognize a form as being powered by SmarkForm and know what kind of experience to expect.

Developers and designers remain fully in control.


Final Thoughts

This started as “I just want a logo SVG”.

It ended as:

  • A parametric branding system
  • A CLI-driven asset pipeline
  • A single source of truth

If you already treat UI as code,
there’s no good reason your branding assets shouldn’t be treated the same way.

Once you do it like this, going back feels… unnecessary 😉

Top comments (0)