From ec698b61de447b120f9236d2b10958dbc4abb912 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 12 Jun 2025 16:31:45 -0500 Subject: [PATCH] Begin combining feed and api --- app/feed.json/route.ts | 4 +- src/app/feed.ts | 140 ++++++++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/app/feed.json/route.ts b/app/feed.json/route.ts index 8b8f64f8..3422017e 100644 --- a/app/feed.json/route.ts +++ b/app/feed.json/route.ts @@ -1,11 +1,11 @@ import { getPhotosCached } from '@/photo/cache'; import { INFINITE_SCROLL_FEED_INITIAL } from '@/photo'; -import { formatPhotoForFeed } from '@/app/feed'; import { BASE_URL, PUBLIC_FEED_ENABLED, META_TITLE, } from '@/app/config'; +import { formatPhotoForFeedJson } from '@/app/feed'; export const dynamic = 'force-static'; @@ -20,7 +20,7 @@ export async function GET() { title: META_TITLE, url: BASE_URL, }, - photos: photos.map(formatPhotoForFeed), + photos: photos.map(formatPhotoForFeedJson), }); } else { return new Response('Feed disabled', { status: 404 }); diff --git a/src/app/feed.ts b/src/app/feed.ts index 5ccbe104..ba16e94f 100644 --- a/src/app/feed.ts +++ b/src/app/feed.ts @@ -1,17 +1,31 @@ -import { Photo } from '@/photo'; +import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo'; import { absolutePathForPhoto } from './paths'; -import { getNextImageUrlForRequest } from '@/platforms/next-image'; -import { formatDate } from '@/utility/date'; +import { + getNextImageUrlForRequest, + NextImageSize, +} from '@/platforms/next-image'; +import { formatDate, formatDateFromPostgresString } from '@/utility/date'; -export const FEED_PHOTO_WIDTH_CONTENT = 1080; -export const FEED_PHOTO_WIDTH_THUMB = 640; +export const API_PHOTO_REQUEST_LIMIT = 40; -export interface PublicFeed { +export const FEED_PHOTO_WIDTH_SMALL = 200; +export const FEED_PHOTO_WIDTH_MEDIUM = 640; +export const FEED_PHOTO_WIDTH_LARGE = 1200; + +export interface PublicFeedJson { meta: { title: string url: string } - photos: PublicFeedPhoto[] + photos: PublicFeedPhotoJson[] +} + +export interface PublicFeedRss { + meta: { + title: string + url: string + } + photos: PublicFeedPhotoRss[] } interface PublicFeedMedia { @@ -20,7 +34,21 @@ interface PublicFeedMedia { height: number } -interface PublicFeedPhoto { +interface PublicFeedPhotoJson { + id: string + title?: string + url: string + make?: string + model?: string + tags?: string[] + takenAtNaive: string + src: Record< + 'small' | 'medium' | 'large', + PublicFeedMedia + > +} + +interface PublicFeedPhotoRss { id: string title?: string description?: string @@ -32,56 +60,66 @@ interface PublicFeedPhoto { > } -export const formatPhotoForFeed = (photo: Photo): PublicFeedPhoto => ({ +const generateFeedMedia = ( + photo: Photo, + size: NextImageSize, +): PublicFeedMedia => ({ + url: getNextImageUrlForRequest({ imageUrl: photo.url, size }), + width: size, + height: Math.round(size / photo.aspectRatio), +}); + +const getCoreFeedFields = (photo: Photo) => ({ id: photo.id, - title: photo.title, - description: photo.caption, - link: absolutePathForPhoto({ photo }), - publicationDate: photo.createdAt, - media: { - content: { - url: getNextImageUrlForRequest({ - imageUrl: photo.url, - size: FEED_PHOTO_WIDTH_CONTENT, - }), - width: FEED_PHOTO_WIDTH_CONTENT, - height: Math.round(FEED_PHOTO_WIDTH_CONTENT / photo.aspectRatio), - }, - thumb: { - url: getNextImageUrlForRequest({ - imageUrl: photo.url, - size: FEED_PHOTO_WIDTH_THUMB, - }), - width: FEED_PHOTO_WIDTH_THUMB, - height: Math.round(FEED_PHOTO_WIDTH_THUMB / photo.aspectRatio), - }, + title: titleForPhoto(photo), + description: descriptionForPhoto(photo), +}); + +export const formatPhotoForFeedJson = (photo: Photo): PublicFeedPhotoJson => ({ + ...getCoreFeedFields(photo), + url: absolutePathForPhoto({ photo }), + ...photo.make && { make: photo.make }, + ...photo.model && { model: photo.model }, + ...photo.tags.length > 0 && { tags: photo.tags }, + takenAtNaive: formatDateFromPostgresString(photo.takenAtNaive), + src: { + small: generateFeedMedia(photo, FEED_PHOTO_WIDTH_SMALL), + medium: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM), + large: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE), }, }); -export const feedPhotoToXml = (photo: PublicFeedPhoto): string => { +export const formatPhotoForFeedRss = (photo: Photo): PublicFeedPhotoRss => ({ + ...getCoreFeedFields(photo), + link: absolutePathForPhoto({ photo }), + publicationDate: photo.createdAt, + media: { + content: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE), + thumb: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM), + }, +}); + +export const feedPhotoToXml = (photo: PublicFeedPhotoRss): string => { const formattedDate = formatDate({ date: photo.publicationDate, length: 'rss', }); - const description = photo.description ? - ` + return ` + ${photo.title} + ${photo.link} + ${formattedDate} + ${photo.link} + - ` : ''; - - return ` - ${photo.title} - ${photo.link} - ${formattedDate} - ${photo.link} - ${description} - - - - `; + + + + + `; };