Skip to content
HomePagesHomePages template kit

Static assets

Authoring static assets (photographs, svg, fonts) at the template level or in a section folder, the derived vs passthrough import lanes, and how assets differ from content images.

A static asset is a file that is part of the template itself — a logo, a background texture, an icon, a brand webfont. It is authored once, is the same for every deliverable rendered from the template, and is referenced either from your CSS or from a component.

An asset lives in one of two places:

  • Template level — e.g. templates/<template>/assets/ — shared across every section in the template. Reference it from a section’s CSS with a ../../assets/… path (below).
  • Section folder — e.g. sections/<section>/assets/ — local to that one section. A section-folder asset must be referenced: imported in Renderer.tsx or url()-referenced from one of the section’s CSS files. An unreferenced binary in a section folder is flagged by bundle-binary-asset — it has no build meaning and can never be served.

Keep a logo, texture, or font that several sections share in the template-level folder; keep one that only a single section uses alongside that section.

.png .jpg .jpeg .webp .avif photographs — derived (see below)
.svg .gif .ico vector / animated images
.woff .woff2 .ttf .otf .eot fonts
.mp4 .webm .mov .mp3 .wav audio / video
.pdf .zip documents

Referencing an asset — from CSS with url()

Section titled “Referencing an asset — from CSS with url()”

Reference a template-level asset from a section’s styles.css with url() — a @font-face src or a background-image resolves to the asset’s built URL, identical in the template-kit dev preview, in the editor, and on the published page. Hand-CSS opens with a css-reason: comment (see css-reason):

/* css-reason: brand webfont + crest watermark, both template assets */
@font-face {
font-family: "Brand Serif";
src: url("../../assets/fonts/brand.woff2") format("woff2");
font-display: swap;
}
.hero-crest {
background-image: url("../../assets/crest.svg");
}

The ../../assets/… path climbs out of the section folder to the template-level assets/ directory. A font you declare here is available to the whole section. If the family you want is one the template’s theme already loads, reference that instead — see TokenTheme; a font the page already has costs nothing to reuse. A font the whole template wants belongs in a theme fonts entry in the first place, where its src resolves against theme.ts — see Loading a web font.

A section-folder asset is referenced the same way, with a section-relative path instead — no climbing out of the folder:

/* css-reason: hero watermark, local to this section */
.hero-crest {
background-image: url("./assets/crest.svg");
}

Referencing an asset — from a component with import

Section titled “Referencing an asset — from a component with import”

An asset you render as an element is imported in the component. What the import gives you depends on the file, following the same split Next.js draws:

Import You get Render with
.png .jpg .jpeg .webp .avif an AuthorImage object <Image value={…} alt="…">
.svg .gif .ico, fonts, video, documents the built URL string <img src={…}>, url(), …

The kit ships an ambient TypeScript declaration for both lanes, so either import typechecks with no per-workspace setup. (Hovering a raster import shows the shim’s own TemplateAuthorImage; it is the same shape as the exported AuthorImage, which is the name to write in your own annotations.)

A raster image you import is derived at build time into a full responsive ladder — several widths in several modern formats, the same treatment the platform gives a property’s own uploaded photo. The import gives you an AuthorImage describing that ladder, which you hand straight to the Image primitive:

import { Image } from "@homepages/template-kit";
import rooftop from "./assets/rooftop.jpg";
export default function Renderer() {
return (
<div className="aspect-[3/2]">
<Image value={rooftop} alt="Rooftop deck at sunset" frame={false} fit="cover" />
</div>
);
}

This is the bare Image primitive, not Slot.Image — a template asset has no slot to bind to. frame={false} renders a single <img class="w-full"> and lets the parent establish the box; pass a FrameConfig instead if you want the primitive to own the aspect box.

alt is required here, and the compiler enforces it: an AuthorImage has no alt of its own to fall back on. Pass alt="" for a purely decorative image — a deliberate empty string, not an omission.

