Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: proxy files with custom slugs #224

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ src/lib/datocms/types.ts
src/lib/i18n/messages.json
src/lib/i18n/types.ts
src/lib/site.json
src/lib/routing/file-proxy-map.json
src/lib/routing/redirects.json
50 changes: 50 additions & 0 deletions config/datocms/migrations/1734959319_customFileSlugs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Client } from '@datocms/cli/lib/cma-client-node';

export default async function (client: Client) {
console.log('Creating new fields/fieldsets');

console.log(
'Create Single-line string field "Slug" (`slug`) in model "\uD83D\uDCE6 File" (`file`)'
);
await client.fields.create('GjWw8t-hTFaYYWyc53FeIg', {
id: 'cnnDYybJTAGvHxiYq2MJ8g',
label: 'Slug',
field_type: 'string',
api_key: 'slug',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think naming this slug is confusing. In my perception, a slug is a human-readable, yet machine-safe path fragment. What is described in the hint, and in the customFields function seems to show a path, not a slug

hint: 'Optional custom slug, like <code>/my-files/example.pdf</code>. This may be useful if you need files to be available under a specific URL.',
validators: {
format: {
custom_pattern: '^/.*',
description: 'Field must start with a forward slash: /',
},
},
appearance: {
addons: [],
editor: 'single_line',
parameters: { heading: false, placeholder: null },
},
default_value: '',
});

console.log('Update existing fields/fieldsets');

console.log(
'Update Single-line string field "Slug" (`slug`) in model "\uD83D\uDCE6 File" (`file`)'
);
await client.fields.update('cnnDYybJTAGvHxiYq2MJ8g', { position: 3 });

console.log(
'Update Single-line string field "Title" (`title`) in model "\uD83D\uDCE6 File" (`file`)'
);
await client.fields.update('YIftd04cTlyz0aEvqsfWXA', {
appearance: {
addons: [],
editor: 'EiyZ3d0SSDCPCNbsKBIwWQ',
parameters: {
defaultFunction:
'if (slug) {\n return slug.split(\'/\').pop()\n}\nconst upload = await getUpload(file.upload_id)\nreturn `${upload.filename}`',
},
field_extension: 'computedFields',
},
});
}
2 changes: 1 addition & 1 deletion datocms-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
* @see docs/getting-started.md on how to use this file
* @see docs/decision-log/2023-10-24-datocms-env-file.md on why file is preferred over env vars
*/
export const datocmsEnvironment = 'action-block';
export const datocmsEnvironment = 'custom-file-slugs';
export const datocmsBuildTriggerId = '30535';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"prep:clean-astro-env": "rm -rf node_modules/.astro",
"prep:cloudflare-env": "touch .dev.vars",
"prep:datocms-types": "graphql-codegen --config .graphqlrc.ts",
"prep:download-file-proxy-map": "jiti scripts/download-file-proxy-map.ts",
"prep:download-redirects": "jiti scripts/download-redirects.ts",
"prep:download-site-data": "jiti scripts/download-site-data.ts",
"prep:download-translations": "jiti scripts/download-translations.ts",
Expand Down
46 changes: 46 additions & 0 deletions scripts/download-file-proxy-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { writeFile } from 'node:fs/promises';
import { buildClient } from '@datocms/cma-client-node';
import dotenv from 'dotenv-safe';
import { datocmsEnvironment } from '../datocms-environment';

dotenv.config({
allowEmptyValues: Boolean(process.env.CI),
});

type FileRecord = {
slug?: string;
file: {
upload_id: string;
}
}
type FileSlugMap = {
[key: string]: string;
};

async function getFileProxyMap() {
// use client instead of http api for pagination support
const client = buildClient({
apiToken: process.env.DATOCMS_READONLY_API_TOKEN!,
environment: datocmsEnvironment,
});

const fileSlugMap = {} as FileSlugMap;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const fileSlugMap = {} as FileSlugMap;
const fileSlugMap: FileSlugMap = {};

This is safer than as:

image


for await (const item of client.items.listPagedIterator({ filter: { type: 'file' } }) as unknown as FileRecord[]) {
if (item.slug) {
const asset = await client.uploads.find(item.file.upload_id);
if (asset) {
fileSlugMap[item.slug] = asset.url;
}
}
}
return fileSlugMap;
}

