ScreenshotNeo

BlogHow-to

How to Build a Website Thumbnail Directory with Astro

Build an Astro directory from structured records, render consistent thumbnails, and optionally generate a static detail page for each site.

By the ScreenshotNeo team4 October 202610 min read

To build a website thumbnail directory with Astro, store each site as a structured record, render those records from a page in src/pages/, and use Astro’s image components for consistently sized thumbnails. If each site needs its own page, add a dynamic route such as src/pages/items/[slug].astro and return one path per record from getStaticPaths(). A content collection does not create routes by itself.

1. Choose how to store directory records

Keep directory data separate from the page markup. For a small, code-maintained directory, a TypeScript or JSON data file is simple. For editorial content with validation and frontmatter, use an Astro content collection. If records come from an API or database, fetch them at build time for a static directory or at request time in a server-rendered deployment.

Source Good fit Trade-off
Local data file A short, stable list edited by developers Validation and editorial workflow are yours to maintain
Content collection Records maintained as content files with a known schema Collection entries still need an explicit page route
External data source A directory managed in another system Builds depend on data availability, credentials, and API shape

Here is a compact local-data version. Put thumbnail files under src/assets/sites/; importing them lets Astro process local assets.

// src/data/sites.ts
import acmeThumb from '../assets/sites/acme.jpg';
import exampleThumb from '../assets/sites/example.jpg';

export const sites = [
  {
    title: 'Acme Design',
    slug: 'acme-design',
    url: 'https://example.com',
    description: 'A short description of the site.',
    thumbnail: acmeThumb,
    thumbnailAlt: 'Preview of the Acme Design homepage',
    category: 'Design',
  },
  {
    title: 'Example Tools',
    slug: 'example-tools',
    url: 'https://example.org',
    description: 'A directory entry with a useful summary.',
    thumbnail: exampleThumb,
    thumbnailAlt: 'Preview of the Example Tools homepage',
    category: 'Tools',
  },
];

Use stable, unique slugs. Treat the destination URL and the route slug as different fields: a site may change its domain while its directory page URL remains stable.

2. Build the listing page

Astro uses file-based routing: supported files in src/pages/ create routes. Add src/pages/index.astro for the directory home page. The following example renders a card grid, links the whole card using a native anchor, and reserves a consistent thumbnail ratio to reduce layout shifts.

---
import { Image } from 'astro:assets';
import { sites } from '../data/sites';
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Website Directory</title>
  </head>
  <body>
    <main>
      <h1>Website Directory</h1>
      <p>Browse selected websites by category.</p>
      <ul class="site-grid">
        {sites.map((site) => (
          <li>
            <a class="site-card" href={`/items/${site.slug}/`}>
              <Image
                src={site.thumbnail}
                alt={site.thumbnailAlt}
                width={640}
                height={400}
                class="thumbnail"
              />
              <div class="card-copy">
                <p class="category">{site.category}</p>
                <h2>{site.title}</h2>
                <p>{site.description}</p>
              </div>
            </a>
          </li>
        ))}
      </ul>
    </main>
  </body>
</html>

