)}
diff --git a/app/layout.tsx b/app/layout.tsx
index 82b8a6d..f7b3fb2 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -149,9 +149,11 @@ export default async function RootLayout({ children }: { children: React.ReactNo
Home
-
- About
-
+ {config.aboutEnabled && (
+
+ About
+
+ )}
{config.map && (
Map
diff --git a/components/AdminDiagnosticBanner.tsx b/components/AdminDiagnosticBanner.tsx
index 6c68137..bf9ee7e 100644
--- a/components/AdminDiagnosticBanner.tsx
+++ b/components/AdminDiagnosticBanner.tsx
@@ -1,4 +1,5 @@
import Link from 'next/link';
+import { IconRefresh } from './Icons';
interface AdminDiagnosticBannerProps {
slug: string;
@@ -39,10 +40,8 @@ export function AdminDiagnosticBanner({
gallery.yaml
- {configuredAlbumCount > 0
- ? `, and configured with ${configuredAlbumCount} album(s)`
- : ''}
- , but Immich returned no accessible albums or assets.
+ {configuredAlbumCount > 0 ? `, and configured with ${configuredAlbumCount} album(s)` : ''},
+ but Immich returned no accessible albums or assets.
{reason && (
@@ -64,17 +63,27 @@ export function AdminDiagnosticBanner({
Possible Causes & Checklist:
-
+
Album Shared Status in Immich: Ensure the album in Immich is marked as{' '}
- Shared (Geteilt). Unshared albums are excluded by Immich Folio for security.
+ Shared (Geteilt). Unshared albums are excluded by Immich Folio for
+ security.
Cache Lag: If you just added the album or changed permissions, click{' '}
Force Reload Fresh below.
- Album UUID: Check if the album UUID in your Page Builder matches the exact Immich Album ID.
+ Album UUID: Check if the album UUID in your Page Builder matches the
+ exact Immich Album ID.
@@ -95,7 +104,7 @@ export function AdminDiagnosticBanner({
gap: '6px',
}}
>
- 🔄 Force Reload Fresh (?fresh=1)
+ Force Reload Fresh (?fresh=1)
- {open ? '✕' : '🎨'}
+ {open ? (
+
+ ) : (
+
+ )}
{/* ── Panel — anchored bottom-left ────────── */}
@@ -260,7 +265,7 @@ exifOnHover: ${exifOnHover}`;
}}
>
- 🎨 Theme Builder
+ Theme Builder
{
+ size?: number;
+ className?: string;
+ strokeWidth?: number;
+}
+
+export function IconSun({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconMoon({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconPalette({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconTarget({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconFilm({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconFrame({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSparkles({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconGear({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCamera({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconMap({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconGrid({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconColumns({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconLink({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconScale({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconFileText({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconSearch({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconGlobe({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconBan({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconArchive({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconShieldCheck({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconRefresh({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconChevronDown({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconFolder({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconTrash({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconPencil({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconLock({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconHeart({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+/** Filled by default — it marks a state (favourite), not an action. */
+export function IconStar({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconImage({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconBarChart({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconGripVertical({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconX({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconPlus({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconHome({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCopy({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconQuote({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconChevronUp({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconArrowLeftRight({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCalendar({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconArrowDown({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconArrowUp({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+/** An "A→Z" marker for alphabetical / filename ordering. */
+export function IconSortAlpha({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+export function IconCheck({ size = 16, className = 'svg-icon', strokeWidth = 2, ...props }: IconProps) {
+ return (
+
+ );
+}
+
+/** Open book — journal entries and photo essays. */
+export function IconBook({
+ size = 16,
+ className = 'svg-icon',
+ strokeWidth = 2,
+ ...props
+}: IconProps) {
+ return (
+
+ );
+}
+
+export function IconEye({
+ size = 16,
+ className = 'svg-icon',
+ strokeWidth = 2,
+ ...props
+}: IconProps) {
+ return (
+
+ );
+}
+
+/** Crossed-out eye — an unlisted page, hidden from the header menu. */
+export function IconEyeOff({
+ size = 16,
+ className = 'svg-icon',
+ strokeWidth = 2,
+ ...props
+}: IconProps) {
+ return (
+
+ );
+}
+
+/** Clock — reading time. */
+export function IconClock({
+ size = 16,
+ className = 'svg-icon',
+ strokeWidth = 2,
+ ...props
+}: IconProps) {
+ return (
+
+ );
+}
diff --git a/components/Lightbox.module.css b/components/Lightbox.module.css
index 3f2f2c5..1567e2d 100644
--- a/components/Lightbox.module.css
+++ b/components/Lightbox.module.css
@@ -143,6 +143,11 @@
position: fixed;
bottom: 24px;
right: 24px;
+ /* Icon and label sit on one line — the buttons carried an emoji before, which
+ needed no alignment of its own. */
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
z-index: 1001;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(8px);
@@ -285,7 +290,9 @@
letter-spacing: 0.1em;
text-transform: uppercase;
color: #ffffff;
- text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 10px rgba(0, 0, 0, 0.7);
+ text-shadow:
+ 0 1px 4px rgba(0, 0, 0, 0.9),
+ 0 0 10px rgba(0, 0, 0, 0.7);
padding: 12px 16px;
user-select: none;
}
@@ -306,7 +313,6 @@
transform: translate(-50%, -50%);
}
-
@media (hover: none) {
.close,
.nav,
diff --git a/components/Lightbox.tsx b/components/Lightbox.tsx
index 86db386..f01cd26 100644
--- a/components/Lightbox.tsx
+++ b/components/Lightbox.tsx
@@ -18,6 +18,7 @@ import { useExif } from '@/hooks/useExif';
import { useSwipe } from '@/hooks/useSwipe';
import styles from './Lightbox.module.css';
import { useProofing } from './ProofingContext';
+import { IconHeart } from './Icons';
export interface LightboxWatermark {
enabled?: boolean;
@@ -299,7 +300,8 @@ export function Lightbox({
aria-label={isFav ? 'Remove from favorites' : 'Add to favorites'}
title={isFav ? 'Remove from favorites' : 'Add to favorites'}
>
- {isFav ? '❤️ Saved' : '🤍 Favorite'}
+
+ {isFav ? 'Saved' : 'Favorite'}
)}
diff --git a/components/ProofingModal.tsx b/components/ProofingModal.tsx
index 803ed5c..4a2eaff 100644
--- a/components/ProofingModal.tsx
+++ b/components/ProofingModal.tsx
@@ -2,6 +2,7 @@
import React, { useState } from 'react';
import { useProofing } from './ProofingContext';
+import { IconCheck, IconCopy, IconLink } from './Icons';
export function ProofingModal() {
const proofing = useProofing();
@@ -125,7 +126,15 @@ export function ProofingModal() {
cursor: 'pointer',
}}
>
- {copiedState === 'link' ? '✓ Link Copied!' : '🔗 Copy Shareable Link'}
+ {copiedState === 'link' ? (
+ <>
+ Link Copied!
+ >
+ ) : (
+ <>
+ Copy Shareable Link
+ >
+ )}
{allowMailto && (
diff --git a/content/essays/sample-story.md b/content/journal/sample-story.md.example
similarity index 59%
rename from content/essays/sample-story.md
rename to content/journal/sample-story.md.example
index b495880..9b05d6c 100644
--- a/content/essays/sample-story.md
+++ b/content/journal/sample-story.md.example
@@ -5,20 +5,27 @@ author: "Portfolio Photographer"
date: "2026-08-05"
---
+Copy this file to `content/journal/.md` to start an entry by hand, or
+create one under Journal in the admin panel — the studio writes this same
+format. Delete this paragraph and the next one when you are done.
+
+Photos are referenced by their Immich asset id, and those ids are specific to
+your server, so this template ships without any. Add them in the studio via
+Add Block → Photo, where the picker fills the id in for you. Written by hand it
+is `` for a single photo — `:wide` or no suffix
+give the narrower widths — and `` for a
+side-by-side pair.
+
# Into the Wilderness
The journey began at dawn along the F-roads winding deep into the interior. As civil infrastructure faded behind us, the landscape shifted dramatically into vast fields of black volcanic ash framed by vivid neon-green moss.
> Silence in the highlands isn't just the absence of sound — it is a physical weight that fills every horizon.
-
-
## Landmannalaugar & Beyond
Walking through the rhyolite mountains, the earth fumes with geothermal vents. The color spectrum ranges from deep ochre and sulfur yellow to dark basalt gray.
-
-
### Reflection & Light
As light broke through the storm clouds late in the afternoon, the contrast between the dark terrain and glowing peaks created a surreal atmosphere that felt entirely unearthly.
diff --git a/lib/config/index.ts b/lib/config/index.ts
index 54f28e1..c637a76 100644
--- a/lib/config/index.ts
+++ b/lib/config/index.ts
@@ -367,6 +367,7 @@ export function getConfig(): AppConfig {
scrollToTop: false,
analytics: true,
proofing: { enabled: false, allowMailto: false },
+ aboutEnabled: true,
albumOverrides: {},
albumDescriptions: {},
albumPasswords: {},
@@ -468,6 +469,7 @@ export function getConfig(): AppConfig {
},
protection: settings.protection,
watermark: settings.watermark,
+ aboutEnabled: settings.about?.enabled !== false,
albumOverrides,
albumDescriptions,
albumPasswords,
diff --git a/lib/config/schema.ts b/lib/config/schema.ts
index f533f72..21bb4bb 100644
--- a/lib/config/schema.ts
+++ b/lib/config/schema.ts
@@ -128,6 +128,7 @@ export interface AppConfig {
enabled: boolean;
allowMailto: boolean;
};
+ aboutEnabled: boolean;
albumOverrides: Record;
albumDescriptions: Record;
albumPasswords: Record;
@@ -263,6 +264,7 @@ export interface SettingsYaml {
opacity?: number;
position?: 'bottom-right' | 'bottom-left' | 'center';
};
+ about?: { enabled?: boolean };
}
export function slugify(name: string): string {