From 4d904517a58f906c6dc6b3e0fda35b934de9f88d Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Thu, 12 Jun 2025 21:07:49 -0500 Subject: [PATCH] Create feed module --- app/feed.json/route.ts | 3 +- app/rss.xml/route.ts | 3 +- src/app/feed.ts | 116 ----------------------------------------- src/feed/index.ts | 32 ++++++++++++ src/feed/json.ts | 36 +++++++++++++ src/feed/rss.ts | 59 +++++++++++++++++++++ 6 files changed, 131 insertions(+), 118 deletions(-) delete mode 100644 src/app/feed.ts create mode 100644 src/feed/index.ts create mode 100644 src/feed/json.ts create mode 100644 src/feed/rss.ts diff --git a/app/feed.json/route.ts b/app/feed.json/route.ts index 19f83e2f..156ddb42 100644 --- a/app/feed.json/route.ts +++ b/app/feed.json/route.ts @@ -4,7 +4,8 @@ import { SITE_FEEDS_ENABLED, META_TITLE, } from '@/app/config'; -import { FEED_PHOTO_REQUEST_LIMIT, formatPhotoForFeedJson } from '@/app/feed'; +import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed'; +import { formatPhotoForFeedJson } from '@/feed/json'; // Cache for 24 hours export const revalidate = 86_400; diff --git a/app/rss.xml/route.ts b/app/rss.xml/route.ts index 4f67951e..25a0a952 100644 --- a/app/rss.xml/route.ts +++ b/app/rss.xml/route.ts @@ -5,7 +5,8 @@ import { META_TITLE, SITE_FEEDS_ENABLED, } from '@/app/config'; -import { createRssItems, FEED_PHOTO_REQUEST_LIMIT } from '@/app/feed'; +import { FEED_PHOTO_REQUEST_LIMIT } from '@/feed'; +import { createRssItems } from '@/feed/rss'; import { ABSOLUTE_PATH_FOR_RSS_XML } from '@/app/paths'; // Cache for 24 hours diff --git a/src/app/feed.ts b/src/app/feed.ts deleted file mode 100644 index 3395c7b0..00000000 --- a/src/app/feed.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo'; -import { absolutePathForPhoto } from './paths'; -import { - getNextImageUrlForRequest, - NextImageSize, -} from '@/platforms/next-image'; -import { formatDate, formatDateFromPostgresString } from '@/utility/date'; -import { formatStringForXml } from '@/utility/string'; - -export const FEED_PHOTO_REQUEST_LIMIT = 40; - -export const FEED_PHOTO_WIDTH_SMALL = 200; -export const FEED_PHOTO_WIDTH_MEDIUM = 640; -export const FEED_PHOTO_WIDTH_LARGE = 1200; - -interface FeedMedia { - url: string - width: number - height: number -} - -interface FeedPhotoJson { - id: string - title: string - url: string - make?: string - model?: string - tags?: string[] - takenAtNaive: string - src: Record<'small' | 'medium' | 'large', FeedMedia> -} - -interface FeedPhotoRss { - id: string - title: string - description?: string - link: string - pubDate: Date - media: Record<'content' | 'thumb', FeedMedia> -} - -export interface FeedJson { - meta: { - title: string - url: string - } - photos: FeedPhotoJson[] -} - -const generateFeedMedia = ( - photo: Photo, - size: NextImageSize, -): FeedMedia => ({ - url: getNextImageUrlForRequest({ imageUrl: photo.url, size }), - width: size, - height: Math.round(size / photo.aspectRatio), -}); - -const getCoreFeedFields = (photo: Photo) => ({ - id: photo.id, - title: titleForPhoto(photo), - description: descriptionForPhoto(photo, true), -}); - -export const formatPhotoForFeedJson = (photo: Photo): FeedPhotoJson => ({ - ...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), - }, -}); - -const formatPhotoForFeedRss = (photo: Photo): FeedPhotoRss => ({ - ...getCoreFeedFields(photo), - link: absolutePathForPhoto({ photo }), - pubDate: photo.createdAt, - media: { - content: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE), - thumb: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM), - }, -}); - -const feedPhotoToXml = (photo: FeedPhotoRss): string => { - const formattedDate = formatDate({ date: photo.pubDate, length: 'rss' }); - return ` - ${photo.title} - ${photo.link} - ${formattedDate} - ${photo.link} - ${photo.description - ? `` - : ''} - - - - `; -}; - -export const createRssItems = (photos: Photo[]) => - photos.map(formatPhotoForFeedRss).map(feedPhotoToXml); diff --git a/src/feed/index.ts b/src/feed/index.ts new file mode 100644 index 00000000..97c4ae1e --- /dev/null +++ b/src/feed/index.ts @@ -0,0 +1,32 @@ +import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo'; +import { + getNextImageUrlForRequest, + NextImageSize, +} from '@/platforms/next-image'; + +export const FEED_PHOTO_REQUEST_LIMIT = 40; + +export const FEED_PHOTO_WIDTH_SMALL = 200; +export const FEED_PHOTO_WIDTH_MEDIUM = 640; +export const FEED_PHOTO_WIDTH_LARGE = 1200; + +export interface FeedMedia { + url: string + width: number + height: number +} + +export const generateFeedMedia = ( + photo: Photo, + size: NextImageSize, +): FeedMedia => ({ + url: getNextImageUrlForRequest({ imageUrl: photo.url, size }), + width: size, + height: Math.round(size / photo.aspectRatio), +}); + +export const getCoreFeedFields = (photo: Photo) => ({ + id: photo.id, + title: titleForPhoto(photo), + description: descriptionForPhoto(photo, true), +}); diff --git a/src/feed/json.ts b/src/feed/json.ts new file mode 100644 index 00000000..9a1fe608 --- /dev/null +++ b/src/feed/json.ts @@ -0,0 +1,36 @@ +import { absolutePathForPhoto } from '@/app/paths'; +import { + FEED_PHOTO_WIDTH_LARGE, + FEED_PHOTO_WIDTH_MEDIUM, + FEED_PHOTO_WIDTH_SMALL, + FeedMedia, + generateFeedMedia, + getCoreFeedFields, +} from '.'; +import { formatDateFromPostgresString } from '@/utility/date'; +import { Photo } from '@/photo'; + +interface FeedPhotoJson { + id: string + title: string + url: string + make?: string + model?: string + tags?: string[] + takenAtNaive: string + src: Record<'small' | 'medium' | 'large', FeedMedia> +} + +export const formatPhotoForFeedJson = (photo: Photo): FeedPhotoJson => ({ + ...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), + }, +}); diff --git a/src/feed/rss.ts b/src/feed/rss.ts new file mode 100644 index 00000000..e34e9d1e --- /dev/null +++ b/src/feed/rss.ts @@ -0,0 +1,59 @@ +import { Photo } from '@/photo'; +import { + FEED_PHOTO_WIDTH_LARGE, + FEED_PHOTO_WIDTH_MEDIUM, + FeedMedia, + generateFeedMedia, + getCoreFeedFields, +} from '.'; +import { absolutePathForPhoto } from '@/app/paths'; +import { formatDate } from '@/utility/date'; +import { formatStringForXml } from '@/utility/string'; + +interface FeedPhotoRss { + id: string + title: string + description?: string + link: string + pubDate: Date + media: Record<'content' | 'thumb', FeedMedia> +} + +const formatPhotoForFeedRss = (photo: Photo): FeedPhotoRss => ({ + ...getCoreFeedFields(photo), + link: absolutePathForPhoto({ photo }), + pubDate: photo.createdAt, + media: { + content: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE), + thumb: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM), + }, +}); + +const feedPhotoToXml = (photo: FeedPhotoRss): string => { + const formattedDate = formatDate({ date: photo.pubDate, length: 'rss' }); + return ` + ${photo.title} + ${photo.link} + ${formattedDate} + ${photo.link} + ${photo.description + ? `` + : ''} + + + + `; +}; + +export const createRssItems = (photos: Photo[]) => + photos.map(formatPhotoForFeedRss).map(feedPhotoToXml);