Commit one honest master per image, at full quality. The master itself is never served, so its file size costs you nothing at the edge; every width a visitor actually downloads is derived from it. Do not hand-export a set of sizes — that is the build’s job.

Where a build has no image toolchain available (a bare local preview), the import still gives you an AuthorImage, just with no ladder in it, and <Image> renders a single <img>. Your code is identical either way — the preview is simply unoptimized.

A logo, icon, or font — import + ordinary markup

Section titled “A logo, icon, or font — import + ordinary markup”

Vector and animated files are passed through untouched: rasterizing line art would be a regression, and a ladder cannot express an animation. These resolve to the asset’s built /section-assets/… URL — the same URL in the dev preview, the editor, and the published page:

import crest from "./assets/crest.svg";
export default function Renderer() {
return <img src={crest} alt="" className="h-8 w-auto" />;
}

A plain <img> is right here: there is one file, at one size, with no ladder to choose from.

  • CSS decoration (backgrounds, @font-face, masks) → url().
  • A photograph rendered as an element → import + <Image value={…} alt="…">.
  • A logo, icon, or font rendered as an element → import + <img> / url().
  • A tiny glyph or icon → inline <svg> markup — no file at all.

These are two different things; keep them apart.

  • A static asset is authored template decoration — a logo, a texture, a brand font. It ships inside the template, is the same on every deliverable, and you reference it from your CSS with url() or by importing it into a component (above).
  • A content image is the property’s own content — listing photos, floor plans, a headshot. It differs on every deliverable and flows through the section’s slots as an ImageValue, never as a file in your template. Declare an image slot in your schema and render it with Slot.Image; the platform supplies the actual photo per deliverable.

The same line runs through the non-image files. A .pdf above is a template document — a spec sheet the agency ships with the design, identical on every deliverable. A property’s own brochure, disclosure pack or fee schedule is content: it is uploaded per listing, so it belongs in a document slot and renders through Slot.Document, never as a file committed into a section folder.

Both end up in the same responsive-image machinery — the distinction is where the bytes come from, not how well they are served. A photograph you ship with the template gets the same width-and-format ladder a user’s upload does.

Rule of thumb: if the picture is the same for every home the template ever renders, it’s an asset — place it at the template level or in the section folder and reference it from CSS or a component import. If it changes with the property, it’s content — give it a slot. Never hard-code a listing photo as an asset.

Deriving a ladder is expensive — a single full-size photograph is two dozen separate encodes — so the results are cached inside your workspace, under .tk-dev-cache/media/. A second template-kit check over unchanged photographs re-uses them instead of encoding again; editing one photograph re-derives that photograph only.

Entries are addressed by the contents of the master, so a stale one cannot exist: change the file and you get a different entry. The directory is safe to delete at any time — the only cost is deriving again. It is already ignored by the workspace’s .gitignore, and never commit it.

It bounds itself to 512 MB, discarding whichever images you used least recently. The preview server (template-kit dev) shares the same cache, so a restart does not re-encode either.

Nothing refuses a section for the assets it carries — a published site ships only the assets its rendered pages reference. But an asset over 150 KB is reported by name, since past that it is usually an accident: see size-assets. Subset a webfont to the weights and characters you use, and drop anything a section doesn’t actually reference.

Photographs are the exception: do not pre-shrink a raster you import, because the build derives every served width from your master and the master itself is never downloaded by a visitor. Shrinking it only lowers the ceiling on what a large screen can be given. Everything else ships as the bytes you committed, so those are worth trimming.

template-kit check reports which of a template’s photographs derived a ladder and at what cost, and which merely passed through unoptimized (no image toolchain in this install) — run it if a preview looks soft or the asset budget runs over.

  • Slot primitivesImage and the rest, including the frame options a template asset can be given.
  • TokenTheme — the fonts and tokens a template already loads, before you ship another file.
  • bundle-binary-asset — why an unreferenced binary in a section folder is reported.
  • size-assets — the per-file guideline a heavy asset is reported against, and why nothing is refused.
  • Ship a static asset — the same ground as a task walkthrough.