Adding Your Own CSS and JS

There are eight places a Leed site takes your own CSS, JavaScript or <head> markup, and every one of them is a file in your repository. There is no code field in the CMS, no per-page CSS box and no <body> hook. This page is the catalog.

PointPath in your repoHow it activatesCache-busted?Tier
Head hooksrc/_includes/header-includes.hbsThe file existingn/a — inline in the pageFree
Tailwind CSStailwind/**/*.css, imported from tailwind/site.config.csssite.config.css existingYes — content hash on the stylesheetFree
Hand-written CSSsrc/static/css/*.cssYou link it from a layoutNoFree
Hand-written JSsrc/static/js/*.jsYou link it from a layoutNoFree
Extra highlight.js languagesrc/static/js/hljs-<lang>.jsNaming the language in the CMSYes — content hashEnterprise to add
Mermaid palettesrc/static/js/mermaid.theme.jsonThe file existingn/a — read at build timeFree
Layouts and partialssrc/_layouts/*.hbs, src/_includes/*.hbsReferencing them from a page or layoutn/aFree
A Leed template you took overleed site eject destinations under src/_includes/The file existingn/aVaries — see below

src/_includes/header-includes.hbs — your tags in every page head

This is the supported hook for your own <link>, <style>, <meta> or <script>. It is rendered as the very first thing inside Leed’s head partial, before Leed’s own metadata, on every layout that includes it.

It is activated by a pure file-existence check. There is no CMS flag to set, no tier gate and nothing to enable — unlike every other custom template, which is gated on a useCustomTemplate flag. Creating the file is what turns it on; deleting it turns it off.



<link rel="preconnect" href="https://analytics.example.com" crossorigin>
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32.png">
<link rel="manifest" href="/static/site.webmanifest">

<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#171717">

Keep it to things that must be true on every page of the site, documentation included. That is the whole reason to put something here rather than in site-template.hbs: the documentation set uses the charlie layout, which site-template.hbs does not wrap, so anything declared there reaches your marketing pages alone. Page-specific tags belong in the page’s own layout.

The full head order, and how this partial sits inside it, are on customizing the <head>. The two pages describe the same file from opposite ends: this one says it is an injection point, that one says what else is in the head around it.

CSS through the Tailwind build

The primary route, and the one to prefer. Put your stylesheets under tailwind/, import them from tailwind/site.config.css, and the build compiles them with Tailwind, tree-shakes the utilities you did not use, and writes one hashed file that every page links.

/* tailwind/site.config.css */
@import "tailwindcss" source("../src");

@import "./site/theme.css" layer(base);
@import "./docs/color-theme-mybrand.css";
@import "./docs/mermaid.css";

The compile, the four lines Leed injects into that file, and the cache-busting hash are all on the Tailwind build. Which of those imports carries a layer(...) and which deliberately does not is a cascade decision, covered on cascade layers and overriding Leed.

Hand-written static CSS and JS

Files under src/static/css/ and src/static/js/ are copied to /static/ untouched. Link them yourself from a layout:

<link rel="stylesheet" href="/static/css/print.css">
<script src="/static/js/widget.js" defer></script>

This is permitted, and the trade-off is the whole reason to prefer the Tailwind route.

The full caching rules, including which paths are exempt, are on static files and caching.

Extra highlight.js languages

Leed’s code blocks ship with a large set of languages already highlighted. Adding one that is not in that set takes two steps, one in the CMS and one in your repository.

  1. Name the language in the CMS, under Settings → General. It is stored on the company record, not on the documentation configuration.
  2. Put the file in your repository at src/static/js/hljs-<lang>.js, using exactly the name you entered.

At build time, Leed looks for that file, hashes its contents, and appends /static/js/hljs-<lang>.js?v=<hash> to the scripts that load with the highlighter. A missing file is a warning, not an error — the build carries on and logs:

Extra language nix not found: /path/to/repo/src/static/js/hljs-nix.js

which is exactly what you will see if the name in the CMS and the filename in the repository have drifted apart.

The Leed CMS Settings → General screen showing the extra highlight.js languages field with two language names entered as chips

Which languages your code blocks can already highlight without any of this is on code blocks; the CMS side of the setting sits beside the other appearance fields documented in themes, fonts and code themes. If the CMS returns a 402 when you add one, the upgrade contract is on when a feature is gated.

The Mermaid palette

src/static/js/mermaid.theme.json sits in the same directory as the highlighter’s language files, for the same reason: it is a build-time input that belongs beside the CSS it has to agree with, rather than a publish cycle away in a settings form.

Spell that directory carefully — static/js/, not static/javascript/. Both exist in a Leed site, and a palette in the wrong one is indistinguishable from a site that never wanted one: no warning, no error, diagrams in Mermaid’s own colors. The file’s shape and every rule about what may be a var() are on theming diagrams.

Layouts and partials

Anything in src/_layouts/ and src/_includes/ is yours. That is full control of the markup, and therefore full control of the classes your CSS has to match — which is usually a better lever than a more specific selector.

Two files in that directory carry a specific meaning worth knowing here: docs-header.hbs and docs-footer.hbs, which replace the documentation chrome. Like header-includes.hbs, both are detected by a plain file-existence probe. Unlike it, both are gated:

How templates and partials fit together is on how templates work. The CMS-side configuration that fills the documentation chrome — the menus, the header button, the light and dark logos — belongs to the documentation set rather than to this page.

Taking over a Leed template

leed site eject <name> copies a Leed-owned template into your repository, where the file’s existence is what makes the build use yours instead. Five templates are ejectable:

NameWhat it controlsRequires
stub-templateRecommendation card markup, cloned per recommendation in the browserGrowth
unsubscribeThe email unsubscribe page—
unsubscribedThe post-unsubscribe confirmation page—
docs-headerThe documentation header, inside Leed’s positioned wrapperStarter
docs-footerThe documentation footerStarter

leed site eject with no argument lists them, marking each customized or Leed default, and it will not hand you a template your plan cannot render — a file the renderer ignores is worse than no file.

One asymmetry is worth carrying, because it changes how you treat the file you get. The footer source is what renders, so ejecting gives you a byte-identical starting point. The header source is a complete, working example that demonstrates the real API rather than a snapshot of the current output, because Leed’s own header wrapper fills itself with a per-layout variant. Diff the header eject against your rendered page before assuming they match.

The registry and what each template owns are on overriding Leed templates; the command itself, with its flags and its output, is leed site eject.

What is not an injection point

The honest list, because these are the things people try.

What people look forDoes it exist?What to use instead
A CSS field in Page SettingsNoA class on the element, styled from tailwind/
Per-page custom CSSNoA page-type-specific layout, or a class the page body sets
<style> or <script> in a page bodyNo — bodies are Leed Markdown, and raw HTML other than <br> is rejectedheader-includes.hbs, or a layout
A code editor in the CMSNo — template files are not editable in the CMSClone the repository and edit locally
A <body> injection hookNo — only the head hook existsheader-includes.hbs with a deferred script
A per-page <head> fieldNoThe page’s own layout

Two of the stylesheet destinations in the table above are worth naming as concrete examples rather than as paths, because both are real files in leed.ai’s own repository and both are imported deliberately differently: tailwind/partials/alerts.css is imported into layer(components), for the reasons on theming alerts, and tailwind/docs/mermaid.css is imported unlayered, for the opposite reason on theming diagrams. This page says where things go; those pages say why.

ESC