Simple Photo Gallery supports custom themes, allowing you to create your own visual design and layout while leveraging the gallery's core functionality.
You can use custom themes in two ways:
Install the theme as a dependency and use the package name:
# Install your custom theme package
npm install @your-org/your-private-theme
# Build with the custom theme
spg build --theme @your-org/your-private-themeYou can also use a local theme directory without publishing to npm:
# Build with a local theme (relative path)
spg build --theme ./themes/my-local-theme
# Build with a local theme (absolute path)
spg build --theme /path/to/my-themeThe local theme directory must contain a package.json file and follow the same structure as an npm theme package.
If you don't specify --theme, the default @simple-photo-gallery/theme-modern theme will be used.
The fastest way to create a theme is to use the built-in scaffolder:
# Creates ./themes/my-theme (or in your monorepo root if you run this inside a workspace package)
spg create-theme my-themeIf you prefer a custom output directory:
spg create-theme my-theme --path ./my-themeThe create-theme command works by copying the base theme template (bundled with the package) and customizing it with your theme name. This means:
- All files from the bundled template are copied (excluding build artifacts)
- The theme name is automatically updated in
package.jsonandREADME.md - You get a complete, working theme ready to customize
After creating your theme:
cd ./themes/my-theme
yarn installNote: The generated theme requires
GALLERY_JSON_PATHto be set (it's how the theme reads yourgallery.json). When you runspg build, the CLI sets it automatically. When you runastro devdirectly, you need to set it yourself (see "Theme Development" below).
Tip: The base theme template is bundled with the
simple-photo-gallerypackage. The source is located atgallery/src/modules/create-theme/templates/basein the repository. For local development, if you're working on the CLI itself and want to test template changes, you can modify the template files there. Alternatively, you can createthemes/basein the workspace root as a fallback for testing - it will be used if present.
A theme is an npm package built with Astro that follows a specific structure and interface.
Your theme package should have the following structure:
your-theme-package/
├── package.json
├── themeConfig.json (optional, but recommended)
├── astro.config.ts
├── tsconfig.json
└── src/
└── pages/
└── index.astro
Your theme package must be a valid npm package with:
- A unique package name (e.g.,
@your-org/your-theme-name) "type": "module"for ES modules support- Required dependencies (see below)
- Files array that includes all necessary files:
{
"name": "@your-org/your-theme-name",
"version": "1.0.0",
"type": "module",
"files": ["public", "src", "astro.config.ts", "tsconfig.json"],
"dependencies": {
"astro": "^5.11.0",
"@simple-photo-gallery/common": "^2.1.3"
}
}Your Astro config must:
- Use
output: 'static'for static site generation - Set
outDirto${outputDir}/_buildwhereoutputDircomes fromprocess.env.GALLERY_OUTPUT_DIR - Define
process.env.GALLERY_JSON_PATHin Vite'sdefineconfig - Use the
astro-relative-linksintegration (recommended)
Example:
import { defineConfig } from "astro/config";
import relativeLinks from "astro-relative-links";
const sourceGalleryPath = process.env.GALLERY_JSON_PATH;
if (!sourceGalleryPath) {
throw new Error("GALLERY_JSON_PATH environment variable is not set");
}
const outputDir =
process.env.GALLERY_OUTPUT_DIR ||
sourceGalleryPath.replace("gallery.json", "");
export default defineConfig({
output: "static",
outDir: outputDir + "/_build",
build: {
assets: "assets",
assetsPrefix: "gallery",
},
integrations: [relativeLinks()],
vite: {
define: {
"process.env.GALLERY_JSON_PATH": JSON.stringify(sourceGalleryPath),
},
},
});Theme authors can provide default configuration for their theme by including a themeConfig.json file in the theme root directory (same level as package.json).
This allows you to set optimal defaults for your theme's layout while still allowing users to override these settings per gallery in their gallery.json file.
Example themeConfig.json:
{
"thumbnails": {
"size": 300,
"edge": "height"
}
}Configuration Options:
thumbnails.size(number): Default thumbnail size in pixels (default: 300)thumbnails.edge(string): How the size is applied"auto": Applied to longer edge (default)"width": Applied to width (good for masonry layouts)"height": Applied to height (good for row-based layouts)
Configuration Hierarchy:
The thumbnail settings follow this priority order:
- Gallery-level (highest priority): Settings in user's
gallery.json - Theme-level: Settings in theme's
themeConfig.json - Built-in defaults (lowest priority): 300px on auto (longer edge)
This means users can override your theme defaults per gallery, while your theme provides sensible defaults that work well with its layout.
Loading Theme Config:
Use the loadThemeConfig() utility from @simple-photo-gallery/common/theme:
import path from "node:path";
import { loadThemeConfig } from "@simple-photo-gallery/common/theme";
const themePath = path.resolve(import.meta.dirname, "../..");
const themeConfig = loadThemeConfig(themePath);See the example in section 3 below for complete integration.
This is your main theme entry point. It must:
- Read
gallery.jsonfrom the path specified inprocess.env.GALLERY_JSON_PATH - Parse and use the
GalleryDatastructure - Generate valid HTML output
Recommended approach - Use the resolver utilities from @simple-photo-gallery/common/theme:
---
import path from 'node:path';
import { loadGalleryData, loadThemeConfig, resolveGalleryData } from '@simple-photo-gallery/common/theme';
import type { ResolvedGalleryData } from '@simple-photo-gallery/common/theme';
// Read gallery.json from the path provided by the build process
const galleryJsonPath = import.meta.env.GALLERY_JSON_PATH || './gallery.json';
// Load gallery data
const raw = loadGalleryData(galleryJsonPath, { validate: true });
// Load theme config (optional, for theme-level defaults)
const themePath = path.resolve(import.meta.dirname, '../..');
const themeConfig = loadThemeConfig(themePath);
// Resolve gallery data with theme config
const gallery: ResolvedGalleryData = await resolveGalleryData(raw, {
galleryJsonPath,
themeConfig
});
// Extract resolved gallery properties
const { hero, sections, subGalleries, metadata, thumbnails } = gallery;
---
<html>
<head>
<title>{hero.title}</title>
<meta name="description" content={hero.description} />
{metadata.analyticsScript && (
<Fragment set:html={metadata.analyticsScript} />
)}
</head>
<body>
<!-- Your theme implementation here -->
<h1>{hero.title}</h1>
<div set:html={hero.parsedDescription} />
<!-- Render hero with responsive images -->
<picture>
<source srcset={hero.srcsets.landscapeAvif} type="image/avif" media="(orientation: landscape)" />
<source srcset={hero.srcsets.landscapeJpg} type="image/jpeg" media="(orientation: landscape)" />
<source srcset={hero.srcsets.portraitAvif} type="image/avif" media="(orientation: portrait)" />
<source srcset={hero.srcsets.portraitJpg} type="image/jpeg" media="(orientation: portrait)" />
<img src={hero.src} alt={hero.title} />
</picture>
<!-- Render gallery sections -->
{sections.map((section) => (
<section>
<h2>{section.title}</h2>
<div set:html={section.parsedDescription} />
{section.images.map((image) => (
<a href={image.imagePath} data-pswp-width={image.width} data-pswp-height={image.height}>
<img
src={image.thumbnailPath}
srcset={image.thumbnailSrcSet}
alt={image.alt || image.filename}
width={image.thumbnailWidth}
height={image.thumbnailHeight}
/>
</a>
))}
</section>
))}
</body>
</html>Alternative - Manual approach (not recommended for new themes):
---
import fs from 'node:fs';
import type { GalleryData } from '@simple-photo-gallery/common';
// Read gallery.json from the path provided by the build process
const galleryJsonPath = process.env.GALLERY_JSON_PATH || './gallery.json';
const galleryData = JSON.parse(fs.readFileSync(galleryJsonPath, 'utf8'));
const gallery = galleryData as GalleryData;
// Extract gallery properties - note: paths and markdown NOT pre-computed
const { title, description, sections } = gallery;
---
<html>
<head>
<title>{title}</title>
<meta name="description" content={description} />
</head>
<body>
<h1>{title}</h1>
<p>{description}</p>
<!-- Note: Using raw filenames, not resolved paths -->
{sections.map((section) => (
<section>
{section.images.map((image) => (
<img src={image.filename} alt={image.alt || ''} />
))}
</section>
))}
</body>
</html>Note: The resolver approach is recommended because it provides pre-computed paths, responsive srcsets, and parsed markdown. The modern theme uses this pattern. See the Common Package API for complete documentation.
Important: There are two data structures to understand:
GalleryData- Raw structure fromgallery.json(manual approach)ResolvedGalleryData- Transformed structure with pre-computed paths (recommended approach)Recommendation: Use
resolveGalleryData()from@simple-photo-gallery/common/themeto get resolved data. This is what the modern theme uses.
Your theme receives a GalleryData object from gallery.json with the following structure:
interface GalleryData {
title: string;
description: string;
headerImage: string;
headerImageBlurHash?: string;
mediaBasePath?: string;
mediaBaseUrl?: string;
thumbsBaseUrl?: string;
url?: string;
analyticsScript?: string;
ctaBanner?: boolean;
thumbnailSize?: number;
metadata: {
image?: string;
imageWidth?: number;
imageHeight?: number;
ogUrl?: string;
ogType?: string;
ogSiteName?: string;
twitterSite?: string;
twitterCreator?: string;
author?: string;
keywords?: string;
canonicalUrl?: string;
robots?: string;
};
sections: Array<{
title?: string;
description?: string;
images: Array<{
filename: string;
width: number;
height: number;
caption?: string;
description?: string;
blurHash?: string;
thumbnail?: {
filename: string;
width: number;
height: number;
};
// ... other image properties
}>;
}>;
subGalleries?: {
title: string;
galleries: Array<{
title: string;
headerImage: string;
path: string;
}>;
};
}The @simple-photo-gallery/common package provides utilities that make theme development easier and more consistent.
loadGalleryData() - Load gallery.json with optional validation:
import { loadGalleryData } from "@simple-photo-gallery/common/theme";
const gallery = loadGalleryData("./gallery.json", { validate: true });resolveGalleryData() - Transform raw data into resolved structure:
import { resolveGalleryData } from "@simple-photo-gallery/common/theme";
const resolved = await resolveGalleryData(gallery, {
galleryJsonPath: "./gallery.json",
});
// Access pre-computed data
resolved.hero.src; // Computed hero image path
resolved.hero.srcsets; // Responsive image srcsets
resolved.sections[0].parsedDescription; // HTML from markdown
resolved.sections[0].images[0].imagePath; // Computed image pathBenefits of using the resolver:
- All image paths pre-computed (no manual path logic)
- Responsive srcsets built automatically
- Markdown descriptions parsed to HTML
- Type-safe with
ResolvedGalleryDatatype
The @simple-photo-gallery/common/client module provides browser-side utilities:
PhotoSwipe Lightbox:
import { createGalleryLightbox } from "@simple-photo-gallery/common/client";
const lightbox = createGalleryLightbox({
gallery: "#gallery",
children: "a",
});
lightbox.init();Blurhash Decoding:
import { decodeAllBlurhashes } from "@simple-photo-gallery/common/client";
// Decodes all canvas elements with data-blurhash attribute
decodeAllBlurhashes();Hero Image Fallback:
import { initHeroImageFallback } from "@simple-photo-gallery/common/client";
// Smooth transition from blurhash to actual image
initHeroImageFallback();CSS Utilities:
import {
setCSSVar,
deriveOpacityColor,
} from "@simple-photo-gallery/common/client";
// Set CSS custom properties dynamically
setCSSVar("--primary-color", "#007bff");
// Create semi-transparent colors
const bgColor = deriveOpacityColor("#007bff", 0.1);
setCSSVar("--bg-color", bgColor);For a comprehensive list of all utilities and types, see the Common Package API documentation.
The build process sets these environment variables that your theme can access:
GALLERY_JSON_PATH: Absolute path to thegallery.jsonfileGALLERY_OUTPUT_DIR: Directory where the built gallery should be output
When building, the gallery CLI will:
- Set
GALLERY_JSON_PATHandGALLERY_OUTPUT_DIRenvironment variables - Run
npx astro buildin your theme package directory - Copy the built output from
_buildto the gallery output directory - Move
index.htmlto the gallery root
Your theme must output an index.html file in the build directory.
When you develop a theme, you’ll usually want to point it at a real gallery.json generated by the CLI.
- Create a gallery (once):
spg init -p /path/to/photos -g /path/to/gallery- Run the theme dev server with the required environment variables:
# macOS / Linux
export GALLERY_JSON_PATH="/path/to/gallery/gallery.json"
export GALLERY_OUTPUT_DIR="/path/to/gallery"
yarn dev# Windows (PowerShell)
$env:GALLERY_JSON_PATH="C:\path\to\gallery\gallery.json"
$env:GALLERY_OUTPUT_DIR="C:\path\to\gallery"
yarn dev- Use the resolver: Use
resolveGalleryData()from@simple-photo-gallery/common/themefor path computation and data transformation (recommended) - Use TypeScript: Import types from
@simple-photo-gallery/commonfor type safety - Leverage client utilities: Import from
@simple-photo-gallery/common/clientfor browser-side functionality like PhotoSwipe and blurhash - Handle optional fields: Many fields in
GalleryDataare optional - always check before using - Use resolved types: Work with
ResolvedGalleryData,ResolvedHero,ResolvedSection, etc. for pre-computed data - Optimize assets: Use Astro's asset optimization features
- Test locally: Use
astro devto preview your theme during development - Follow Astro conventions: Use Astro components, layouts, and best practices
- Base theme template: Bundled with the
simple-photo-gallerypackage and used byspg create-theme. This is a minimal, functional theme that serves as the starting point for all new themes. The source is ingallery/src/modules/create-theme/templates/base(orthemes/basein the repository for development). @simple-photo-gallery/theme-modern: A more advanced theme example. The source code is available in thethemes/moderndirectory of this repository.
Both themes demonstrate the required structure and can be used as reference implementations.
Once your theme is ready, you can use it in two ways:
Option 1: Local Development (No Publishing Required)
# Use the local theme directly
spg build --theme ./themes/my-themeOption 2: Publish to npm
- Publish it to npm (or your private registry)
- Install it in your project:
npm install @your-org/your-theme-name - Use it when building:
spg build --theme @your-org/your-theme-name
Local themes are perfect for development and private projects, while npm packages are ideal for sharing themes with others or using across multiple projects.
Theme not found
- For npm packages: Ensure the theme package is installed:
npm install @your-org/your-theme-name - For npm packages: Verify the package name matches exactly (including scope)
- For local paths: Verify the path is correct and the directory contains a
package.jsonfile - For local paths: Use an absolute path or a path relative to your current working directory
Build errors
- Check that
GALLERY_JSON_PATHis being read correctly - Verify your
astro.config.tsmatches the required structure - Ensure all dependencies are installed
Missing gallery data
- Verify
gallery.jsonexists at the expected path - Check that the
GalleryDatastructure matches the expected format