diff --git a/_art/2024-10-09-x_DSC-0999.md b/_art/2024-10-09-x_DSC-0999.md
index 874ab5c..01d413f 100644
--- a/_art/2024-10-09-x_DSC-0999.md
+++ b/_art/2024-10-09-x_DSC-0999.md
@@ -3,8 +3,8 @@ layout: post
title: x_DSC-0999.jpg
date: 2024-10-09
categories: art
+anchor: "Kazakhstan - World Nomad Games"
---

-
diff --git a/_art/2024-10-25-Taq-e-Bostan_000521270020.md b/_art/2024-10-25-Taq-e-Bostan_000521270020.md
index 94e18bd..750741c 100644
--- a/_art/2024-10-25-Taq-e-Bostan_000521270020.md
+++ b/_art/2024-10-25-Taq-e-Bostan_000521270020.md
@@ -3,6 +3,7 @@ layout: post
title: Taq-e-Bostan_000521270020.jpg
date: 2024-10-25
categories: art
+anchor: "Iran - Hamadan, Kermanshah, Tabriz"
---

diff --git a/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db.md b/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db-00.md
similarity index 89%
rename from _art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db.md
rename to _art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db-00.md
index 30ee8ad..c2abb14 100644
--- a/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db.md
+++ b/_art/2026-09-19-photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db-00.md
@@ -1,7 +1,7 @@
---
layout: post
title: "photo-260cc99527c13f256d4ea39d0959f5daa942c888ea882852e73fd94f6c30b7db"
-date: "2026-09-19T12:10:40.964254+00:00"
+date: "2026-09-19T12:29:32.999986+00:00"
categories: art
---
diff --git a/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c.md b/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c-00.md
similarity index 89%
rename from _art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c.md
rename to _art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c-00.md
index ae50f4a..ec5c4f3 100644
--- a/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c.md
+++ b/_art/2026-09-19-photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c-00.md
@@ -1,7 +1,7 @@
---
layout: post
title: "photo-474ab3283ce8801ac6e59b67b65b4169167053873a5a8db4a15b840a3448266c"
-date: "2026-09-19T12:29:33.758101+00:00"
+date: "2026-09-19T12:29:32.999994+00:00"
categories: art
---
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00.md
new file mode 100644
index 0000000..2da03d0
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00.md
@@ -0,0 +1,9 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-00"
+date: "2026-09-19T12:29:32.999993+00:00"
+categories: art
+anchor: "Romania"
+---
+
+

diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01.md
new file mode 100644
index 0000000..c92ec89
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-01"
+date: "2026-09-19T12:29:32.999992+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02.md
new file mode 100644
index 0000000..50d2481
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-02"
+date: "2026-09-19T12:29:32.999984+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03.md
new file mode 100644
index 0000000..50817d0
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-03"
+date: "2026-09-19T12:29:32.999983+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04.md
new file mode 100644
index 0000000..5727132
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-04"
+date: "2026-09-19T12:29:32.999995+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05.md
new file mode 100644
index 0000000..0eaf6a9
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-05"
+date: "2026-09-19T12:29:32.999996+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06.md
new file mode 100644
index 0000000..67c1fd9
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-06"
+date: "2026-09-19T12:29:32.999997+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07.md
new file mode 100644
index 0000000..4dee853
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07.md
@@ -0,0 +1,9 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-07"
+date: "2026-09-19T12:29:32.999998+00:00"
+categories: art
+---
+
+
+Transmission of Flesh by Erfan Ashourioun
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08.md
new file mode 100644
index 0000000..ffe7201
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-08"
+date: "2026-09-19T12:29:32.999999+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09.md
new file mode 100644
index 0000000..d869d87
--- /dev/null
+++ b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d-09.md
@@ -0,0 +1,9 @@
+---
+layout: post
+title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"
+date: "2026-09-19T12:29:33+00:00"
+categories: art
+anchor: "Armenia"
+---
+
+
diff --git a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d.md b/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d.md
deleted file mode 100644
index 531aeeb..0000000
--- a/_art/2026-09-19-photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-layout: post
-title: "photo-7c832afa1b73023d8f2391ff40c221be9ffd468a1a96a6bf4900cbf95595d31d"
-date: "2026-09-19T12:27:27.038898+00:00"
-categories: art
----
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673.md b/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673-00.md
similarity index 89%
rename from _art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673.md
rename to _art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673-00.md
index f8ed6b3..c38d5bf 100644
--- a/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673.md
+++ b/_art/2026-09-19-photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673-00.md
@@ -1,7 +1,7 @@
---
layout: post
title: "photo-b18a2f1c7127077c801160782012cb164759f0b9dd41762205b45e4c9a07d673"
-date: "2026-09-19T12:03:11.135424+00:00"
+date: "2026-09-19T12:29:32.999985+00:00"
categories: art
---
diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-00.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-00.md
new file mode 100644
index 0000000..0cc3f80
--- /dev/null
+++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-00.md
@@ -0,0 +1,10 @@
+---
+layout: post
+title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"
+date: "2026-09-19T12:29:32.999991+00:00"
+categories: art
+---
+
+
+
+
diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01.md
new file mode 100644
index 0000000..c55b96f
--- /dev/null
+++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-01"
+date: "2026-09-19T12:29:32.999990+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02.md
new file mode 100644
index 0000000..3f578c6
--- /dev/null
+++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-02"
+date: "2026-09-19T12:29:32.999989+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03.md
new file mode 100644
index 0000000..ac93ea0
--- /dev/null
+++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-03"
+date: "2026-09-19T12:29:32.999988+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04.md
new file mode 100644
index 0000000..e4e0071
--- /dev/null
+++ b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b-04"
+date: "2026-09-19T12:29:32.999987+00:00"
+categories: art
+---
+
+
diff --git a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b.md b/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b.md
deleted file mode 100644
index 4afb7f6..0000000
--- a/_art/2026-09-19-photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-layout: post
-title: "photo-c53f20236f166632bff163339c8e789cc09b0d7e63c5b531b1fc20705460975b"
-date: "2026-09-19T12:26:31.454056+00:00"
-categories: art
----
-
-
-
-
-
-
-
-
diff --git a/photo-upload/README.md b/photo-upload/README.md
index 678510a..1ccae1f 100644
--- a/photo-upload/README.md
+++ b/photo-upload/README.md
@@ -150,10 +150,13 @@ Create **Publish to blog**:
only `published` means the deployed site's receipt contains this album.
The ZIP route accepts 1–20 images in archive-entry order and keeps them as one
-upload. Each photo renders as its own gallery ``, including photos in older
-batches. The caption appears on each photo; the first image is the highlight target. Metadata sidecars
-under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API clients can also send
-repeated `photos` parts; send either `archive` or `photos`, never both.
+upload job. Each photo is stored as its own `_art` document and renders as its
+own gallery ``. The shared caption is repeated on each photo and an optional
+highlight begins on the first photo. Photos remain contiguous in the selection order.
+Older batch documents are still split at render time for backward compatibility.
+Metadata sidecars under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API
+clients can also send repeated `photos` parts; send either `archive` or `photos`,
+never both.
Select still photos; videos/Live Photo video components aren't supported. Dates
are upload time in UTC; captions are plain text. A caption is optional.
An optional highlight name (up to 200 characters) adds one link to **Highlights**,
diff --git a/photo-upload/app.py b/photo-upload/app.py
index ba2f3ff..67d6c6b 100644
--- a/photo-upload/app.py
+++ b/photo-upload/app.py
@@ -10,7 +10,7 @@ from pathlib import Path
import re
import shutil
import tempfile
-from datetime import datetime, timezone
+from datetime import datetime, timedelta, timezone
import warnings
import zipfile
@@ -79,6 +79,26 @@ def web_image(raw, destination):
Image.Resampling.LANCZOS)
+def photo_entry(job, date, index, count, caption, highlight):
+ # Give each photo its own collection document. Descending dates preserve the
+ # selection order because the gallery renders the newest document first.
+ item_date = datetime.fromisoformat(date) + timedelta(microseconds=count - index)
+ title = f"photo-{job}" if index == 0 else f"photo-{job}-{index:02}"
+ content = ["---", "layout: post", f'title: "{title}"',
+ f'date: "{item_date.isoformat()}"', "categories: art"]
+ if index == 0 and highlight:
+ content.append(f"anchor: {json.dumps(highlight)}")
+ content.extend(["---", ""])
+ if index == 0 and not highlight:
+ content.extend([f'', ""])
+ content.append(f'
')
+ if caption:
+ content.append('' +
+ public_text(caption).replace("\n", "
") + "")
+ return "\n".join(content) + "\n"
+
+
def create_app(data_dir=None, token=None):
app = Flask(__name__)
app.config.update(MAX_CONTENT_LENGTH=100 * 1024 * 1024,
@@ -261,21 +281,15 @@ def create_app(data_dir=None, token=None):
if kind == "snippet":
manifest.update(kind=kind, text=text)
atomic_json(temporary / "manifest.json", manifest)
- content = ["---", "layout: post", f'title: "photo-{job}"',
- f'date: "{date}"', "categories: art"]
- if highlight:
- content.append(f"anchor: {json.dumps(highlight)}")
- content.extend(["---", ""])
- # The gallery already renders an anchor before the first photo for highlights.
- if not highlight:
- content.extend([f'', ""])
- for index in range(len(photos)):
- content.append(f'
')
- if caption:
- content.append('' +
- public_text(caption).replace("\n", "
") + "")
- if kind == "snippet":
+ if kind == "photo":
+ (temporary / "entries").mkdir()
+ for index in range(len(photos)):
+ (temporary / "entries" / f"{index:02}.md").write_text(
+ photo_entry(job, date, index, len(photos), caption, highlight))
+ # Retain the old filename for compatibility with publisher versions
+ # that predate one-document-per-photo submissions.
+ content = [(temporary / "entries/00.md").read_text()]
+ else:
content = [f'', ""]
for paragraph in (text, caption):
if paragraph:
diff --git a/photo-upload/publisher.py b/photo-upload/publisher.py
index 3446570..22a14f0 100644
--- a/photo-upload/publisher.py
+++ b/photo-upload/publisher.py
@@ -86,9 +86,20 @@ class Publisher:
entry = Path("_art") / f"{manifest['date'][:10]}-photo-{job}.md"
images = Path("img/arts/uploads") / job
(self.repo / entry).parent.mkdir(parents=True, exist_ok=True)
- shutil.copyfile(submission / "entry.md", self.repo / entry)
+ entries = submission / "entries"
+ if entries.is_dir():
+ # Remove the former batch document when a queued submission
+ # is republished by a newer service version.
+ self.git("rm", "--ignore-unmatch", "--", str(entry))
+ for source in sorted(entries.glob("*.md")):
+ destination = Path("_art") / f"{manifest['date'][:10]}-photo-{job}-{source.stem}.md"
+ shutil.copyfile(source, self.repo / destination)
+ self.git("add", "--", str(destination))
+ else:
+ shutil.copyfile(submission / "entry.md", self.repo / entry)
+ self.git("add", "--", str(entry))
shutil.copytree(submission / "images", self.repo / images, dirs_exist_ok=True)
- self.git("add", "--", str(entry), str(images))
+ self.git("add", "--", str(images))
if self.git("diff", "--cached", "--name-only"):
self.git("commit", "-m", f"Publish {len(jobs)} submission(s)")
self.git("push", "origin", f"HEAD:refs/heads/{self.branch}")
diff --git a/photo-upload/tests/test_upload.py b/photo-upload/tests/test_upload.py
index 93cae9a..c86a4b4 100644
--- a/photo-upload/tests/test_upload.py
+++ b/photo-upload/tests/test_upload.py
@@ -116,10 +116,12 @@ def test_album_caption_order_and_retry_deduplication(service):
assert response.status_code == 202
job = response.json["id"]
folder = data / "submissions" / job
- entry = (folder / "entry.md").read_text()
- assert "