Guides / Tutorials

A dog breed gallery in SvelteKit

By the Dog Image API team. Updated .

A SvelteKit gallery for one breed is a dynamic route with a +page.server.js load function and a +page.svelte that loops over the nine views. The load function calls GET /api/v1/breeds/{breed}, which needs no key, and hands the page the breed name, summary and view list.

This guide uses SvelteKit 2 with Svelte 5. Create a project if you need one:

npx sv create dog-gallery
cd dog-gallery
npm install

Choose the minimal template.

The load function

Create src/routes/dogs/[breed]/+page.server.js. It calls the API, turns a missing breed into a 404, and also fetches the catalog so the page knows the label of each view:

// src/routes/dogs/[breed]/+page.server.js
import { error } from "@sveltejs/kit";

const API = "https://dogimage.dev";

export async function load({ params, fetch, setHeaders }) {
  const [breedRes, catalogRes] = await Promise.all([
    fetch(`${API}/api/v1/breeds/${params.breed}`),
    fetch(`${API}/api/v1/breeds`),
  ]);

  if (breedRes.status === 404) error(404, "Breed not found");
  if (!breedRes.ok || !catalogRes.ok) error(502, "The image catalog is not reachable");

  const breed = await breedRes.json();
  const catalog = await catalogRes.json();

  setHeaders({ "cache-control": "public, max-age=3600" });

  return {
    slug: breed.slug,
    name: breed.name,
    summary: breed.summary,
    views: catalog.views,
  };
}

In SvelteKit 2, error() throws on its own, so you do not write throw. The fetch passed into load is SvelteKit's version, which is safe on the server and in the browser.

The page returns only the pieces it needs. The nine preview URLs are predictable, so the page builds them from the slug and a view id.

The gallery page

Create src/routes/dogs/[breed]/+page.svelte:

<script>
  let { data } = $props();

  const API = "https://dogimage.dev";
  const src = (view, size) => `${API}/api/v1/images/${data.slug}/default/${view}?size=${size}`;

  let activeId = $state(null);
  const active = $derived(data.views.find((v) => v.id === activeId));

  function onKey(event) {
    if (event.key === "Escape") activeId = null;
  }
</script>

<svelte:window onkeydown={onKey} />

<svelte:head>
  <title>{data.name} from nine angles</title>
</svelte:head>

<h1>{data.name}</h1>
{#if data.summary?.text}
  <p>{data.summary.text}</p>
{/if}

<ul class="grid">
  {#each data.views as view (view.id)}
    <li>
      <button type="button" onclick={() => (activeId = view.id)}>
        <img
          src={src(view.id, 512)}
          alt="{data.name}, {view.label}, AI-generated illustration"
          width="256"
          height="256"
          loading="lazy"
        />
        <span>{view.label}</span>
      </button>
    </li>
  {/each}
</ul>

{#if active}
  <div class="overlay" role="dialog" aria-modal="true" aria-label={active.label}>
    <button type="button" class="close" onclick={() => (activeId = null)}>Close</button>
    <img src={src(active.id, 1024)} alt="{data.name}, {active.label}" width="1024" height="1024" />
  </div>
{/if}

<p class="credit">
  <a href="{API}/dogs/{data.slug}">{data.name} image: Dog Image API</a>.
  {#if data.summary?.url}
    Summary from <a href={data.summary.url}>Wikipedia</a>.
  {/if}
</p>

<style>
  .grid { list-style: none; margin: 1rem 0; padding: 0; display: grid;
          grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; }
  .grid button { all: unset; cursor: pointer; display: grid; gap: 4px; }
  .grid img { width: 100%; height: auto; background: #f4f1ea; border-radius: 8px; }
  .overlay { position: fixed; inset: 0; display: grid; place-items: center;
             background: rgb(20 20 20 / 0.85); }
  .overlay img { max-width: min(90vmin, 1024px); height: auto; background: #f4f1ea; border-radius: 12px; }
  .close { position: absolute; top: 1rem; right: 1rem; }
</style>

Start the dev server with npm run dev and open /dogs/beagle. You get nine tiles, and a click opens the 1024 pixel preview of that view.

How the pieces fit

Runes. $props() receives the load result, $state holds the open view and $derived finds its label. On Svelte 4 you would write export let data and a reactive $: statement instead, and on:click instead of onclick.

Eager small, lazy large. The grid loads 512 pixel files with loading="lazy", and only the clicked view loads at 1024. Both sizes are WebP files with transparent backgrounds. The credit link sits on the same page as the images, which is what the free preview terms ask for.

Unknown slugs. Visit /dogs/not-a-breed and the API answers with a 404 and a list of valid choices. The load function turns that into SvelteKit's error page. If you would rather prerender, export a prerender constant and a list of entries from the catalog, but then you must keep the data current by rebuilding.

Color variants

French Bulldog, Golden Retriever, German Shepherd, Dachshund, Doberman, Poodle and Great Dane come in several colors. The breed response lists each one under variants, with its own views. To let the visitor pick a color, return breed.variants from the load function and use the chosen variant slug in the URL instead of default. default always points to the breed's main set.

Where the full-size files fit

Do not use a key in +page.svelte or any file under src/lib that the browser can import. If you want 2048 pixel PNGs, a server-only module such as $lib/server/ or a script run at build time can fetch them with Authorization: Bearer and the key in DOG_IMAGE_API_KEY. SvelteKit blocks client code from importing anything inside $lib/server, which gives you a useful guard. The Next.js guide has a complete download script that works the same way here.

Pricing for full-size plans is on the pricing page. Compare angles across every breed in the front three-quarter gallery.

Questions

Why use a load function instead of fetching in the component?

A load function runs before the page renders, so the gallery arrives with its data and works with server rendering. It also lets SvelteKit return a proper 404 for an unknown breed.

Which views does a breed gallery show?

Nine: front, front-left, left, rear-left, rear, rear-right, right, front-right and head. They come from the same dog, on the same transparent canvas.

Do I need a key for the gallery?

No. The breed endpoint and the preview images are keyless. A key is only needed for full-size PNG files, which you should fetch from a server.

Next steps