Vercel/src/app/feed.ts
2025-06-12 19:04:42 -05:00

118 lines
3.1 KiB
TypeScript

import { descriptionForPhoto, Photo, titleForPhoto } from '@/photo';
import { absolutePathForPhoto } from './paths';
import {
getNextImageUrlForRequest,
NextImageSize,
} from '@/platforms/next-image';
import { formatDate, formatDateFromPostgresString } from '@/utility/date';
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 PublicFeedMedia {
url: string
width: number
height: number
}
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
link: string
publicationDate: Date
media: Record<'content' | 'thumb', PublicFeedMedia>
}
export interface PublicFeedJson {
meta: {
title: string
url: string
}
photos: PublicFeedPhotoJson[]
}
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: 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),
},
});
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),
},
});
const feedPhotoToXml = (photo: PublicFeedPhotoRss): string => {
const formattedDate = formatDate({
date: photo.publicationDate,
length: 'rss',
});
return `<item>
<title>${photo.title}</title>
<link>${photo.link}</link>
<pubDate>${formattedDate}</pubDate>
<guid isPermaLink="true">${photo.link}</guid>
<description>
<![CDATA[${photo.description}]]>
</description>
<media:content url="<![CDATA[${photo.media.content.url}]]>"
type="image/jpeg"
medium="image"
width="${photo.media.content.width}"
height="${photo.media.content.height}"
>
<media:thumbnail
url="<![CDATA[${photo.media.thumb.url}]]>"
width="${photo.media.thumb.width}"
height="${photo.media.thumb.height}"
/>
</media:content>
</item>`;
};
export const createRssItems = (photos: Photo[]) =>
photos.map(formatPhotoForFeedRss).map(feedPhotoToXml);