Leverage adjacent meta for about description

This commit is contained in:
Sam Becker 2026-03-01 22:36:11 -06:00
parent be859f380e
commit 8569347c27
6 changed files with 84 additions and 77 deletions

View File

@ -1,12 +1,11 @@
import AdminAboutEditPage from '@/about/AdminAboutEditPage'; import AdminAboutEditPage from '@/about/AdminAboutEditPage';
import { getAbout } from '@/about/query'; import { getAboutData } from '@/about/data';
import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config'; import { PRESERVE_ORIGINAL_UPLOADS } from '@/app/config';
import { feedQueryOptions } from '@/feed'; import { feedQueryOptions } from '@/feed';
import { import {
getPhotosCached, getPhotosCached,
getPhotosMetaCached, getPhotosMetaCached,
} from '@/photo/cache'; } from '@/photo/cache';
import { getPhoto } from '@/photo/query';
import { TAG_FAVS } from '@/tag'; import { TAG_FAVS } from '@/tag';
const PHOTO_CHOOSER_QUERY_OPTIONS = feedQueryOptions({ const PHOTO_CHOOSER_QUERY_OPTIONS = feedQueryOptions({
@ -25,24 +24,7 @@ export default async function AboutEditPage() {
photosCount, photosCount,
photosFavs, photosFavs,
] = await Promise.all([ ] = await Promise.all([
getAbout() getAboutData()
.then(async about => {
const photoAvatar = about?.photoIdAvatar
? await getPhoto(about?.photoIdAvatar ?? '', true)
.catch(() => undefined)
: undefined;
const photoHero = about?.photoIdHero
? await getPhoto(about?.photoIdHero ?? '', true)
.catch(() => undefined)
: undefined;
return {
about,
photoAvatar,
photoHero,
};
})
.catch(() => ({ .catch(() => ({
about: undefined, about: undefined,
photoAvatar: undefined, photoAvatar: undefined,

View File

@ -1,5 +1,6 @@
import { getDescriptionWithFallback } from '@/about';
import AboutPageClient from '@/about/AboutPageClient'; import AboutPageClient from '@/about/AboutPageClient';
import { getAboutCached } from '@/about/cache'; import { getAboutDataCached } from '@/about/data';
import { SHOW_ABOUT_PAGE } from '@/app/config'; import { SHOW_ABOUT_PAGE } from '@/app/config';
import { PATH_ROOT } from '@/app/path'; import { PATH_ROOT } from '@/app/path';
import { getDataForCategoriesCached } from '@/category/cache'; import { getDataForCategoriesCached } from '@/category/cache';
@ -7,11 +8,7 @@ import {
getLastModifiedForCategories, getLastModifiedForCategories,
NULL_CATEGORY_DATA, NULL_CATEGORY_DATA,
} from '@/category/data'; } from '@/category/data';
import { import { getPhotosMetaCached } from '@/photo/cache';
getPhotoCached,
getPhotosCached,
getPhotosMetaCached,
} from '@/photo/cache';
import PhotosEmptyState from '@/photo/PhotosEmptyState'; import PhotosEmptyState from '@/photo/PhotosEmptyState';
import { getAllPhotoIdsWithUpdatedAt } from '@/photo/query'; import { getAllPhotoIdsWithUpdatedAt } from '@/photo/query';
import { TAG_FAVS } from '@/tag'; import { TAG_FAVS } from '@/tag';
@ -34,26 +31,8 @@ export default async function AboutPage() {
photos, photos,
categories, categories,
] = await Promise.all([ ] = await Promise.all([
getAboutCached() getAboutDataCached()
.then(async about => { .catch(() => ({
const photoAvatar = await (about?.photoIdAvatar
? getPhotoCached(about?.photoIdAvatar ?? '', true)
: undefined);
const photoHero = await (about?.photoIdHero
? getPhotoCached(about?.photoIdHero ?? '', true)
// Fall back to favorite photos if no hero photo is set
: getPhotosCached({ tag: TAG_FAVS, limit: 1 })
.then(photos => photos.length > 0
? photos[0]
// Fall back to oldest photo if no favorite photos exist
: getPhotosCached({ limit: 1, sortBy: 'takenAtAsc' })
.then(photos => photos[0])));
return {
about,
photoAvatar,
photoHero,
};
}).catch(() => ({
about: undefined, about: undefined,
photoAvatar: undefined, photoAvatar: undefined,
photoHero: undefined, photoHero: undefined,
@ -63,10 +42,15 @@ export default async function AboutPage() {
getDataForCategoriesCached().catch(() => (NULL_CATEGORY_DATA)), getDataForCategoriesCached().catch(() => (NULL_CATEGORY_DATA)),
]); ]);
const description = about?.description const description = getDescriptionWithFallback(about);
? <div dangerouslySetInnerHTML={{
__html: safelyParseFormattedHtml(about.description), const descriptionHtml = description
}} /> ? <div
className="text-medium [&>*>a]:underline"
dangerouslySetInnerHTML={{
__html: safelyParseFormattedHtml(description),
}}
/>
: undefined; : undefined;
const { const {
@ -88,7 +72,7 @@ export default async function AboutPage() {
? <AboutPageClient ? <AboutPageClient
title={about?.title} title={about?.title}
subhead={about?.subhead} subhead={about?.subhead}
description={description} descriptionHtml={descriptionHtml}
photosCount={photosMeta?.count} photosCount={photosMeta?.count}
photosOldest={photosMeta?.dateRange?.start} photosOldest={photosMeta?.dateRange?.start}
photoAvatar={photoAvatar} photoAvatar={photoAvatar}

View File

@ -28,7 +28,7 @@ import AdminEmptyState from '@/admin/AdminEmptyState';
export default function AboutPageClient({ export default function AboutPageClient({
title, title,
subhead, subhead,
description, descriptionHtml,
photosCount = 0, photosCount = 0,
photosOldest, photosOldest,
photoAvatar, photoAvatar,
@ -43,7 +43,7 @@ export default function AboutPageClient({
}: { }: {
title?: string title?: string
subhead?: string subhead?: string
description?: ReactNode descriptionHtml?: ReactNode
photosCount?: number photosCount?: number
photosOldest?: string photosOldest?: string
photoAvatar?: Photo photoAvatar?: Photo
@ -178,26 +178,24 @@ export default function AboutPageClient({
</div> </div>
{isUserSignedIn && <AdminAboutMenu />} {isUserSignedIn && <AdminAboutMenu />}
</div> </div>
{description {descriptionHtml
? <div className="text-medium [&>*>a]:underline"> ? descriptionHtml
{description}
</div>
: isUserSignedIn && : isUserSignedIn &&
<Link <Link
href={PATH_ADMIN_ABOUT_EDIT} href={PATH_ADMIN_ABOUT_EDIT}
className={clsx( className={clsx(
'flex items-center justify-center gap-2.5', 'flex items-center justify-center gap-2.5',
'border border-dashed border-medium rounded-lg', 'border border-dashed border-medium rounded-lg',
)} )}
>
<AdminEmptyState
icon={<LuCirclePlus size={22} />}
includeContainer={false}
className="gap-3! p-6!"
> >
Add optional description <AdminEmptyState
</AdminEmptyState> icon={<LuCirclePlus size={22} />}
</Link>} includeContainer={false}
className="gap-3! p-6!"
>
Add optional description
</AdminEmptyState>
</Link>}
<AnimateItems <AnimateItems
className={clsx( className={clsx(
'grid gap-x-2 gap-y-6 grid-cols-2 lg:grid-cols-4', 'grid gap-x-2 gap-y-6 grid-cols-2 lg:grid-cols-4',

View File

@ -3,7 +3,7 @@
import { PATH_ABOUT } from '@/app/path'; import { PATH_ABOUT } from '@/app/path';
import LinkWithStatus from '@/components/LinkWithStatus'; import LinkWithStatus from '@/components/LinkWithStatus';
import { useState } from 'react'; import { useState } from 'react';
import { About, AboutInsert } from '.'; import { About, AboutInsert, getDescriptionWithFallback } from '.';
import FieldsetWithStatus from '@/components/FieldsetWithStatus'; import FieldsetWithStatus from '@/components/FieldsetWithStatus';
import AdminChildPage from '@/components/AdminChildPage'; import AdminChildPage from '@/components/AdminChildPage';
import { updateAboutAction } from './actions'; import { updateAboutAction } from './actions';
@ -32,8 +32,6 @@ export default function AdminAboutEditPage({
const [aboutForm, setAboutForm] = useState<Partial<AboutInsert>>(about ?? {}); const [aboutForm, setAboutForm] = useState<Partial<AboutInsert>>(about ?? {});
const convertUrlToPhotoId = (url?: string) => url?.split('/').pop();
return ( return (
<AdminChildPage <AdminChildPage
backPath={PATH_ABOUT} backPath={PATH_ABOUT}
@ -48,7 +46,7 @@ export default function AdminAboutEditPage({
<FieldsetPhotoChooser <FieldsetPhotoChooser
id="photoIdAvatar" id="photoIdAvatar"
label="Avatar" label="Avatar"
value={aboutForm?.photoIdAvatar ?? ''} value={aboutForm?.photoIdAvatar || photoAvatar?.id || ''}
onChange={photoIdAvatar => setAboutForm(form => onChange={photoIdAvatar => setAboutForm(form =>
({ ...form, photoIdAvatar }))} ({ ...form, photoIdAvatar }))}
photo={photoAvatar} photo={photoAvatar}
@ -74,15 +72,16 @@ export default function AdminAboutEditPage({
label="Description" label="Description"
type="textarea" type="textarea"
value={aboutForm?.description ?? ''} value={aboutForm?.description ?? ''}
placeholder={getDescriptionWithFallback(about)}
onChange={description => setAboutForm(form => onChange={description => setAboutForm(form =>
({ ...form, description }))} ({ ...form, description }))}
/> />
<FieldsetPhotoChooser <FieldsetPhotoChooser
id="photoIdHero" id="photoIdHero"
label="Hero" label="Hero"
value={aboutForm?.photoIdHero ?? ''} value={aboutForm?.photoIdHero || photoHero?.id || ''}
onChange={photoIdHero => setAboutForm(form => onChange={photoIdHero => setAboutForm(form =>
({ ...form, photoIdHero: convertUrlToPhotoId(photoIdHero) }))} ({ ...form, photoIdHero }))}
photo={photoHero} photo={photoHero}
photos={photos} photos={photos}
photosCount={photosCount} photosCount={photosCount}

37
src/about/data.ts Normal file
View File

@ -0,0 +1,37 @@
import { getPhotoCached, getPhotosCached } from '@/photo/cache';
import { About } from '.';
import { TAG_FAVS } from '@/tag';
import { getAbout } from './query';
import { getAboutCached } from './cache';
const getAboutAvatar = (about?: About) =>
about?.photoIdAvatar
? getPhotoCached(about?.photoIdAvatar ?? '', true)
: undefined;
const getAboutHero = (about?: About) =>
about?.photoIdHero
? getPhotoCached(about?.photoIdHero ?? '', true)
// Fall back to favorite photos if no hero photo is set
: getPhotosCached({ tag: TAG_FAVS, limit: 1 })
.then(photos => photos.length > 0
? photos[0]
// Fall back to oldest photo if no favorite photos exist
: getPhotosCached({ limit: 1, sortBy: 'takenAtAsc' })
.then(photos => photos[0]));
export const getAboutData = () =>
getAbout()
.then(async about => ({
about,
photoAvatar: await getAboutAvatar(about),
photoHero: await getAboutHero(about),
}));
export const getAboutDataCached = () =>
getAboutCached()
.then(async about => ({
about,
photoAvatar: await getAboutAvatar(about),
photoHero: await getAboutHero(about),
}));

View File

@ -1,3 +1,5 @@
import { META_DESCRIPTION, SIDEBAR_TEXT } from '@/app/config';
export interface AboutInsert { export interface AboutInsert {
id: number id: number
title?: string title?: string
@ -11,3 +13,8 @@ export interface About extends AboutInsert {
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
export const getDescriptionWithFallback = (about?: About) =>
about?.description ||
META_DESCRIPTION ||
SIDEBAR_TEXT;