release: v0.17.0 (#677)
See CHANGELOG.md, section [0.17.0] — 2026-09-23.
This commit is contained in:
@@ -2,7 +2,15 @@ import { notFound } from 'next/navigation';
|
||||
import { cookies } from 'next/headers';
|
||||
import type { Metadata } from 'next';
|
||||
import { readJournalEntry, listJournalEntries } from '@/lib/admin/journal-service';
|
||||
import type { ParsedJournal } from '@/lib/journal';
|
||||
import {
|
||||
collectAssetIds,
|
||||
mapBlockAssetIds,
|
||||
type JournalBlock,
|
||||
type MapPin,
|
||||
type ParsedJournal,
|
||||
} from '@/lib/journal';
|
||||
import { entryMapPins } from '@/lib/journalMap';
|
||||
import { expandAlbumBlocks } from '@/lib/journalAlbum';
|
||||
import { isAdminAuthenticated } from '@/lib/admin/auth';
|
||||
import { isAuthenticated } from '@/lib/auth';
|
||||
import { journalNeighbours } from '@/lib/journalNav';
|
||||
@@ -114,7 +122,7 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { frontmatter, blocks, referencedAssetIds } = entry.parsed;
|
||||
const { frontmatter, blocks: authoredBlocks, referencedAssetIds } = entry.parsed;
|
||||
const isAuthedAdmin = await isAdminAuthenticated();
|
||||
|
||||
// If draft, only visible to authenticated admin
|
||||
@@ -164,14 +172,45 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
|
||||
}
|
||||
});
|
||||
|
||||
const rawAssets = (await Promise.all(assetPromises)).filter((a): a is ImmichAsset => a !== null);
|
||||
const fetchedAssets = (await Promise.all(assetPromises)).filter(
|
||||
(a): a is ImmichAsset => a !== null,
|
||||
);
|
||||
|
||||
// Album blocks become photo blocks here. The raw fetch bypasses the album
|
||||
// allowlist on purpose: an entry may already show any single photo by id,
|
||||
// and the author's pick is the gate for a whole album just the same. An
|
||||
// album Immich cannot deliver drops its block rather than the page.
|
||||
const albumAssetsById = new Map<string, ImmichAsset[]>();
|
||||
for (const block of authoredBlocks) {
|
||||
if (block.type !== 'album' || !block.albumId || albumAssetsById.has(block.albumId)) continue;
|
||||
try {
|
||||
albumAssetsById.set(block.albumId, await immich.getAlbumAssetsRaw(block.albumId));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[journal] ${slug}: album ${block.albumId} could not be loaded:`, error);
|
||||
}
|
||||
}
|
||||
const { blocks } = expandAlbumBlocks(
|
||||
authoredBlocks,
|
||||
(albumId) => albumAssetsById.get(albumId),
|
||||
config.albumManualOrders,
|
||||
);
|
||||
const knownIds = new Set(fetchedAssets.map((a) => a.id));
|
||||
const rawAssets = [...fetchedAssets];
|
||||
for (const list of albumAssetsById.values()) {
|
||||
for (const asset of list) {
|
||||
if (knownIds.has(asset.id)) continue;
|
||||
knownIds.add(asset.id);
|
||||
rawAssets.push(asset);
|
||||
}
|
||||
}
|
||||
|
||||
// EssayView drops photo blocks it cannot resolve, which is right for visitors
|
||||
// but leaves no trace of *why* a photo vanished. Legacy positional references
|
||||
// ("1", "2") are the usual cause: they only resolved against a subpage album,
|
||||
// and a standalone journal entry has none.
|
||||
if (rawAssets.length < referencedAssetIds.length) {
|
||||
const resolved = new Set(rawAssets.map((a) => a.id));
|
||||
if (fetchedAssets.length < referencedAssetIds.length) {
|
||||
const resolved = new Set(fetchedAssets.map((a) => a.id));
|
||||
const missing = referencedAssetIds.filter((id) => !resolved.has(id));
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
@@ -191,6 +230,28 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
|
||||
const tokenByAssetId = new Map(rawAssets.map((a) => [a.id, encodeAssetId(a.id)]));
|
||||
const toToken = (assetId: string) => tokenByAssetId.get(assetId) ?? '';
|
||||
|
||||
// A map block's pins are computed here, from the photos fetched above and
|
||||
// under each photo's `location:` precision, and only when the site
|
||||
// publishes a map at all. With the map switched off the block is dropped
|
||||
// before it can reach the client; with it on, the client gets pins only —
|
||||
// never the items, which carry raw asset ids.
|
||||
const entryPhotoIds = collectAssetIds(blocks.filter((b) => b.type !== 'map'));
|
||||
const pinsByBlock = new Map<JournalBlock, MapPin[]>();
|
||||
if (config.map) {
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'map') {
|
||||
pinsByBlock.set(block, await entryMapPins(block.items, rawAssets, entryPhotoIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Photos that only anchor a map pin are fetched for their EXIF but are not
|
||||
// part of the story, so they stay out of the lightbox sequence.
|
||||
const shownAssetIds = new Set([
|
||||
...entryPhotoIds,
|
||||
...(frontmatter.coverAssetId ? [frontmatter.coverAssetId] : []),
|
||||
]);
|
||||
|
||||
const essayForClient: ParsedJournal = {
|
||||
// Named fields, not a spread of `frontmatter`: EssayView reads only
|
||||
// title, subtitle, coverAssetId, author and date, but a spread put the
|
||||
@@ -203,23 +264,20 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
|
||||
date: frontmatter.date,
|
||||
coverAssetId: frontmatter.coverAssetId ? toToken(frontmatter.coverAssetId) : undefined,
|
||||
},
|
||||
blocks: blocks.map((block) => {
|
||||
if (block.type === 'photo') {
|
||||
return { ...block, assetId: toToken(block.assetId) };
|
||||
blocks: blocks.flatMap((block): JournalBlock[] => {
|
||||
if (block.type === 'map') {
|
||||
const pins = pinsByBlock.get(block);
|
||||
return pins
|
||||
? [{ type: 'map', caption: block.caption, line: block.line, items: [], pins }]
|
||||
: [];
|
||||
}
|
||||
if (block.type === 'photo-pair') {
|
||||
return {
|
||||
...block,
|
||||
assetIds: [toToken(block.assetIds[0]), toToken(block.assetIds[1])] as [string, string],
|
||||
};
|
||||
}
|
||||
return block;
|
||||
return [mapBlockAssetIds(block, toToken)];
|
||||
}),
|
||||
referencedAssetIds: rawAssets.map((a) => encodeAssetId(a.id)),
|
||||
};
|
||||
|
||||
const images: PhotoItem[] = rawAssets
|
||||
.filter((a) => a.type === 'IMAGE' || a.type === 'VIDEO')
|
||||
.filter((a) => shownAssetIds.has(a.id) && (a.type === 'IMAGE' || a.type === 'VIDEO'))
|
||||
.map((a) => {
|
||||
const ph = assetPlaceholder(a);
|
||||
const exif =
|
||||
|
||||
Reference in New Issue
Block a user