Photography portfolio built with Astro

Photography portfolio built with Astro

The challenges and lessons learned

I used WordPress to set up a theme for a new photographic brand called Saturn9 Photography 🚀a few years ago. The project was born and presented on the web. While we were progressing with photographic work over the years, the web presentation was slow and uninspiring so I decided to create a new, modern, SEO-friendly, and fast website. I wanted to build a simple static site without using any CMS systems.

As a front-end developer, I have a hard time choosing a tech stack. There are so many JS frameworks and libraries that it can be overwhelming. In my day-to-day job, I work with Angular, but for a static site that feels like an overkill. I also had some experience with Gatsby, which is in fact React, but I liked the plugin ecosystem, support of static site generation, using MDX for content, etc. I decided to go ahead and prototype, but then I got hooked on GSAP and started playing around with cool animations and transitions for our new photography project. The project was evolving though, and with that increased my attempts to redesign and implement the web. I made 3 different versions I think that eventually ended up in the GitLab cemetery of failed projects.

The idea of creating something pretty fast and simple turned out to be an endless iteration cycle with no end in sight. Things have settled down a bit after the photography book project 📷📙 we worked on for years got to its final phase.

Vysehrad - the swans

The fog has lifted and I made a final decision to build the website with Astro.

Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.

Another web framework to wrap my head around, I know. But I was intrigued and the possibilities were promising, hearing only praise about it. Thankfully, the documentation and concepts are very well explained.

Astro is content-focused and the HTML is rendered on the server. The main building block is the Astro component (.astro file). We can create for example Hero component Hero.astro, put some HTML inside, and import it in index.astro as a custom component </Hero>. Just as in React, we can pass down the props, render the lists in a similar way, etc. To speed up the development and jumpstart your project, Astro also provides free themes. I chose the Portfolio one.

Things were going well until I started facing some problems. The theme I used is helpful but has limitations in terms of displaying the content in a way I needed. I'll get to it shortly but let's describe some basics first. Here's the Astro structure:

Pages are responsible for handling routing, data loading, and overall page layout for every page in your website. The [..slug].astro is an interesting bit, which serves as a dynamic routing and generates a page for every 'work' in this case. Think of it as a layout for a blog post.

In the content folder, we can create multiple folders or so-called Content collections and each would contain the markdown (.md or .mdx) files. These are like blog categories and the posts within.

Let's say we have a blog collection and we want to get its content. There is a getCollection() function for that.

const allBlogPosts = await getCollection('blog');

Then we can loop through it and spit the content defined in the markdowns. What about the images?

Problem: Assets

Now this is an exciting part I struggled with. I started developing the site with Astro v2 but then Astro changed how the assets work. I enabled the experimental assets flag and later migrated fully to Astro v3.

In v3, there are 2 main folders to work with the images:

  • public

  • src/assets

Images in the public folder are not processed, while in the src folder, they are optimized and bundled. If you'd like to display a gallery with many photos, that's the right place to put them. Then I wanted to use the detail [...slug] page to show them under some content defined in the .mdx files. I created a bunch of folders in the assets folder for each photo gallery.

How can we display many photos on that page? Let's see what the .mdx file looks like

title: Grébovka
publishDate: 2022-10-01 00:00:00
img: /src/assets/grebovka/grebovka_20221009_090622.jpg
img_alt: Grébovka - Havlíčkovy sady
img_folder: /assets/grebovka
description: |
  Grébovka - Havlíčkovy sady: 2022 - present
author: Miro
published: true
  - Digital
  - Street photography


The part above Content is called frontmatter and is used to define metadata. It contains things like the title, date, tags, etc. We could define even an array [] here. I added the img_folder property to specify the path for the images.

Make sure you read the docs carefully because things might not work as you would expect. For the images in the content collections, we should also define the schema:

import { defineCollection, z } from 'astro:content';

export const collections = {
    work: defineCollection({
        schema: ({ image }) => z.object({
            title: z.string(),
            description: z.string(),
            author: z.string(),
            published: z.boolean(),
            tags: z.array(z.string()),
            img: image(),
            img_alt: z.string().optional(),
            img_folder: z.string().optional(),

Notice the img: image() helper for example. Without that displaying for example a cover photo wouldn't work. See also this StackOverflow question.

We know how to display a cover photo, but we still haven't figured out the gallery. Let's get back to frontmatter. In the [...slug].astro page we can retrieve and use those metadata like this:

const { entry } = Astro.props;
<BaseLayout title={} description={}>

That means that {} would contain the path for the images. There is a handy (not so handy) function glob() that can return all the files on that path.

const assetsDir = await Astro.glob('/src/assets/**/*.jpg');

Glob import is the function of Vite, which is powering Astro and it accepts only the string literal for the arguments.

That is the issue! We can't pass the img_folder there, because it's not a string literal. We could try glob-fast package, which supports the variables but that wouldn't help either as we will see. Never mind, we can just pass the string as above in the example to get all the JPGs in each folder. Since we have the entry metadata we can filter only the folder we need right?

const assetsDir = await Astro.glob('/src/assets/**/*.jpg');
const gallery = assetsDir.filter(dir => {
  if (dir?.default.src.includes( {
    return dir;

Then in the template, we can display all the images like this: => (
  <a href={img.default.src}>

This works only when Astro runs locally. As soon as we build the project and run the preview script, the images are gone. There's only a blank space. I've also tried filtering and mapping the images directly in the template with the same result.


The attempts described above didn't work, so I had to grab all those images with glob() and display them on the page. The solution that works even after the page is built and deployed on Netlify is the following one.

  assetsDir.length > 0 && => (
            ? "show"
            : "hidden"

I check whether the source file name (retrieved from glob()) includes the name of the gallery folder (passed in prop and if so I set a Tailwind class to either show or hide it. Note that I had to make sure all the files and folders have the same name to get a match. For example:

The result looks like this:

Grebovka detail page

If you know about a better way to display a gallery in the markdown file using Astro please let me know! This might not be ideal, but it works and I hope it helps you to build your own Astro site full of images.

And if you also like photography, check out our Saturn9 page and the new photobook project Seeing Vyšehrad 🏰. We put our heart and soul into it 💛

Thank you for reading!