Skip to content
AstroCraft Docs
On this theme

Images

Images in Indexa go through Astro’s asset pipeline. An image imported from src/ — or named in a collection’s frontmatter — is optimized, hashed and dimension-typed at build time. Twenty-four source photographs become 150 WebP variants in dist/_astro/, and nothing in the source tree is served as-is.

The two directories

src/assets/images/ holds every photograph the site renders: the hero background, the four body-type tiles, the car photos, the blog heroes, the page mastheads. They are imported, so Astro optimizes them, gives them content-hashed filenames and types their intrinsic dimensions — which is what lets <Image> emit width and height and prevent layout shift without you measuring anything.

public/ holds three files and should hold almost nothing: favicon.svg, favicon.ico and og.jpg. Files in public/ are copied byte-for-byte to the root with no optimization, no hashing and no dimension typing. That is exactly right for these three — a favicon needs a fixed path, and an OG image is fetched by a crawler that will not parse your HTML for a hashed URL — and exactly wrong for a photograph.

The rule to carry: if markup or frontmatter references it, put it in src/assets/. If something outside your HTML fetches it by a fixed path, put it in public/.

The car photo pool

Sixty-nine records share thirteen photographs through src/config/carImages.ts, keyed by CarImageKey. A record names a key; the pool maps it to an imported ImageMetadata.

The split between the pool and the data file is load-bearing. carsData.json.ts holds no asset imports, so it can be loaded by plain node --experimental-strip-types — which is how four self-checks assert against the real dataset without running a build. Add an import photo from "@assets/…" to the data file and those checks stop working immediately, with a module-resolution error rather than a helpful one. The Car Records covers the dataset.

It is a placeholder pool awaiting per-listing photography. Five of the keys are stock shots for the new and electric hubs, resized to 1600px wide, and the source says so.

Rendering one

---
import { Image } from "astro:assets";
import { carImages } from "@config/carImages";
---

<Image
  src={carImages[car.image]}
  alt={car.imageAlt}
  widths={[200, 400, 600]}
  sizes="200px"
  loading={priority ? "eager" : "lazy"}
  class="h-[134px] w-[200px] rounded-lg object-cover"
/>

Three of those props are the pattern worth copying. widths plus sizes generates a srcset matched to the slot the design gives the image rather than to arbitrary breakpoints — a 200px card thumbnail has no business downloading a 1600px file. loading is threaded from a priority prop the parent passes, so the first row of a grid loads eagerly and the rest lazily; CarRecordCard takes that as an explicit prop rather than guessing from an index. And alt comes from the record, not from the component, because only the data knows what the photograph shows.

Blog images

A post’s heroImage is image() in the collection schema, which means it is required and validated: a post that names a missing file fails the build with the entry named. That is deliberate — every post needs an image for its card and its OG tag, so the schema refuses a shareless post rather than letting one ship. heroCaption is the optional caption rendered under the feature image.

Frontmatter paths are relative to the markdown file: ../../../assets/images/blog-price-index.png from src/data/blog/<slug>/index.md. Content Collections has the whole schema.

The social image

public/og.jpg is the default for every page that does not pass its own image, and it is currently a placeholder. Replace it with a real 1200×630 file, because BaseHead hard-codes those dimensions in og:image:width/height for the default case — a differently sized replacement would advertise the wrong dimensions to every crawler.

It also stands in for the JSON-LD Organization logo, marked with a ponytail: note. A brand logo and a social card are not the same image; swap that one for a real mark before launch. SEO has the graph.

Adding a format

sharp is already a dependency, so AVIF is available: pass format="avif" to an <Image>, or use <Picture> with formats={["avif", "webp"]} for a source set. The build currently emits WebP because it is the one format with universal support and a good enough ratio, and because one format means one variant per width rather than two.

NEXT STEPComponents Reference