Create feed module

This commit is contained in:
Sam Becker 2025-06-12 21:07:49 -05:00
parent 9eb04f6015
commit 4d904517a5
6 changed files with 131 additions and 118 deletions

View File

@ -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;

View File

@ -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

View File

@ -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 `<item>
<title>${photo.title}</title>
<link>${photo.link}</link>
<pubDate>${formattedDate}</pubDate>
<guid isPermaLink="true">${photo.link}</guid>
${photo.description
? `<description><![CDATA[${photo.description}]]></description>`
: ''}
<media:content
url="${formatStringForXml(photo.media.content.url)}"
type="image/jpeg"
medium="image"
width="${photo.media.content.width}"
height="${photo.media.content.height}"
>
<media:thumbnail
url="${formatStringForXml(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);

32
src/feed/index.ts Normal file
View File

@ -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),
});

36
src/feed/json.ts Normal file
View File

@ -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),
},
});

59
src/feed/rss.ts Normal file
View File

@ -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 `<item>
<title>${photo.title}</title>
<link>${photo.link}</link>
<pubDate>${formattedDate}</pubDate>
<guid isPermaLink="true">${photo.link}</guid>
${photo.description
? `<description><![CDATA[${photo.description}]]></description>`
: ''}
<media:content
url="${formatStringForXml(photo.media.content.url)}"
type="image/jpeg"
medium="image"
width="${photo.media.content.width}"
height="${photo.media.content.height}"
>
<media:thumbnail
url="${formatStringForXml(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);