Direct R2 assets
This commit is contained in:
@@ -6,6 +6,9 @@ PUBLIC_WEATHER_MQTT_HOST=https://wx.swansea-airport.wales/mqtt
|
|||||||
|
|
||||||
DIRECTUS_URL=https://cms.swansea-airport.wales
|
DIRECTUS_URL=https://cms.swansea-airport.wales
|
||||||
DIRECTUS_PUBLIC_URL=https://cms.swansea-airport.wales
|
DIRECTUS_PUBLIC_URL=https://cms.swansea-airport.wales
|
||||||
|
# Optional for Worker test/prod builds. Leave unset locally to use Directus /assets URLs.
|
||||||
|
# DIRECTUS_ASSET_BASE_URL=https://assets.swansea-airport.wales/
|
||||||
|
# DIRECTUS_ASSET_URL_TEMPLATE=https://assets.swansea-airport.wales/{id}
|
||||||
DIRECTUS_ADMIN_TOKEN=replace-with-production-directus-token
|
DIRECTUS_ADMIN_TOKEN=replace-with-production-directus-token
|
||||||
DIRECTUS_HOMEPAGE_BANNER_FOLDER=homepage-banners
|
DIRECTUS_HOMEPAGE_BANNER_FOLDER=homepage-banners
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ PUBLIC_WEATHER_MQTT_HOST=https://wx.swansea-airport.wales/mqtt
|
|||||||
|
|
||||||
DIRECTUS_URL=https://egfhcmstest.pattinson.org
|
DIRECTUS_URL=https://egfhcmstest.pattinson.org
|
||||||
DIRECTUS_PUBLIC_URL=https://egfhcmstest.pattinson.org
|
DIRECTUS_PUBLIC_URL=https://egfhcmstest.pattinson.org
|
||||||
|
# Optional for Worker test/prod builds. Leave unset locally to use Directus /assets URLs.
|
||||||
|
# DIRECTUS_ASSET_BASE_URL=https://assets-test.example.com/
|
||||||
|
# DIRECTUS_ASSET_URL_TEMPLATE=https://assets-test.example.com/{id}
|
||||||
DIRECTUS_ADMIN_TOKEN=replace-with-test-directus-token
|
DIRECTUS_ADMIN_TOKEN=replace-with-test-directus-token
|
||||||
DIRECTUS_HOMEPAGE_BANNER_FOLDER=homepage-banners
|
DIRECTUS_HOMEPAGE_BANNER_FOLDER=homepage-banners
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,30 @@ CF_WORKER_NAME=swansea-airfield-test
|
|||||||
|
|
||||||
For Directus, use public HTTPS URLs in Worker env files. Do not use Docker-only hostnames such as `http://directus:8055` outside Docker Compose.
|
For Directus, use public HTTPS URLs in Worker env files. Do not use Docker-only hostnames such as `http://directus:8055` outside Docker Compose.
|
||||||
|
|
||||||
|
## Directus Assets From R2
|
||||||
|
|
||||||
|
Local development can keep using Directus asset URLs. For test and production Worker builds, set one of these optional values in `.env.worker.test` and `.env.worker.prod` to make the generated Astro HTML point at R2-hosted files instead:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DIRECTUS_ASSET_BASE_URL=https://assets.swansea-airport.wales/
|
||||||
|
```
|
||||||
|
|
||||||
|
With `DIRECTUS_ASSET_BASE_URL`, a Directus file ID such as `abc-123` becomes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://assets.swansea-airport.wales/abc-123.jpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
The extension comes from Directus `filename_download`, so R2 object keys should use the pattern `<directus-file-id>.<extension>`.
|
||||||
|
|
||||||
|
If the R2 public URL needs a custom path shape, use a template instead:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DIRECTUS_ASSET_URL_TEMPLATE=https://assets.swansea-airport.wales/directus/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
`DIRECTUS_ASSET_URL_TEMPLATE` takes priority over `DIRECTUS_ASSET_BASE_URL`. The template supports `{id}`, `{key}`, and `{fileId}` as aliases for the R2 object key, usually Directus `filename_disk`. Leave both unset in local `.env` to keep using `DIRECTUS_PUBLIC_URL/assets/<id>`.
|
||||||
|
|
||||||
## Build Locally For Test
|
## Build Locally For Test
|
||||||
|
|
||||||
When working through Docker Compose:
|
When working through Docker Compose:
|
||||||
|
|||||||
+55
-8
@@ -35,6 +35,13 @@ const directusUrl = process.env.DIRECTUS_URL ?? 'http://directus:8055';
|
|||||||
const directusPublicUrl = process.env.DIRECTUS_PUBLIC_URL && !process.env.DIRECTUS_PUBLIC_URL.includes('example.com')
|
const directusPublicUrl = process.env.DIRECTUS_PUBLIC_URL && !process.env.DIRECTUS_PUBLIC_URL.includes('example.com')
|
||||||
? process.env.DIRECTUS_PUBLIC_URL
|
? process.env.DIRECTUS_PUBLIC_URL
|
||||||
: directusUrl;
|
: directusUrl;
|
||||||
|
const directusAssetBaseUrl = process.env.DIRECTUS_ASSET_BASE_URL && !process.env.DIRECTUS_ASSET_BASE_URL.includes('example.com')
|
||||||
|
? process.env.DIRECTUS_ASSET_BASE_URL
|
||||||
|
: undefined;
|
||||||
|
const directusAssetUrlTemplate =
|
||||||
|
process.env.DIRECTUS_ASSET_URL_TEMPLATE && !process.env.DIRECTUS_ASSET_URL_TEMPLATE.includes('example.com')
|
||||||
|
? process.env.DIRECTUS_ASSET_URL_TEMPLATE
|
||||||
|
: undefined;
|
||||||
const directusToken = process.env.DIRECTUS_ADMIN_TOKEN;
|
const directusToken = process.env.DIRECTUS_ADMIN_TOKEN;
|
||||||
const homepageBannerFolder = process.env.DIRECTUS_HOMEPAGE_BANNER_FOLDER ?? 'homepage-banners';
|
const homepageBannerFolder = process.env.DIRECTUS_HOMEPAGE_BANNER_FOLDER ?? 'homepage-banners';
|
||||||
const cafePageFolder = process.env.DIRECTUS_CAFE_PAGE_FOLDER ?? 'cafe-page';
|
const cafePageFolder = process.env.DIRECTUS_CAFE_PAGE_FOLDER ?? 'cafe-page';
|
||||||
@@ -49,6 +56,7 @@ type DirectusFile = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
filename_download?: string;
|
filename_download?: string;
|
||||||
|
filename_disk?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,8 +65,8 @@ type EventTemplateRecord = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string | DirectusFile;
|
||||||
logo?: string;
|
logo?: string | DirectusFile;
|
||||||
booking_url?: string;
|
booking_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +121,46 @@ async function readDirectusEndpoint<T>(endpoint: URL): Promise<T[]> {
|
|||||||
return payload.data ?? [];
|
return payload.data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDirectusAssetUrl(fileId: string): string {
|
function extensionFromFilename(filename?: string): string {
|
||||||
|
if (!filename) return '';
|
||||||
|
|
||||||
|
const lastSegment = filename.split('/').pop() ?? '';
|
||||||
|
const dotIndex = lastSegment.lastIndexOf('.');
|
||||||
|
|
||||||
|
if (dotIndex <= 0 || dotIndex === lastSegment.length - 1) return '';
|
||||||
|
return lastSegment.slice(dotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function directusFileId(file: string | DirectusFile): string {
|
||||||
|
return typeof file === 'string' ? file : file.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function directusObjectKey(file: string | DirectusFile): string {
|
||||||
|
if (typeof file !== 'string' && file.filename_disk) {
|
||||||
|
return file.filename_disk;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileId = directusFileId(file);
|
||||||
|
return `${fileId}${extensionFromFilename(typeof file === 'string' ? undefined : file.filename_download)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDirectusAssetUrl(file: string | DirectusFile): string {
|
||||||
|
const fileId = directusFileId(file);
|
||||||
|
const r2ObjectKey = directusObjectKey(file);
|
||||||
|
const encodedObjectKey = encodeURIComponent(r2ObjectKey);
|
||||||
|
|
||||||
|
if (directusAssetUrlTemplate) {
|
||||||
|
return directusAssetUrlTemplate
|
||||||
|
.replaceAll('{fileId}', encodedObjectKey)
|
||||||
|
.replaceAll('{id}', encodedObjectKey)
|
||||||
|
.replaceAll('{key}', encodedObjectKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directusAssetBaseUrl) {
|
||||||
|
const baseUrl = directusAssetBaseUrl.endsWith('/') ? directusAssetBaseUrl : `${directusAssetBaseUrl}/`;
|
||||||
|
return new URL(encodedObjectKey, baseUrl).toString();
|
||||||
|
}
|
||||||
|
|
||||||
return new URL(`/assets/${fileId}`, directusPublicUrl).toString();
|
return new URL(`/assets/${fileId}`, directusPublicUrl).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,11 +184,11 @@ async function getImagesFromFolder(folderName: string, fallbackImages: HomepageB
|
|||||||
endpoint.searchParams.set('sort', '-uploaded_on');
|
endpoint.searchParams.set('sort', '-uploaded_on');
|
||||||
endpoint.searchParams.set('filter[folder][_eq]', folder.id);
|
endpoint.searchParams.set('filter[folder][_eq]', folder.id);
|
||||||
endpoint.searchParams.set('filter[type][_starts_with]', 'image/');
|
endpoint.searchParams.set('filter[type][_starts_with]', 'image/');
|
||||||
endpoint.searchParams.set('fields', 'id,title,description,filename_download,type');
|
endpoint.searchParams.set('fields', 'id,title,description,filename_download,filename_disk,type');
|
||||||
|
|
||||||
const files = await readDirectusEndpoint<DirectusFile>(endpoint);
|
const files = await readDirectusEndpoint<DirectusFile>(endpoint);
|
||||||
const images = files.map((file) => ({
|
const images = files.map((file) => ({
|
||||||
src: resolveDirectusAssetUrl(file.id),
|
src: resolveDirectusAssetUrl(file),
|
||||||
alt: file.description || file.title || file.filename_download || 'Swansea Airport',
|
alt: file.description || file.title || file.filename_download || 'Swansea Airport',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -195,9 +242,9 @@ async function getRecurringEvents(): Promise<EventItem[]> {
|
|||||||
const endpoint = new URL('/items/event_dates', directusUrl);
|
const endpoint = new URL('/items/event_dates', directusUrl);
|
||||||
endpoint.searchParams.set('limit', '100');
|
endpoint.searchParams.set('limit', '100');
|
||||||
endpoint.searchParams.set('sort', 'date');
|
endpoint.searchParams.set('sort', 'date');
|
||||||
endpoint.searchParams.set(
|
endpoint.searchParams.set(
|
||||||
'fields',
|
'fields',
|
||||||
'id,date,template.id,template.title,template.slug,template.description,template.image,template.logo,template.booking_url',
|
'id,date,template.id,template.title,template.slug,template.description,template.image.id,template.image.filename_download,template.image.filename_disk,template.logo.id,template.logo.filename_download,template.logo.filename_disk,template.booking_url',
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventDates = await readDirectusEndpoint<EventDateRecord>(endpoint);
|
const eventDates = await readDirectusEndpoint<EventDateRecord>(endpoint);
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export type EventItem = {
|
|||||||
end_datetime?: string;
|
end_datetime?: string;
|
||||||
location_text?: string;
|
location_text?: string;
|
||||||
registration_link?: string;
|
registration_link?: string;
|
||||||
realimage?: string | { id?: string; filename_download?: string; title?: string };
|
realimage?: string | { id?: string; filename_download?: string; filename_disk?: string; title?: string };
|
||||||
logo?: string | { id?: string; filename_download?: string; title?: string };
|
logo?: string | { id?: string; filename_download?: string; filename_disk?: string; title?: string };
|
||||||
status?: string;
|
status?: string;
|
||||||
is_featured?: boolean;
|
is_featured?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { getEvents } from '../../lib/directus';
|
import { getEvents, resolveDirectusAssetUrl } from '../../lib/directus';
|
||||||
import { getUpcomingEvents } from '../../lib/events';
|
import { getUpcomingEvents } from '../../lib/events';
|
||||||
import { formatDate, formatDateTime } from '../../lib/format';
|
import { formatDate, formatDateTime } from '../../lib/format';
|
||||||
import { normalizeSlug } from '../../lib/slug';
|
import { normalizeSlug } from '../../lib/slug';
|
||||||
|
|
||||||
type EventItem = Awaited<ReturnType<typeof getEvents>>[number];
|
type EventItem = Awaited<ReturnType<typeof getEvents>>[number];
|
||||||
|
|
||||||
declare const process: {
|
|
||||||
env: Record<string, string | undefined>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveEventImageSource(realimage: EventItem['realimage']): string | null {
|
function resolveEventImageSource(realimage: EventItem['realimage']): string | null {
|
||||||
if (!realimage) return null;
|
if (!realimage) return null;
|
||||||
|
|
||||||
const candidate = typeof realimage === 'string' ? realimage : realimage.id;
|
const candidate = typeof realimage === 'string' ? realimage : realimage.id;
|
||||||
if (!candidate) return null;
|
if (!candidate) return null;
|
||||||
|
|
||||||
const configuredDirectusPublicUrl = process.env.DIRECTUS_PUBLIC_URL;
|
if (typeof realimage !== 'string' && realimage.id) {
|
||||||
const directusPort = process.env.DIRECTUS_PORT ?? '8066';
|
return resolveDirectusAssetUrl({
|
||||||
const localFallbackUrl = `${Astro.url.protocol}//${Astro.url.hostname}:${directusPort}`;
|
id: realimage.id,
|
||||||
const directusBaseUrl =
|
filename_download: realimage.filename_download,
|
||||||
configuredDirectusPublicUrl && !configuredDirectusPublicUrl.includes('example.com')
|
});
|
||||||
? configuredDirectusPublicUrl
|
}
|
||||||
: localFallbackUrl;
|
|
||||||
|
|
||||||
if (candidate.startsWith('/')) {
|
if (candidate.startsWith('/assets/')) {
|
||||||
return new URL(candidate, directusBaseUrl).toString();
|
return resolveDirectusAssetUrl(candidate.slice('/assets/'.length).split('/')[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
|
if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
|
||||||
const candidateUrl = new URL(candidate);
|
const candidateUrl = new URL(candidate);
|
||||||
return candidateUrl.pathname.startsWith('/assets/') ? new URL(candidateUrl.pathname, directusBaseUrl).toString() : candidate;
|
return candidateUrl.pathname.startsWith('/assets/')
|
||||||
|
? resolveDirectusAssetUrl(candidateUrl.pathname.slice('/assets/'.length).split('/')[0])
|
||||||
|
: candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new URL(`/assets/${candidate}`, directusBaseUrl).toString();
|
if (candidate.startsWith('/')) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveDirectusAssetUrl(candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
|||||||
Reference in New Issue
Block a user