Add rss.xml and feed.json endpoints

This commit is contained in:
Tadej Novak 2025-06-08 13:26:35 +02:00
parent 28b1c92edb
commit 3c4adc2f9e
No known key found for this signature in database
3 changed files with 163 additions and 0 deletions

28
app/feed.json/route.ts Normal file
View File

@ -0,0 +1,28 @@
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';
export const dynamic = 'force-static';
export async function GET() {
if (PUBLIC_FEED_ENABLED) {
const photos = await getPhotosCached({
limit: INFINITE_SCROLL_FEED_INITIAL,
sortBy: 'createdAt',
});
return Response.json({
meta: {
title: META_TITLE,
url: BASE_URL,
},
photos: photos.map(formatPhotoForFeed),
});
} else {
return new Response('Feed disabled', { status: 404 });
}
}

48
app/rss.xml/route.ts Normal file
View File

@ -0,0 +1,48 @@
import { INFINITE_SCROLL_FEED_INITIAL } from '@/photo';
import { getPhotosCached } from '@/photo/cache';
import {
BASE_URL,
META_DESCRIPTION,
META_TITLE,
PUBLIC_FEED_ENABLED,
} from '@/app/config';
import { feedPhotoToXml, formatPhotoForFeed } from '@/app/feed';
export const dynamic = 'force-static';
export async function GET() {
if (PUBLIC_FEED_ENABLED) {
const photos = await getPhotosCached({
limit: INFINITE_SCROLL_FEED_INITIAL,
sortBy: 'createdAt',
});
const items = photos.map(formatPhotoForFeed).map(feedPhotoToXml);
return new Response(
`<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>${META_TITLE}</title>
<atom:link href="${BASE_URL}/rss.xml"
rel="self" type="application/rss+xml" />
<link>${BASE_URL}</link>
<description>${META_DESCRIPTION}</description>
${items.join('\n\n ')}
</channel>
</rss>`,
{
headers: {
'Content-Type': 'text/xml',
},
},
);
} else {
return new Response('RSS feed access disabled', { status: 404 });
}
}

87
src/app/feed.ts Normal file
View File

@ -0,0 +1,87 @@
import { Photo } from '@/photo';
import { absolutePathForPhoto } from './paths';
import { getNextImageUrlForRequest } from '@/platforms/next-image';
import { formatDate } from '@/utility/date';
export const FEED_PHOTO_WIDTH_CONTENT = 1080;
export const FEED_PHOTO_WIDTH_THUMB = 640;
export interface PublicFeed {
meta: {
title: string
url: string
}
photos: PublicFeedPhoto[]
}
interface PublicFeedMedia {
url: string
width: number
height: number
}
interface PublicFeedPhoto {
id: string
title?: string
description?: string
link: string
publicationDate: Date
media: Record<
'content' | 'thumb',
PublicFeedMedia
>
}
export const formatPhotoForFeed = (photo: Photo): PublicFeedPhoto => ({
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),
},
},
});
export const feedPhotoToXml = (photo: PublicFeedPhoto): string => {
const formattedDate = formatDate({
date: photo.publicationDate,
length: 'rss',
});
const description = photo.description ?
`<description>
<![CDATA[${photo.description}]]>
</description>` : '';
return ` <item>
<title>${photo.title}</title>
<link>${photo.link}</link>
<pubDate>${formattedDate}</pubDate>
<guid isPermaLink="true">${photo.link}</guid>
${description}
<media:content url="${photo.media.content.url.replace(/&/g, '&amp;')}"
type="image/jpeg"
medium="image"
width="${photo.media.content.width}"
height="${photo.media.content.height}">
<media:thumbnail url="${photo.media.thumb.url.replace(/&/g, '&amp;')}"
width="${photo.media.thumb.width}"
height="${photo.media.thumb.height}" />
</media:content>
</item>`;
};