Skip to content
AstroCraft Docs
On this theme

Images & Assets

Olsa runs every photograph and render through astro:assets. Sources live in src/, get transformed and content-hashed into the build, and expose ImageMetadata so <Image> can emit real intrinsic dimensions — which is what keeps cumulative layout shift at zero.

Three kinds of asset live in three places, and the split is about how each is used rather than what format it is.

Where things live

src/assets/ — anything processed at build time.

src/assets/
├── images/
│   ├── about-story.jpg
│   ├── team-*.jpg              # eight team portraits
│   ├── testimonial-*.jpg       # four testimonial portraits
│   ├── blog/*.jpg              # post hero images
│   ├── features/*.jpg          # the three feature-row renders
│   └── integrations/dashboard.jpg
├── logos/<id>.svg              # integration brand glyphs, inlined raw
├── cta-grid.svg                # the GrainyPanel floor vector
└── logo-wordmark.svg           # the brand wordmark, inlined raw

public/ — three files served verbatim, referenced by absolute path:

public/
├── favicon.svg
├── favicon.ico
└── og.jpg          # the default social image — a placeholder

Content-relative paths — a collection entry’s image is a path relative to its markdown file, and Zod’s image() helper resolves it:

heroImage: ../../assets/images/blog/series-a.jpg

The rule is simple. If it should be optimized, hashed and given intrinsic dimensions, it goes in src/assets/. If it must keep an exact, predictable URL — favicons, the OG fallback, a robots.txt you were serving statically — it goes in public/.

The <Image> convention

Every call site follows the same shape:

---
import { Image } from "astro:assets";
import storyImage from "@images/about-story.jpg";
---

<Image
  src={storyImage}
  alt="Sunlit studio workspace with a monitor showing an automation dashboard"
  width={1120}
  height={960}
  loading="lazy"
  class="aspect-[7/6] w-full rounded-[32px] object-cover"
/>

Four conventions in that:

Explicit width and height at roughly two times the largest rendered size, which covers high-density displays without shipping the original. Astro emits the intrinsic dimensions on the element regardless, so the aspect ratio is reserved before the bytes arrive.

loading="lazy" below the fold. Above-the-fold images — the home hero, a blog post’s hero — leave it off so they load eagerly.

alt is written or deliberately empty. A decorative image inside a card whose title already says the same thing gets alt=""; a content image gets a real description. BlogCard’s thumbnail is alt="" because the card’s heading is right beside it; the About story photo gets a full description because it is content.

Sizing lives in class, not in the width and height. The attributes are the source dimensions; the class is the rendered box.

Content collection images

Both the blog and integrations schemas use Zod’s image() helper, which means the path is validated at build time and the result is ImageMetadata with real dimensions rather than a string:

heroImage: image(),        // required on blog
image: image(),            // required on integrations
avatar: image().optional() // optional on authors

A path that does not resolve is a build error naming the entry. This is why heroImage being required matters: every post needs it twice — as the card thumbnail and as the page’s og:image — and passing ImageMetadata to BaseLayout’s image prop is what lets BaseHead emit accurate og:image:width and og:image:height instead of falling back to the 1200×630 convention. See SEO & Structured Data.

The raw-SVG cases

Three assets are deliberately not run through astro:assets, because they are markup rather than pictures.

The brand wordmark is imported with ?raw and inlined, so it inherits currentColor and follows the theme.

Integration logos are globbed eagerly by @js/integrationUtils and inlined the same way, keyed by collection entry id:

const logos = import.meta.glob<string>("../assets/logos/*.svg", {
  query: "?raw",
  import: "default",
  eager: true,
});

A missing glyph throws with the fix in the message — No logo for integration "linear" — add src/assets/logos/linear.svg. The eager glob is a deliberate choice for a nine-entry set; a catalog of hundreds would want a lazy import.

Icons are a registry of inner SVG markup inlined at build time — a system of its own, covered in Icons.

The general rule: an SVG that should inherit colour, respond to the theme or be styled by CSS gets inlined. An SVG that is a picture goes through astro:assets like any other image.

How optimization works

sharp is a dev dependency and does the work at build time. Nothing in dist/ depends on an image service, which is what keeps the output deployable to a plain static host.

pnpm-workspace.yaml denies sharp’s post-install build script. That is intentional and not a problem — sharp ships prebuilt binaries — and it keeps installs free of native toolchain requirements on a CI runner. A notice about ignored build scripts during install is that setting working.

If you later move to a host with its own image CDN and swap in the matching adapter, that path changes and your markup does not.

What to replace before launch

public/og.jpg is a placeholder. It is the fallback og:image for every page without its own, and BaseHead currently also uses it as the Organization logo in JSON-LD. A real 1200×630 image fixes both; give the Organization a proper logo separately when you have one.

public/favicon.svg and favicon.ico are Astro’s defaults.

Every photograph in src/assets/images/ is sample content. The eight team portraits, four testimonial portraits, three feature renders, seven blog heroes and the About story photo all ship with the theme and all look finished, which is exactly why they are easy to forget.

src/assets/images/integrations/dashboard.jpg is shared by all nine integration entries. Give each integration its own screenshot, or the detail pages will all show the same picture — and so will their social cards.

src/assets/logos/*.svg are the nine sample brand glyphs. Replace them alongside the entries.

Adding images

Drop the file in src/assets/images/ (or a subfolder), import it through the @images/* alias, and render it with <Image> following the conventions above. For a collection entry, put it near the others and reference it relatively from the frontmatter.

Two things to keep in mind. Source your JPEGs at about twice the rendered size — Astro can downscale but not invent detail. And write the alt text as you add the image, not afterwards; the ESLint jsx-a11y rules will catch a missing alt attribute but not a lazy one.

NEXT STEPComponents Reference