Guides / Tutorials

Dog breed pages in Astro

By the Dog Image API team. Updated .

Astro builds one static page per breed when you fetch the Dog Image API catalog inside getStaticPaths. Each page then prints the nine views from the same breed in a grid, with the credit link below. There is no client JavaScript and no key.

This guide uses Astro 5 with TypeScript in the frontmatter, and the result is a folder of plain HTML files you can host anywhere.

Create the project

npm create astro@latest dog-pages -- --template minimal
cd dog-pages

Accept the defaults. You will add three files: a small catalog helper, an index page and a breed page.

Fetch the catalog once

The catalog lists every breed and the nine view ids. Both pages need it, so put the request in a helper that remembers its result. Create src/lib/catalog.ts:

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

export interface View {
  id: string;
  label: string;
}

export interface Breed {
  slug: string;
  name: string;
  crossbreed: boolean;
  default_variant: string;
}

export interface Catalog {
  views: View[];
  breeds: Breed[];
}

let cached: Promise<Catalog> | undefined;

export function getCatalog(): Promise<Catalog> {
  cached ??= fetch(`${API}/api/v1/breeds`).then((res) => {
    if (!res.ok) {
      throw new Error(`Catalog request failed with status ${res.status}`);
    }
    return res.json() as Promise<Catalog>;
  });
  return cached;
}

export function previewUrl(slug: string, view: string, size = 512): string {
  return `${API}/api/v1/images/${slug}/default/${view}?size=${size}`;
}

export function creditLink(slug: string): string {
  return `${API}/dogs/${slug}`;
}

If the request fails, the build stops with a clear message. That is what you want: a half-empty site is worse than a failed build.

The index page

Create src/pages/index.astro. It lists every breed and links to its page:

---
import { getCatalog } from "../lib/catalog";

const { breeds } = await getCatalog();
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Dog breeds</title>
  </head>
  <body>
    <main>
      <h1>Dog breeds</h1>
      <p>{breeds.length} breeds, from the Dog Image API catalog.</p>
      <ul>
        {breeds.map((breed) => (
          <li><a href={`/breeds/${breed.slug}/`}>{breed.name}</a></li>
        ))}
      </ul>
    </main>
  </body>
</html>

The count comes from the array length, so it stays correct without edits.

One page per breed

Create src/pages/breeds/[slug].astro. A dynamic route in a static build must export getStaticPaths, which returns one entry for each page:

---
import type { GetStaticPaths } from "astro";
import { getCatalog, previewUrl, creditLink } from "../../lib/catalog";
import type { Breed, View } from "../../lib/catalog";

export const getStaticPaths = (async () => {
  const { breeds, views } = await getCatalog();
  return breeds.map((breed) => ({
    params: { slug: breed.slug },
    props: { breed, views },
  }));
}) satisfies GetStaticPaths;

const { breed, views } = Astro.props as { breed: Breed; views: View[] };
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{breed.name} images from nine angles</title>
    <style>
      body { font-family: system-ui, sans-serif; margin: 0; }
      main { max-width: 960px; margin: 0 auto; padding: 2rem 1rem; }
      .grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 1rem;
        list-style: none;
        padding: 0;
      }
      figure { margin: 0; }
      img { width: 100%; height: auto; background: #f4f1ea; border-radius: 8px; }
      figcaption { font-size: 0.875rem; margin-top: 0.25rem; }
    </style>
  </head>
  <body>
    <main>
      <p><a href="/">All breeds</a></p>
      <h1>{breed.name}</h1>
      <p>
        Nine views of one adult {breed.name}, AI-generated illustrations on a
        transparent background.
      </p>
      <ul class="grid">
        {views.map((view) => (
          <li>
            <figure>
              <img
                src={previewUrl(breed.slug, view.id)}
                alt={`${breed.name}, ${view.label}, AI-generated illustration`}
                width="512"
                height="512"
                loading="lazy"
              />
              <figcaption>{view.label}</figcaption>
            </figure>
          </li>
        ))}
      </ul>
      <p>
        <a href={creditLink(breed.slug)}>{breed.name} image: Dog Image API</a>
      </p>
    </main>
  </body>
</html>

Two things are worth a closer look.

First, getStaticPaths runs once, at build time, and passes breed and views to the page as props. The page body does not fetch again. Second, the grid uses auto-fill with a 200 pixel minimum, so it shows one column on a narrow phone and four or five on a wide screen without media queries.

Build and check

npm run dev

Open /breeds/pug/ and you should see nine tiles: Front; Front three-quarter, left; Left side; and so on through Head portrait. Then run npm run build. The output folder dist/breeds/ holds one directory for each breed.

The credit link

Free previews are allowed for any use, commercial included, when a visible link credits Dog Image API next to the image or on the same page. The link at the bottom of each page has the form each breed page on the site gives you. Because the code builds it from the slug, every generated page carries it. See the license for the exact wording.

Things to adjust

  • Fewer builds, fewer requests. The previews are loaded by your visitors' browsers, not by the build. A build makes one catalog request.
  • Breed summaries. GET /api/v1/breeds/{breed} returns a short Wikipedia-based summary and its source link. You can fetch it inside the page frontmatter, but that is one request per breed, so cache it if you rebuild often. Credit Wikipedia where you show the text.
  • Other angles on one page. The left side gallery shows one view across every breed, which is a good model for a second route.
  • Full-size files. They need a key and belong in a build step that stores them on your side. The Next.js guide shows that pattern, and the docs list the endpoints.

Questions

Does the build need an API key?

No. The breed catalog and the 512 pixel previews are keyless. A key is only needed for full-size PNG files, which you would download in a separate build or server step.

How many pages will the build create?

One per breed in the live catalog, plus an index page. The code reads the list at build time, so you never hardcode the number and new breeds appear on the next build.

How do I add colors such as fawn or black?

Breeds with several colors list them as variants in the catalog. Replace the word default in the image URL with a variant slug, or add a nested route that reads each breed's variants.

Next steps