My photography portfolio with galleries, tags, folders, and a globe ๐
The prerequisites are Node >= 20 and pnpm >= 8.
First, install the dependencies:
pnpm installRun the development server:
pnpm devOpen http://localhost:3000 to see the app!
This project uses Contentful to upload photos and manage photo albums. No other CMS are currently supported.
Beginning April 30, 2025, Contentful has made changes to the Free plan entitlements by reducing the monthly asset bandwidth from 800GB to 50GB. If you expect to exceed 50GB in CDN bandwidth a month, you may consider replacing it with another CMS and update the GraphQL queries and environment variables as necessary.
To start, you'll need to create a Contentful space. Once created, head to Settings -> API Keys -> Add API Key.
Here, you'll see a space ID and two tokens. You can then populate an .env.local file with these:
CONTENTFUL_SPACE_ID=abcdefghijkl
CONTENTFUL_PREVIEW_ACCESS_TOKEN=roughly40randomcharacters
CONTENTFUL_ACCESS_TOKEN=roughly40randomcharacters
Then, head to Content model -> Create content type. Create the Photo Albums model using the schema below. Once made, you can add entries on the Content tab, and upload images to each entry's media field.
This project does not make use of the Contentful Image API to optimize photos. Instead, we optimize images beforehand by converting them to .webp in a script using cwebp and ImageMagick:
bash scripts/webp.sh <some directory of image files>
The project is written in TypeScript, using Zod, Tailwind, and Next.js.
Globe.GL for the homepage globe and cobe for the about page globe
d3-geo for globe data
three for scene creation
PhotoSwipe for image lightboxes
Masonic for masonry layouts
Pig for image grid layouts
cwebp for compressing .jpg to .webp images. See scripts/webp.sh for details regarding image optimization.
The Next site is statically exported and hosted on a GitHub Pages site using GitHub Actions.
All assets are stored on Contentful and fetched from their GraphQL endpoint.
This model refers to the albums featured on the front page.
When asked, the Contentful model should be created with the API identifier photoGallery. You can pick any ID, just be sure to then update photoGalleryCollection where needed in the codebase.
| Field | Contentful Type |
|---|---|
| title | Short text |
| photos | Media, many files |
| color | Short text - Hexadecimal color code |
| type | Short text - Enum['location', 'custom']ยน |
| description | Long text |
| date | Short text (optional) |
| lat | Decimal |
| lng | Decimal |
| locations | JSON (optional)ยฒ |
| order | Decimal |
ยน type: 'location' refers to albums on the front page with coordinate data. type: 'custom' has a fancy animation and no coordinate data, e.g. the Music album on my site.
ยฒ This is an array of objects, each with lat, lng, and description; e.g. [{"lat": 40.00, "lng": 70.00, "description": "narnia"}, ...]
For more information, see the album Zod schemas.
This model refers to the folders feature available at the /folders route.
When asked, the Contentful model should be created with the API identifier photoFolders. You can pick any ID, just be sure to then update photoFoldersCollection where needed in the codebase.
This feature is optional.
| Field | Contentful Type |
|---|---|
| title | Short text |
| parent_title | Short text |
| photos | Media, many files |
| description | Long text |
| date | Short text |
| order | Decimal |