async function downloadFileProxyMap() {
const files = await getFileProxyMap();
await writeFile('./src/lib/routing/file-proxy-map.json', JSON.stringify(files, null, 2));
}

downloadFileProxyMap()
.then(() => console.log('File slugs downloaded'));
1 change: 1 addition & 0 deletions src/lib/routing/FileRoute.fragment.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ fragment FileRoute on FileRecord {
url
}
locale
customSlug: slug
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required to resolve clashing types with other slug fields on route fragments

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also be resolved when renaming the field

title
}
20 changes: 20 additions & 0 deletions src/lib/routing/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { FileRouteFragment } from '@lib/datocms/types';
import { datocmsAssetsOrigin } from '@lib/datocms';
import fileProxyMapUntyped from './file-proxy-map.json';

export const getFileHref = (record: FileRouteFragment) => {
if (record.customSlug) {
return record.customSlug;
}

return record.file.url.replace(datocmsAssetsOrigin, '/files/');
};

type FileProxyMap = {
[key: string]: string;
};
const fileProxyMap = fileProxyMapUntyped as FileProxyMap;

export const getFileUrlBySlug = (slug: string) => {
return fileProxyMap[slug];
};
6 changes: 2 additions & 4 deletions src/lib/routing/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import type { FileRouteFragment, HomeRouteFragment, PageRouteFragment, SiteLocale } from '@lib/datocms/types';
import { datocmsAssetsOrigin } from '@lib/datocms';
import { getLocale } from '@lib/i18n';
import { getPagePath } from './page';
import { getFileHref } from './file';

export type RecordRoute =
| FileRouteFragment
| HomeRouteFragment
| PageRouteFragment;

export const getFileHref = (record: FileRouteFragment) => {
return record.file.url.replace(datocmsAssetsOrigin, '/files/');
};
export { getFileHref } from './file';

export const getHomeHref = ({ locale = getLocale() } = {}) => {
return `/${locale}/`;
Expand Down
3 changes: 2 additions & 1 deletion src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SiteLocale } from '@lib/i18n/types';
import { getRedirectTarget } from '@lib/routing/redirects';
import { datocmsEnvironment } from '@root/datocms-environment';
import { DATOCMS_READONLY_API_TOKEN, HEAD_START_PREVIEW_SECRET, HEAD_START_PREVIEW } from 'astro:env/server';
import { proxyFiles } from './middleware/proxy-files';

export const previewCookieName = 'HEAD_START_PREVIEW';

Expand Down Expand Up @@ -72,4 +73,4 @@ const redirects = defineMiddleware(async ({ request, redirect }, next) => {
return response;
});

export const onRequest = sequence(datocms, i18n, preview, redirects);
export const onRequest = sequence(datocms, i18n, preview, proxyFiles, redirects);
37 changes: 37 additions & 0 deletions src/middleware/proxy-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineMiddleware } from 'astro/middleware';
Copy link
Member Author

@jbmoelker jbmoelker Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the new atomic middleware handlers setup introduced in #222 . Should maybe be merged first?

import { getFileUrlBySlug } from '@lib/routing/file';

/**
* Proxy files middleware:
* If there is no matching route (404) and there is a file with a matching custom slug,
* proxy the file from DatoCMS assets.
*/
export const proxyFiles = defineMiddleware(async ({ request }, next) => {
const originalResponse = await next();

// only process 404 responses
if (originalResponse.status !== 404) {
return originalResponse;
}

const { pathname } = new URL(request.url);
const fileUrl = getFileUrlBySlug(pathname);
if (!fileUrl) {
return originalResponse;
}

const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) {
return originalResponse;
}

// Astro forces the original 404 status unless we use `X-Astro-Rewrite: true`
// @see https://github.com/withastro/roadmap/discussions/665#discussioncomment-6831528
return new Response(fileResponse.body, {
...fileResponse,
headers: {
...fileResponse.headers,
'X-Astro-Rewrite': 'true',
},
});
});
Loading