Finalize feed behavior

This commit is contained in:
Sam Becker 2025-06-12 20:35:38 -05:00
parent 099fcdec8b
commit 9eb04f6015
5 changed files with 45 additions and 34 deletions

View File

@ -3,6 +3,7 @@
"ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP",
"Acros", "Acros",
"affordance", "affordance",
"apos",
"ARROWLEFT", "ARROWLEFT",
"ARROWRIGHT", "ARROWRIGHT",
"Astia", "Astia",

View File

@ -6,7 +6,8 @@ import {
} from '@/app/config'; } from '@/app/config';
import { FEED_PHOTO_REQUEST_LIMIT, formatPhotoForFeedJson } from '@/app/feed'; import { FEED_PHOTO_REQUEST_LIMIT, formatPhotoForFeedJson } from '@/app/feed';
export const dynamic = 'force-static'; // Cache for 24 hours
export const revalidate = 86_400;
export async function GET() { export async function GET() {
if (SITE_FEEDS_ENABLED) { if (SITE_FEEDS_ENABLED) {

View File

@ -8,7 +8,8 @@ import {
import { createRssItems, FEED_PHOTO_REQUEST_LIMIT } from '@/app/feed'; import { createRssItems, FEED_PHOTO_REQUEST_LIMIT } from '@/app/feed';
import { ABSOLUTE_PATH_FOR_RSS_XML } from '@/app/paths'; import { ABSOLUTE_PATH_FOR_RSS_XML } from '@/app/paths';
export const dynamic = 'force-static'; // Cache for 24 hours
export const revalidate = 86_400;
export async function GET() { export async function GET() {
if (SITE_FEEDS_ENABLED) { if (SITE_FEEDS_ENABLED) {
@ -19,18 +20,19 @@ export async function GET() {
const items = createRssItems(photos); const items = createRssItems(photos);
return new Response(` return new Response(`<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"
<rss
version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:atom="http://www.w3.org/2005/Atom" xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/" xmlns:media="http://search.yahoo.com/mrss/"
> >
<channel> <channel>
<title>${META_TITLE}</title> <title>${META_TITLE}</title>
<atom:link href="${ABSOLUTE_PATH_FOR_RSS_XML}" <atom:link
rel="self" type="application/rss+xml" /> href="${ABSOLUTE_PATH_FOR_RSS_XML}"
rel="self"
type="application/rss+xml"
/>
<link>${BASE_URL}</link> <link>${BASE_URL}</link>
<description>${META_DESCRIPTION}</description> <description>${META_DESCRIPTION}</description>
${items.join('\n')} ${items.join('\n')}
@ -40,6 +42,6 @@ export async function GET() {
{ headers: { 'Content-Type': 'text/xml' } }, { headers: { 'Content-Type': 'text/xml' } },
); );
} else { } else {
return new Response('RSS feed access disabled', { status: 404 }); return new Response('Feed disabled', { status: 404 });
} }
} }

View File

@ -5,6 +5,7 @@ import {
NextImageSize, NextImageSize,
} from '@/platforms/next-image'; } from '@/platforms/next-image';
import { formatDate, formatDateFromPostgresString } from '@/utility/date'; import { formatDate, formatDateFromPostgresString } from '@/utility/date';
import { formatStringForXml } from '@/utility/string';
export const FEED_PHOTO_REQUEST_LIMIT = 40; export const FEED_PHOTO_REQUEST_LIMIT = 40;
@ -12,44 +13,44 @@ export const FEED_PHOTO_WIDTH_SMALL = 200;
export const FEED_PHOTO_WIDTH_MEDIUM = 640; export const FEED_PHOTO_WIDTH_MEDIUM = 640;
export const FEED_PHOTO_WIDTH_LARGE = 1200; export const FEED_PHOTO_WIDTH_LARGE = 1200;
interface PublicFeedMedia { interface FeedMedia {
url: string url: string
width: number width: number
height: number height: number
} }
interface PublicFeedPhotoJson { interface FeedPhotoJson {
id: string id: string
title?: string title: string
url: string url: string
make?: string make?: string
model?: string model?: string
tags?: string[] tags?: string[]
takenAtNaive: string takenAtNaive: string
src: Record<'small' | 'medium' | 'large', PublicFeedMedia> src: Record<'small' | 'medium' | 'large', FeedMedia>
} }
interface PublicFeedPhotoRss { interface FeedPhotoRss {
id: string id: string
title?: string title: string
description?: string description?: string
link: string link: string
publicationDate: Date pubDate: Date
media: Record<'content' | 'thumb', PublicFeedMedia> media: Record<'content' | 'thumb', FeedMedia>
} }
export interface PublicFeedJson { export interface FeedJson {
meta: { meta: {
title: string title: string
url: string url: string
} }
photos: PublicFeedPhotoJson[] photos: FeedPhotoJson[]
} }
const generateFeedMedia = ( const generateFeedMedia = (
photo: Photo, photo: Photo,
size: NextImageSize, size: NextImageSize,
): PublicFeedMedia => ({ ): FeedMedia => ({
url: getNextImageUrlForRequest({ imageUrl: photo.url, size }), url: getNextImageUrlForRequest({ imageUrl: photo.url, size }),
width: size, width: size,
height: Math.round(size / photo.aspectRatio), height: Math.round(size / photo.aspectRatio),
@ -58,10 +59,10 @@ const generateFeedMedia = (
const getCoreFeedFields = (photo: Photo) => ({ const getCoreFeedFields = (photo: Photo) => ({
id: photo.id, id: photo.id,
title: titleForPhoto(photo), title: titleForPhoto(photo),
description: descriptionForPhoto(photo), description: descriptionForPhoto(photo, true),
}); });
export const formatPhotoForFeedJson = (photo: Photo): PublicFeedPhotoJson => ({ export const formatPhotoForFeedJson = (photo: Photo): FeedPhotoJson => ({
...getCoreFeedFields(photo), ...getCoreFeedFields(photo),
url: absolutePathForPhoto({ photo }), url: absolutePathForPhoto({ photo }),
...photo.make && { make: photo.make }, ...photo.make && { make: photo.make },
@ -75,37 +76,35 @@ export const formatPhotoForFeedJson = (photo: Photo): PublicFeedPhotoJson => ({
}, },
}); });
const formatPhotoForFeedRss = (photo: Photo): PublicFeedPhotoRss => ({ const formatPhotoForFeedRss = (photo: Photo): FeedPhotoRss => ({
...getCoreFeedFields(photo), ...getCoreFeedFields(photo),
link: absolutePathForPhoto({ photo }), link: absolutePathForPhoto({ photo }),
publicationDate: photo.createdAt, pubDate: photo.createdAt,
media: { media: {
content: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE), content: generateFeedMedia(photo, FEED_PHOTO_WIDTH_LARGE),
thumb: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM), thumb: generateFeedMedia(photo, FEED_PHOTO_WIDTH_MEDIUM),
}, },
}); });
const feedPhotoToXml = (photo: PublicFeedPhotoRss): string => { const feedPhotoToXml = (photo: FeedPhotoRss): string => {
const formattedDate = formatDate({ const formattedDate = formatDate({ date: photo.pubDate, length: 'rss' });
date: photo.publicationDate,
length: 'rss',
});
return `<item> return `<item>
<title>${photo.title}</title> <title>${photo.title}</title>
<link>${photo.link}</link> <link>${photo.link}</link>
<pubDate>${formattedDate}</pubDate> <pubDate>${formattedDate}</pubDate>
<guid isPermaLink="true">${photo.link}</guid> <guid isPermaLink="true">${photo.link}</guid>
<description> ${photo.description
<![CDATA[${photo.description}]]> ? `<description><![CDATA[${photo.description}]]></description>`
</description> : ''}
<media:content url="<![CDATA[${photo.media.content.url}]]>" <media:content
url="${formatStringForXml(photo.media.content.url)}"
type="image/jpeg" type="image/jpeg"
medium="image" medium="image"
width="${photo.media.content.width}" width="${photo.media.content.width}"
height="${photo.media.content.height}" height="${photo.media.content.height}"
> >
<media:thumbnail <media:thumbnail
url="<![CDATA[${photo.media.thumb.url}]]>" url="${formatStringForXml(photo.media.thumb.url)}"
width="${photo.media.thumb.width}" width="${photo.media.thumb.width}"
height="${photo.media.thumb.height}" height="${photo.media.thumb.height}"
/> />

View File

@ -35,6 +35,14 @@ export const parameterize = (
) )
.toLocaleLowerCase(); .toLocaleLowerCase();
export const formatStringForXml = (string: string) =>
string
.replace(/&/g, '&amp;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
export const deparameterize = (string: string) => export const deparameterize = (string: string) =>
capitalizeWords(string.replaceAll('-', ' ')); capitalizeWords(string.replaceAll('-', ' '));