diff --git a/README.md b/README.md
index 6d236c25..73b19079 100644
--- a/README.md
+++ b/README.md
@@ -62,3 +62,4 @@ Installation
1. Set `NEXT_PUBLIC_HIDE_REPO_LINK = 1` to remove footer link to repo
2. Set `NEXT_PUBLIC_PRO_MODE = 1` to enable higher quality image storage
+3. Set `NEXT_PUBLIC_PUBLIC_API = 1` to enable a public API available at `/api`
diff --git a/src/app/api/route.ts b/src/app/api/route.ts
new file mode 100644
index 00000000..5573b5e5
--- /dev/null
+++ b/src/app/api/route.ts
@@ -0,0 +1,24 @@
+import { getPhotosCached } from '@/cache';
+import { parsePhotoForApi } from '@/photo';
+import {
+ BASE_URL,
+ PUBLIC_API_ENABLED,
+ SITE_TITLE,
+} from '@/site/config';
+
+const API_PHOTO_LIMIT = 20;
+
+export async function GET() {
+ if (PUBLIC_API_ENABLED) {
+ const photos = await getPhotosCached({ limit: API_PHOTO_LIMIT });
+ return Response.json({
+ meta: {
+ title: SITE_TITLE,
+ url: BASE_URL,
+ },
+ photos: photos.map(parsePhotoForApi),
+ });
+ } else {
+ return Response.json({ message: 'API is disabled' });
+ }
+}
diff --git a/src/photo/actions.ts b/src/photo/actions.ts
index ed6e071f..0034fe19 100644
--- a/src/photo/actions.ts
+++ b/src/photo/actions.ts
@@ -18,7 +18,7 @@ import {
revalidateBlobKey,
revalidatePhotosKey,
} from '@/cache';
-import { IS_PRO_MODE } from '@/site/config';
+import { PRO_MODE_ENABLED } from '@/site/config';
import { getNextImageUrlForRequest } from '@/utility/image';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths';
@@ -31,10 +31,10 @@ export async function createPhotoAction(formData: FormData) {
const updatedUrl = await convertUploadToPhoto(
photo.url,
photo.id,
- !IS_PRO_MODE
+ !PRO_MODE_ENABLED
? getNextImageUrlForRequest(photo.url, 3840, 90, requestOrigin)
: undefined,
- !IS_PRO_MODE ? 'webp' : undefined,
+ !PRO_MODE_ENABLED ? 'webp' : undefined,
);
if (updatedUrl) { photo.url = updatedUrl; }
diff --git a/src/photo/index.ts b/src/photo/index.ts
index a6ee8910..d22cfbfd 100644
--- a/src/photo/index.ts
+++ b/src/photo/index.ts
@@ -1,4 +1,7 @@
-import { ABSOLUTE_PATH_FOR_HOME_IMAGE } from '@/site/paths';
+import {
+ ABSOLUTE_PATH_FOR_HOME_IMAGE,
+ absolutePathForPhoto,
+} from '@/site/paths';
import { formatDateFromPostgresString } from '@/utility/date';
import {
formatAperture,
@@ -7,6 +10,7 @@ import {
formatExposureTime,
formatFocalLength,
} from '@/utility/exif';
+import { getNextImageUrlForRequest } from '@/utility/image';
import camelcaseKeys from 'camelcase-keys';
import type { Metadata } from 'next';
@@ -86,6 +90,21 @@ export const parsePhotoFromDb = (photoDbRaw: PhotoDb): Photo => {
};
};
+export const parsePhotoForApi = (photo: Photo) => ({
+ id: photo.id,
+ title: photo.title,
+ 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: getNextImageUrlForRequest(photo.url, 200),
+ medium: getNextImageUrlForRequest(photo.url, 640),
+ large: getNextImageUrlForRequest(photo.url, 1200),
+ },
+});
+
export const parseCachedPhotoDates = (photo: Photo) => ({
...photo,
takenAt: new Date(photo.takenAt),
diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx
index 1609a894..6487380d 100644
--- a/src/site/SiteChecklistClient.tsx
+++ b/src/site/SiteChecklistClient.tsx
@@ -26,7 +26,8 @@ export default function SiteChecklistClient({
hasTitle,
hasDomain,
showRepoLink,
- isProMode,
+ isProModeEnabled,
+ isPublicApiEnabled,
showRefreshButton,
secret,
}: {
@@ -37,7 +38,8 @@ export default function SiteChecklistClient({
hasTitle: boolean
hasDomain: boolean
showRepoLink: boolean
- isProMode: boolean
+ isProModeEnabled: boolean
+ isPublicApiEnabled: boolean
showRefreshButton?: boolean
secret: string
}) {
@@ -219,7 +221,7 @@ export default function SiteChecklistClient({
/api:
+ {renderEnvVars(['NEXT_PUBLIC_PUBLIC_API'])}
+