<style>
  body { margin: 0; color: #18212b; font: 16px/1.5 system-ui, sans-serif; }
  main { width: min(1120px, calc(100% - 2rem)); margin: 3rem auto; }
  .site-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 1.25rem; padding: 0; list-style: none; }
  .site-card { display: block; height: 100%; overflow: hidden; border: 1px solid #dce2e8; border-radius: .75rem; color: inherit; text-decoration: none; }
  .site-card:focus-visible { outline: 3px solid #315ee7; outline-offset: 3px; }
  .thumbnail { display: block; width: 100%; aspect-ratio: 8 / 5; object-fit: cover; background: #eef1f4; }
  .card-copy { padding: 1rem; }
  .card-copy h2 { margin: .2rem 0 .5rem; font-size: 1.2rem; }
  .card-copy p { margin: .3rem 0; }
  .category { color: #596775; font-size: .85rem; }
</style>

The imported asset dimensions can be used by Astro’s image tooling. The example sets display dimensions to a shared card size; prepare source images with a consistent crop if you want all previews to show comparable framing. Meaningful alternative text should describe the destination preview when it adds information. If the image is purely decorative beside an equivalent title, an empty alt may be more appropriate.

3. Add optional detail pages with a dynamic route

If each directory entry needs a description, tags, links, or a larger preview, create src/pages/items/[slug].astro. In static output, Astro calls getStaticPaths() to learn which dynamic paths to prerender. The route parameter key must match the bracketed filename parameter, and its value must be a string. The function runs in an isolated scope, so import or query the records inside it rather than relying on arbitrary page frontmatter variables.

---
import { Image } from 'astro:assets';
import { sites } from '../../data/sites';

export function getStaticPaths() {
  return sites.map((site) => ({
    params: { slug: site.slug },
    props: { site },
  }));
}

const { site } = Astro.props;
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{site.title}</title>
    <meta name="description" content={site.description} />
  </head>
  <body>
    <main>
      <p><a href="/">Back to directory</a></p>
      <article>
        <h1>{site.title}</h1>
        <p>{site.description}</p>
        <Image src={site.thumbnail} alt={site.thumbnailAlt} width={1200} height={750} />
        <p><a href={site.url}>Visit {site.title}</a></p>
      </article>
    </main>
  </body>
</html>

Without detail pages, link each card directly to site.url or use a button-free card that links to an internal filtered listing. Do not emit item links unless the corresponding route is generated.

4. Use a content collection instead of a TypeScript array

Collections are useful when each record deserves its own content file. They are stored outside src/pages/ and do not automatically become routes. Define a schema and store each record under a collection directory. The exact image path and import arrangement depends on your Astro version and project structure; Astro’s image guide covers associating local images with collection entries and rendering those images in a listing.

// src/content.config.ts (Astro content collections configuration)
import { defineCollection, z } from 'astro:content';

const sites = defineCollection({
  schema: z.object({
    title: z.string(),
    url: z.string().url(),
    description: z.string(),
    category: z.string(),
    thumbnail: z.string(),
    thumbnailAlt: z.string(),
  }),
});

export const collections = { sites };

A collection-backed listing queries entries in its frontmatter and maps the results into cards. For dynamic item pages, query the collection inside getStaticPaths() and pass each entry through props. Check the current Astro collection and image documentation for the image-loading helper and content configuration format that match your installed Astro version.

5. Choose local or remote thumbnails

Local assets are straightforward to optimize at build time. Remote image URLs are a separate case: configure approved hosts with image.domains or the more specific image.remotePatterns setting when applicable. Astro documents that remote images from other sources are not optimized. Its <Image /> component can still provide dimensions that help avoid layout shifts, but source authorization and image transformation are distinct concerns. Confirm your image service and deployment adapter support the transformation you plan to use.

// astro.config.mjs — allow a known remote image host
import { defineConfig } from 'astro/config';

export default defineConfig({
  image: {
    domains: ['images.example.com'],
  },
});

Only allow sources you intend to use. If directory records are user-submitted, validate image hostnames and URLs before rendering them, and consider downloading approved thumbnails into managed storage so a third-party host cannot unexpectedly change or remove them.

6. Add sorting, filtering, and pagination only when needed

For a small directory, sort and filter the records before mapping them. Use URL query parameters for filters if users need to share a filtered view; static pages can also be generated for a known set of categories. For large directories, paginate the output rather than placing every card in one document. The best choice depends on record count, update frequency, and whether search must work without client-side JavaScript.

const sortedSites = [...sites].sort((a, b) => a.title.localeCompare(b.title));
const categories = [...new Set(sortedSites.map((site) => site.category))].sort();

Keep filter controls as real links or semantic form controls, preserve visible headings, and make the destination of every card understandable without relying on its thumbnail alone.

7. Capture thumbnails for the directory

If thumbnails should show current website pages, you need a capture process in addition to Astro’s rendering. You can capture pages yourself with a browser automation tool, then store the resulting image as a local asset or at an image host. For repeatable output, define a fixed viewport, wait condition, and capture schedule; store the capture time and source URL in your record so stale previews can be found and refreshed. Browser setup and page behavior vary by site, and bot checks or consent dialogs can affect a capture.

For examples and configuration options that fit your setup, see the ScreenshotNeo API documentation. ScreenshotNeo is a website screenshot API and MCP server from Yorker Media.

Or skip the browser setup

Make one request for a site URL and save the returned image. The examples below use a WebP output filename and the API endpoint; create an API key first and replace the placeholder. See the ScreenshotNeo docs for output and capture options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o thumbnail.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
with open("thumbnail.webp", "wb") as image:
    image.write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('thumbnail.webp', image));

Cookie banners, newsletter popups, and chat widgets are removed before capture, and each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing; response headers identify the page verdict and whether a shot was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. See ScreenshotNeo for details, or sign up for 1,000 free screenshots a month with no card.

8. Refreshing and storing captures

Choose a refresh policy based on how quickly the source sites change. A directory of stable company homepages may need infrequent refreshes; time-sensitive pages need shorter intervals. Cache images at build time or in your asset store so visitors do not trigger a new capture. Keep source URLs and capture metadata with the record, and regenerate only entries that are new, stale, or explicitly requested. Avoid capturing on every page request unless you intentionally run a server-side dynamic workflow.

9. Performance, reliability, and cost

  • Build time: Static detail routes grow with the number of records. Keep path generation deterministic and avoid unnecessary remote calls for every entry.
  • Page weight: Use appropriately sized thumbnails, consistent dimensions, and modern image formats supported by your delivery setup. Do not send full-resolution source captures to every card.
  • Remote dependencies: A remote thumbnail can fail or change independently of the site build. Download and retain approved assets when stable presentation matters.
  • Capture reliability: A target may load slowly, show a bot challenge, or require interaction. Record failures separately and retry selectively rather than repeatedly rebuilding the entire directory.
  • Capture cost: Browser-based self-hosting trades API charges for browser compute, storage, and maintenance. With ScreenshotNeo, plans are $5 for 3,000, $15 for 15,000, $39 for 60,000, $99 for 250,000, or $249 for 1,000,000 shots; yearly billing gives two months free. Free includes 1,000 monthly shots. Only clean shots are billed.

10. Troubleshooting

Symptom Likely cause Fix
Collection records do not have pages Content collections are data, not automatic routes Add a file under src/pages/ and generate paths for dynamic pages with getStaticPaths().
Build reports a missing route parameter The params key does not match the bracketed filename, or the value is not a string For [slug].astro, return params: { slug: String(value) }.
Detail links lead to 404 pages The listing links to slugs that were not returned from getStaticPaths() Check that every listed record has a unique slug and that all records are included when paths are built.
Remote images are not transformed The source host is not permitted or the deployment image service does not support the requested transformation Configure the approved domain or remote pattern and verify adapter/service support; otherwise serve the source image without promising optimization.
Thumbnails have uneven card heights Intrinsic image dimensions or CSS aspect ratio are inconsistent Set consistent width, height, and aspect-ratio; use object-fit: cover for a uniform crop.
Capture shows a challenge, blank page, or incomplete content The destination blocks automation, loads slowly, or renders after the capture point Review page behavior, wait strategy, and capture settings; mark the preview unavailable when the page cannot be captured reliably.
Build fails when fetching external directory data The source API is unavailable, credentials are absent, or its response changed Validate the response schema, report missing configuration clearly, and use a controlled snapshot or retry policy if the directory can tolerate stale data.

FAQ

Do Astro content collections create pages automatically?

No. They hold structured content. Add routes in src/pages/ and use getStaticPaths() for generated static detail pages.

Can I make the directory without individual detail pages?

Yes. Render the records in one listing and link each card to its external site, or use category pages if those routes add value.

Can a directory use thumbnails hosted on another domain?

Yes. Configure approved remote sources as needed, and check whether your image service and deployment adapter can optimize them.

Astro documentation