diff --git a/README.md b/README.md
index 8ebd954d..939fcc06 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,7 @@ Installation
- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api`
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo
- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar
+- `NEXT_PUBLIC_HIDE_EXIF_DATA = 1` hides EXIF data in photo details and OG images (potentially useful for portfolios, which don't focus on photography)
- `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint)
- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top)
@@ -82,24 +83,27 @@ Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used a
- Setup CORS under bucket settings:
```json
[{
- "AllowedHeaders": ["*"]
- "AllowedOrigins": [
- "http://localhost:3000",
- "https://{VERCEL_PROJECT_NAME}*.vercel.app",
- "{PRODUCTION_DOMAIN}"
- ],
+ "AllowedHeaders": ["*"],
"AllowedMethods": [
"GET",
"PUT"
],
+ "AllowedOrigins": [
+ "http://localhost:3000",
+ "https://{VERCEL_PROJECT_NAME}*.vercel.app",
+ "{PRODUCTION_DOMAIN}"
+ ]
}]
```
- - Enable R2.dev subdomain (necessary in order to serve files publicly without a custom domain)
- - Store configuration:
+ - Enable public hosting by doing one of the following:
+ - Select "Connect Custom Domain" and choose a Cloudflare domain
+ - OR
+ - Select "Allow Access" from R2.dev subdomain
+ - Store public configuration:
- `NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET`: bucket name
- `NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID`: account id (found on R2 overview page)
- - `NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN`: r2.dev subdomain, e.g., "pub-jf90908..."
-2. Setup credentials
+ - `NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN`: e.g., either "pub-jf90908...r2.dev" or "custom-domain.com"
+2. Setup private credentials
- Create API token by selecting "Manage R2 API Tokens," and clicking "Create API Token"
- Select "Object Read & Write," choose "Apply to specific buckets only," and select the bucket created in Step 1.
- Store credentials (⚠️ _Ensure access keys are not prefixed with `NEXT_PUBLIC`_):
@@ -126,10 +130,10 @@ Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used a
"ExposeHeaders": []
}]
```
- - Store configuration
+ - Store public configuration
- `NEXT_PUBLIC_AWS_S3_BUCKET`: bucket name
- `NEXT_PUBLIC_AWS_S3_REGION`: bucket region, e.g., "us-east-1"
-2. Setup credentials
+2. Setup private credentials
- [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) using JSON editor:
```json
{
diff --git a/next.config.js b/next.config.js
index b8999f9a..c6ea678c 100644
--- a/next.config.js
+++ b/next.config.js
@@ -7,9 +7,7 @@ const VERCEL_BLOB_HOSTNAME = VERCEL_BLOB_STORE_ID
: undefined;
const CLOUDFLARE_R2_HOSTNAME =
- process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN
- ? `${process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN}.r2.dev`
- : undefined;
+ process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN;
const AWS_S3_HOSTNAME =
process.env.NEXT_PUBLIC_AWS_S3_BUCKET &&
diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx
index 935a3b55..466caf34 100644
--- a/src/photo/PhotoLarge.tsx
+++ b/src/photo/PhotoLarge.tsx
@@ -1,4 +1,9 @@
-import { Photo, photoHasCameraData, photoHasExifData, titleForPhoto } from '.';
+import {
+ Photo,
+ shouldShowCameraDataForPhoto,
+ shouldShowExifDataForPhoto,
+ titleForPhoto,
+} from '.';
import SiteGrid from '@/components/SiteGrid';
import ImageLarge from '@/components/ImageLarge';
import { clsx } from 'clsx/lite';
@@ -92,7 +97,7 @@ export default function PhotoLarge({
{tags.length > 0 &&
}
- {showCamera && photoHasCameraData(photo) &&
+ {showCamera && shouldShowCameraDataForPhoto(photo) &&
}
>)}
{renderMiniGrid(<>
- {photoHasExifData(photo) &&
+ {shouldShowExifDataForPhoto(photo) &&
-
{photo.focalLengthFormatted}
diff --git a/src/photo/image-response/PhotoImageResponse.tsx b/src/photo/image-response/PhotoImageResponse.tsx
index 499ca283..fcc28897 100644
--- a/src/photo/image-response/PhotoImageResponse.tsx
+++ b/src/photo/image-response/PhotoImageResponse.tsx
@@ -1,4 +1,4 @@
-import { Photo } from '..';
+import { Photo, shouldShowExifDataForPhoto } from '..';
import { AiFillApple } from 'react-icons/ai';
import ImageCaption from './components/ImageCaption';
import ImagePhotoGrid from './components/ImagePhotoGrid';
@@ -30,25 +30,26 @@ export default function PhotoImageResponse({
height,
...OG_TEXT_BOTTOM_ALIGNMENT && { imagePosition: 'top' },
}} />
-
- {photo.make === 'Apple' &&
+ {shouldShowExifDataForPhoto(photo) &&
+
+ {photo.make === 'Apple' &&
+ }
+ {model &&
+
+ {model}
+
}
}
- {model &&
+ {photo.focalLengthFormatted}
+
- {model}
-
}
-
- {photo.focalLengthFormatted}
-
-
- {photo.fNumberFormatted}
-
-
- {photo.isoFormatted}
-
-
+ {photo.fNumberFormatted}
+
+
+ {photo.isoFormatted}
+
+ }
);
};
diff --git a/src/photo/index.ts b/src/photo/index.ts
index 664d4aba..bef1ae1b 100644
--- a/src/photo/index.ts
+++ b/src/photo/index.ts
@@ -1,4 +1,5 @@
import { FilmSimulation } from '@/simulation';
+import { SHOW_EXIF_DATA } from '@/site/config';
import { ABSOLUTE_PATH_FOR_HOME_IMAGE } from '@/site/paths';
import { formatDateFromPostgresString } from '@/utility/date';
import {
@@ -228,14 +229,20 @@ export const dateRangeForPhotos = (
return { start, end, description };
};
-export const photoHasCameraData = (photo: Photo) =>
+const photoHasCameraData = (photo: Photo) =>
photo.make &&
photo.model;
-export const photoHasExifData = (photo: Photo) =>
+const photoHasExifData = (photo: Photo) =>
photo.focalLength ||
photo.focalLengthIn35MmFormat ||
photo.fNumberFormatted ||
photo.isoFormatted ||
photo.exposureTimeFormatted ||
photo.exposureCompensationFormatted;
+
+export const shouldShowCameraDataForPhoto = (photo: Photo) =>
+ SHOW_EXIF_DATA && photoHasCameraData(photo);
+
+export const shouldShowExifDataForPhoto = (photo: Photo) =>
+ SHOW_EXIF_DATA && photoHasExifData(photo);
diff --git a/src/services/storage/aws-s3.ts b/src/services/storage/aws-s3.ts
index 2ec518d5..07bc3dc9 100644
--- a/src/services/storage/aws-s3.ts
+++ b/src/services/storage/aws-s3.ts
@@ -11,8 +11,10 @@ const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
const AWS_S3_ACCESS_KEY = process.env.AWS_S3_ACCESS_KEY ?? '';
const AWS_S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_ACCESS_KEY ?? '';
-export const AWS_S3_BASE_URL =
- `https://${AWS_S3_BUCKET}.s3.${AWS_S3_REGION}.amazonaws.com`;
+
+export const AWS_S3_BASE_URL = AWS_S3_BUCKET && AWS_S3_REGION
+ ? `https://${AWS_S3_BUCKET}.s3.${AWS_S3_REGION}.amazonaws.com`
+ : undefined;
export const awsS3Client = () => new S3Client({
region: AWS_S3_REGION,
@@ -25,7 +27,7 @@ export const awsS3Client = () => new S3Client({
const urlForKey = (key?: string) => `${AWS_S3_BASE_URL}/${key}`;
export const isUrlFromAwsS3 = (url: string) =>
- url.startsWith(AWS_S3_BASE_URL);
+ AWS_S3_BASE_URL && url.startsWith(AWS_S3_BASE_URL);
export const awsS3PutObjectCommandForKey = (Key: string) =>
new PutObjectCommand({ Bucket: AWS_S3_BUCKET, Key, ACL: 'public-read' });
diff --git a/src/services/storage/cloudflare-r2.ts b/src/services/storage/cloudflare-r2.ts
index 7866005a..cf4ac5d2 100644
--- a/src/services/storage/cloudflare-r2.ts
+++ b/src/services/storage/cloudflare-r2.ts
@@ -11,20 +11,23 @@ const CLOUDFLARE_R2_BUCKET =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? '';
const CLOUDFLARE_R2_ACCOUNT_ID =
process.env.NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID ?? '';
-const CLOUDFLARE_R2_DEV_SUBDOMAIN =
- process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN ?? '';
+const CLOUDFLARE_R2_PUBLIC_DOMAIN =
+ process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN ?? '';
const CLOUDFLARE_R2_ACCESS_KEY =
process.env.CLOUDFLARE_R2_ACCESS_KEY ?? '';
const CLOUDFLARE_R2_SECRET_ACCESS_KEY =
process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY ?? '';
-const CLOUDFLARE_R2_ENDPOINT =
- `https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`;
+const CLOUDFLARE_R2_ENDPOINT = CLOUDFLARE_R2_ACCOUNT_ID
+ ? `https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
+ : undefined;
+export const CLOUDFLARE_R2_BASE_URL_PUBLIC = CLOUDFLARE_R2_PUBLIC_DOMAIN
+ ? `https://${CLOUDFLARE_R2_PUBLIC_DOMAIN}`
+ : undefined;
export const CLOUDFLARE_R2_BASE_URL_PRIVATE =
- `${CLOUDFLARE_R2_ENDPOINT}/${CLOUDFLARE_R2_BUCKET}`;
-
-export const CLOUDFLARE_R2_BASE_URL_PUBLIC =
- `https://${CLOUDFLARE_R2_DEV_SUBDOMAIN}.r2.dev`;
+ CLOUDFLARE_R2_ENDPOINT && CLOUDFLARE_R2_BUCKET
+ ? `${CLOUDFLARE_R2_ENDPOINT}/${CLOUDFLARE_R2_BUCKET}`
+ : undefined;
export const cloudflareR2Client = () => new S3Client({
region: 'auto',
@@ -39,9 +42,13 @@ const urlForKey = (key?: string, isPublic = true) => isPublic
? `${CLOUDFLARE_R2_BASE_URL_PUBLIC}/${key}`
: `${CLOUDFLARE_R2_BASE_URL_PRIVATE}/${key}`;
-export const isUrlFromCloudflareR2 = (url: string) =>
- url.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE) ||
- url.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC);
+export const isUrlFromCloudflareR2 = (url: string) => (
+ CLOUDFLARE_R2_BASE_URL_PRIVATE &&
+ url.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE)
+) || (
+ CLOUDFLARE_R2_BASE_URL_PUBLIC &&
+ url.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC)
+);
export const cloudflareR2PutObjectCommandForKey = (Key: string) =>
new PutObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Key });
diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts
index 2b58ebe3..9e7a1e1f 100644
--- a/src/services/storage/index.ts
+++ b/src/services/storage/index.ts
@@ -130,7 +130,7 @@ export const convertUploadToPhoto = async (
): Promise => {
const fileName = photoId ? `${PREFIX_PHOTO}-${photoId}` : `${PREFIX_PHOTO}`;
const fileExtension = getExtensionFromStorageUrl(uploadUrl);
- const photoUrl = `${fileName}.${fileExtension ?? 'jpg'}`;
+ const photoPath = `${fileName}.${fileExtension ?? 'jpg'}`;
const storageType = storageTypeFromUrl(uploadUrl);
@@ -139,17 +139,17 @@ export const convertUploadToPhoto = async (
// Copy file
switch (storageType) {
case 'vercel-blob':
- url = await vercelBlobCopy(uploadUrl, photoUrl, photoId === undefined);
+ url = await vercelBlobCopy(uploadUrl, photoPath, photoId === undefined);
break;
case 'cloudflare-r2':
url = await cloudflareR2Copy(
getFileNameFromStorageUrl(uploadUrl),
- photoUrl,
+ photoPath,
photoId === undefined,
);
break;
case 'aws-s3':
- url = await awsS3Copy(uploadUrl, photoUrl, photoId === undefined);
+ url = await awsS3Copy(uploadUrl, photoPath, photoId === undefined);
break;
}
diff --git a/src/services/storage/vercel-blob.ts b/src/services/storage/vercel-blob.ts
index a8b5094f..fb6f013c 100644
--- a/src/services/storage/vercel-blob.ts
+++ b/src/services/storage/vercel-blob.ts
@@ -6,10 +6,12 @@ const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
)?.[1].toLowerCase();
-export const VERCEL_BLOB_BASE_URL =
- `https://${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`;
+export const VERCEL_BLOB_BASE_URL = VERCEL_BLOB_STORE_ID
+ ? `https://${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`
+ : undefined;
export const isUrlFromVercelBlob = (url: string) =>
+ VERCEL_BLOB_BASE_URL &&
url.startsWith(VERCEL_BLOB_BASE_URL);
export const vercelBlobUploadFromClient = async (
diff --git a/src/site/NavClient.tsx b/src/site/NavClient.tsx
index 066ba107..e15e3590 100644
--- a/src/site/NavClient.tsx
+++ b/src/site/NavClient.tsx
@@ -68,7 +68,7 @@ export default function NavClient({
showAdmin={showAdmin}
/>
-
+
{renderLink(SITE_DOMAIN_OR_TITLE, PATH_ROOT)}
]
diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx
index 6ea9004e..fb940901 100644
--- a/src/site/SiteChecklistClient.tsx
+++ b/src/site/SiteChecklistClient.tsx
@@ -35,6 +35,7 @@ export default function SiteChecklistClient({
hasDomain,
showRepoLink,
showFilmSimulations,
+ showExifInfo,
isProModeEnabled,
isGeoPrivacyEnabled,
isPriorityOrderEnabled,
@@ -146,7 +147,8 @@ export default function SiteChecklistClient({
title={!hasStorage
? 'Setup storage (one of the following)'
: hasMultipleStorageProviders
- ? `Setup storage (current: ${labelForStorage(currentStorage)})`
+ // eslint-disable-next-line max-len
+ ? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
: 'Setup storage'}
status={hasStorage}
isPending={isPendingPage}
@@ -317,6 +319,15 @@ export default function SiteChecklistClient({
simulations showing up in /grid sidebar:
{renderEnvVars(['NEXT_PUBLIC_HIDE_FILM_SIMULATIONS'])}
+
+ Set environment variable to {'"1"'} to hide EXIF data:
+ {renderEnvVars(['NEXT_PUBLIC_HIDE_EXIF_DATA'])}
+
0 &&
(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID ?? '').length > 0 &&
- (process.env.NEXT_PUBLIC_CLOUDFLARE_R2_DEV_SUBDOMAIN ?? '').length > 0;
+ (process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN ?? '').length > 0;
export const HAS_CLOUDFLARE_R2_STORAGE =
HAS_CLOUDFLARE_R2_STORAGE_CLIENT &&
(process.env.CLOUDFLARE_R2_ACCESS_KEY ?? '').length > 0 &&
@@ -83,6 +83,7 @@ export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1';
export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
export const SHOW_FILM_SIMULATIONS =
process.env.NEXT_PUBLIC_HIDE_FILM_SIMULATIONS !== '1';
+export const SHOW_EXIF_DATA = process.env.NEXT_PUBLIC_HIDE_EXIF_DATA !== '1';
export const GRID_ASPECT_RATIO = process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO
? parseFloat(process.env.NEXT_PUBLIC_GRID_ASPECT_RATIO)
: 1;
@@ -111,6 +112,7 @@ export const CONFIG_CHECKLIST_STATUS = {
hasDomain: (process.env.NEXT_PUBLIC_SITE_DOMAIN ?? '').length > 0,
showRepoLink: SHOW_REPO_LINK,
showFilmSimulations: SHOW_FILM_SIMULATIONS,
+ showExifInfo: SHOW_EXIF_DATA,
isProModeEnabled: PRO_MODE_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
isPriorityOrderEnabled: PRIORITY_ORDER_ENABLED,