← Back to site

Image System

All images on the site are local files in src/assets/, processed by Astro’s image optimization pipeline at build time. The image system consists of three utility modules that work together.

Architecture

imageConstants.ts    →  Supported formats, validation regex
imageGlobs.ts        →  Centralized glob patterns per directory
imageLoader.ts       →  Loading functions with fallback + dev warnings

Image Constants

File: src/utils/imageConstants.ts

ExportValuePurpose
VALID_IMAGE_EXTENSIONS['jpg', 'jpeg', 'png', 'webp', 'svg']Source of truth for supported formats
IMAGE_GLOB_PATTERN*.{jpg,jpeg,png,webp,svg}For use in glob patterns
IMAGE_EXTENSION_REGEX/\.(jpg|jpeg|png|webp|svg)$/iSchema validation
VALID_EXTENSIONS_MESSAGE".jpg, .jpeg, .png, .webp, or .svg"Error messages

Glob Patterns

File: src/utils/imageGlobs.ts

Each asset directory has a centralized glob pattern exported as a constant:

ExportDirectoryUsed by
eventImages/src/assets/events/Events listing, detail pages
projectImages/src/assets/projects/Projects listing, detail pages
sponsorLogos/src/assets/sponsors/**/Sponsor showcase
whatIsBearsImages/src/assets/whatIsBears/**/Landing “What is BEARS” carousel (content-driven via landing/what-is-bears page-text singleton)
ourMissionImages/src/assets/about-us/our-mission/About page hero
peopleImages/src/assets/people/Faces of BEARS grid, Meet the Team portraits, Testimonials carousel, People category on /media (resolved via loadCollectionImages)
allAssetImages/src/assets/**/Img.astro’s string-src resolver; also consulted by the Media-page body-image extractor in contentQueries.ts so any path Keystatic writes into an MDX <Img src="…"> can resolve regardless of which asset subdir it points at
aboutHeroImages/src/assets/hero/about-us/About page hero
eventsHeroImages/src/assets/hero/events/Events page hero
projectsHeroImages/src/assets/hero/projects/Projects page hero
mediaHeroImages/src/assets/hero/media/Media page hero
sponsorsHeroImages/src/assets/hero/sponsors/Sponsors page hero
contactHeroImages/src/assets/hero/contact/Contact page hero
headerLogoImages/src/assets/header/Site header logo (filename chosen via branding singleton)
footerLogoImages/src/assets/footer/Site footer logo (filename chosen via branding singleton)
heroLogoImages/src/assets/hero/landingpage/logo/Landing hero logo (filename chosen via branding singleton)
defaultImages/src/assets/default-images/Default fallback covers (filenames chosen via branding singleton)
heroImages/src/assets/hero/landingpage/Landing hero images only
heroMedia/src/assets/hero/landingpage/Landing hero images + videos

Logo images

The header, footer, and landing hero logos are content-driven through the branding Keystatic singleton. Each field in that singleton names a specific file inside its asset directory; the component reads the filename from the entry and resolves it through the matching glob. Editors can upload a new logo from the admin UI without touching code.

Field on branding singletonAsset directoryUsed byFallback
headerLogosrc/assets/header/Header logo (all pages)Plain “BEARS” text
footerLogosrc/assets/footer/Footer logo (all pages)Plain “BEARS” text
heroLogosrc/assets/hero/landingpage/logo/Landing page hero logoPlain “BEARS” heading

Vite limitation: Glob pattern strings must be static literals. They cannot be constructed dynamically from variables. This is why each pattern is hardcoded rather than generated from IMAGE_GLOB_PATTERN.

Loader Functions

File: src/utils/imageLoader.ts

loadImage(options)

Loads a single image from a glob with optional fallback.

const image = await loadImage({
  glob: eventImages,
  imagePath: "/src/assets/events/event-1.jpg",
  fallbackImage: await getDefaultEventImage(),
  context: { itemTitle: "My Event", itemSlug: "my-event" }
});

Returns ImageMetadata | null.

loadCollectionImages(collection, type)

The primary API for loading images for a content collection. Uses type-specific configuration (glob, base directory, fallback image) and returns items with a loadedImage property attached.

const eventsWithImages = await loadCollectionImages(sortedEvents, 'event');
const projectsWithImages = await loadCollectionImages(sortedProjects, 'project');
const peopleWithImages = await loadCollectionImages(sortedPeople, 'person');

Supported types: 'event', 'project', 'person'. The 'person' loader backs both the Faces of BEARS grid and the Testimonials carousel — same portrait folder, same defaults.

TypeScript overloads guarantee that when a fallback is configured (which it always is for the built-in types), loadedImage is non-null.

loadCoverImage(fileName, type, context?)

Loads a single cover image for a detail page. Used in [slug].astro routes.

const coverImage = await loadCoverImage(
  entry.data.coverImage, 'event',
  { itemTitle: entry.data.title, itemSlug: entry.slug }
);

Always returns a non-null ImageMetadata (falls back to default).

loadAllImagesFromDirectory(glob)

Loads all images from a glob. Useful as a fallback when no specific filename is configured (e.g. picking the first hero file off disk if the page-text singleton’s image field is blank).

const fallbackHeroes = await loadAllImagesFromDirectory(mediaHeroImages);

Returns ImageMetadata[], filtering out failed loads.

Note: the “What is BEARS” carousel used to rely on this helper. It is now content-driven — see WhatIsBears.astro, which reads the image list (in order) from the landing/what-is-bears page-text singleton and resolves each filename through whatIsBearsImages via loadImage.

Default / Fallback Images

Default images live in src/assets/default-images/. The filename used for each slot is chosen in the defaultImages Keystatic singleton (labelled “Default images”, under Site-wide), so editors can swap placeholders without code changes. Consumers pull them through async getters that read the default-images entry and resolve each filename through the defaultImages glob:

GetterField on defaultImages singletonSeed fileUsed for
getDefaultEventImage()defaultEventImagedefault-event.jpgEvents with no custom cover
getDefaultProjectImage()defaultProjectImagedefault-project.jpgProjects with no custom cover
getDefaultSponsorImage()defaultSponsorImagedefault-sponsor.jpgSponsors with missing logo
getDefaultFaceImage()defaultFaceImagedefault-face.jpgPeople with missing portrait (Faces of BEARS + Testimonials)

Each getter returns an ImageMetadata.

Favicon & social-share image

The browser-tab favicon and the default Open Graph image live in /public/ and are editable on the “Branding / logos” singleton (same Site-wide group). Each field stores a bare filename; BaseLayout.astro and DocsLayout.astro prepend / to build the URL. Dropping in a new favicon replaces public/<filename> and the <link rel="icon"> tag picks it up on the next build — note that browsers cache favicons aggressively, so a hard-reload may be needed to see the new icon.

coverImageType Logic

Events and projects have a coverImageType field (derived in the schema transform):

  • DEFAULT — No custom image provided. Uses the fallback image. Logs 🔹 in dev console.
  • CUSTOM — Custom image specified. Attempts to load it. If it fails, logs ⚠️ and falls back.

Adding Images for a New Collection

  1. Create an asset directory: src/assets/<name>/
  2. Add a default fallback image: src/assets/default-images/default-<name>.jpg
  3. Add a glob export in src/utils/imageGlobs.ts:
    export const myImages: ImageGlob = import.meta.glob<{ default: ImageMetadata }>(
      "/src/assets/<name>/*.{jpg,jpeg,png,webp,svg}"
    );
  4. Import the default image and add a type config to loadCollectionImages() in src/utils/imageLoader.ts
  5. Add an overload signature for the new type