Skip to content

Commit

Permalink
refactor: middleware as atomic files + docs (#222)
Browse files Browse the repository at this point in the history
l# Changes

Refactors middleware to atomic files (`src/middleware/...`), so
middleware stays maintainable when adding more (like the upcoming file
proxy middleware) and has a structure suitable to add tests to (in
`src/middleware/tests/` like done in `src/lib/datocms/tests/`).

# Associated issue

N/A

# How to test

1. Open preview link
2. Verify the different middleware handlers (`i18n`, `redirects`) still
work (`preview` can't be tested on deploy preview, unless we list it as
a preview branch; `datocms` is hard to check on deploy preview)
3. Run locally
4. Verify the different middleware handlers (`datocms`, `i18n`,
`preview`, `redirects`) still work

# Checklist

- [x] I have performed a self-review of my own code
- [x] I have made sure that my PR is easy to review (not too big,
includes comments)
- [x] I have made updated relevant documentation files (in project
README, docs/, etc)
- ~I have added a decision log entry if the change affects the
architecture or changes a significant technology~
- [x] I have notified a reviewer

<!-- Please strike through and check off all items that do not apply
(rather than removing them) -->

---------

Co-authored-by: Jurgen Beliën <[email protected]>
  • Loading branch information
jbmoelker and jurgenbelien authored Jan 12, 2025
1 parent bac41ed commit 8af3de9
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 78 deletions.
3 changes: 3 additions & 0 deletions docs/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Inside of this project, you'll see the following folders and files:
│ │ └── Default.astro
│ ├── lib/
│ │ └── some-helper-function.ts
│ ├── middleware/
│ │ └── some-req-res-interceptor.ts
│ └── pages/
│ ├── api/
| | └── some-dynamic-endpoint.ts
Expand All @@ -44,6 +46,7 @@ Inside of this project, you'll see the following folders and files:
- `blocks/` - Blocks are a specific set of components which have a complementary content [Block](https://www.datocms.com/docs/content-modelling/blocks) in DatoCMS and therefore have a paired GraphQL fragment file.
- `layouts/` - [Layouts](https://docs.astro.build/en/core-concepts/layouts/) are Astro components used to provide a reusable UI structure, such as a page template.
- `lib/` - Shared logic and utility helpers, like `datocms`, `i18n` and `routing`.
- `middleware` - intercept and (possibly) transform requests & responses. See [Astro middleware](https://docs.astro.build/en/guides/middleware/).
- `assets/` - is for assets that require a build step. See [Assets](./assets.md).
- `public/` is for any static assets that are served as-is. See [Assets](./assets.md).
- `config/` bundles all our configuration files (like DatoCMS migrations), so the project root doesn't become too cluttered.
Expand Down
6 changes: 5 additions & 1 deletion docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

**Head Start leverages [Astro file-based routing](https://docs.astro.build/en/core-concepts/routing/#_top) combined with Cloudflare features for redirects and page not found behaviour. The setup is enhanced with i18n routing, API routing, nested page routing and helpers to resolve routes.**

## Routing middleware

Head Start leverages [Astro middleware](https://docs.astro.build/en/guides/middleware/) to add checks, data and error handling to routes. See [`src/middleware/` directory for details](../src/middleware/).

## I18n routes

Head Start supports multi-language websites with localised routing (`/:locale/page/to/page/`). See [i18n configuration and routing](./i18n.md).
Expand Down Expand Up @@ -57,4 +61,4 @@ export function GET ({ locals }) {
const { city, latitude, longitude } = locals.runtime.cf;
return new Response(JSON.stringify({ city, latitude, longitude }, null, 2));
}
```
```
75 changes: 0 additions & 75 deletions src/middleware.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/middleware/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Middleware

**[Astro supports middleware](https://docs.astro.build/en/guides/middleware/) that allows you to intercept requests and responses and inject behaviors dynamically every time a page or endpoint is about to be rendered.**
12 changes: 12 additions & 0 deletions src/middleware/datocms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineMiddleware } from 'astro:middleware';
import { datocmsEnvironment } from '@root/datocms-environment';
import { DATOCMS_READONLY_API_TOKEN } from 'astro:env/server';

export const datocms = defineMiddleware(async ({ locals }, next) => {
Object.assign(locals, {
datocmsEnvironment,
datocmsToken: DATOCMS_READONLY_API_TOKEN
});
const response = await next();
return response;
});
23 changes: 23 additions & 0 deletions src/middleware/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineMiddleware } from 'astro:middleware';
import { defaultLocale, locales, setLocale } from '@lib/i18n';
import type { SiteLocale } from '@lib/i18n/types';

/**
* i18n middleware:
* ensure a locale is always defined in the context.params object
*/
export const i18n = defineMiddleware(async ({ params, request }, next) => {
if (!params.locale) {
// if the locale param is unavailable, it didn't match a [locale]/* route
// so we attempt to extract the locale from the URL and fallback to the default locale
const pathLocale = new URL(request.url).pathname.split('/')[1];
const locale = locales.includes(pathLocale as SiteLocale)
? pathLocale
: defaultLocale;
Object.assign(params, { locale });
}
setLocale(params.locale as SiteLocale);

const response = await next();
return response;
});
13 changes: 13 additions & 0 deletions src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { sequence } from 'astro:middleware';

import { datocms } from './datocms';
import { i18n } from './i18n';
import { preview } from './preview';
import { redirects } from './redirects';

export const onRequest = sequence(
datocms,
i18n,
preview,
redirects
);
27 changes: 27 additions & 0 deletions src/middleware/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineMiddleware } from 'astro:middleware';
import { HEAD_START_PREVIEW_SECRET, HEAD_START_PREVIEW } from 'astro:env/server';

export const previewCookieName = 'HEAD_START_PREVIEW';

export const hashSecret = async (secret: string) => {
const msgUint8 = new TextEncoder().encode(secret);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
};

export const preview = defineMiddleware(async ({ cookies, locals }, next) => {
const previewSecret = HEAD_START_PREVIEW_SECRET!;
Object.assign(locals, {
isPreview: HEAD_START_PREVIEW,
isPreviewAuthOk: Boolean(previewSecret) && cookies.get(previewCookieName)?.value === await hashSecret(previewSecret),
previewSecret
});
const response = await next();

if (locals.isPreview) {
response.headers.set('X-Robots-Tag', 'noindex');
}

return response;
});
19 changes: 19 additions & 0 deletions src/middleware/redirects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineMiddleware } from 'astro:middleware';
import { getRedirectTarget } from '@lib/routing/redirects';

/**
* Redirects middleware:
* If there is no matching route (404) and there is a matching redirect rule,
* redirect to the target URL with the specified status code.
*/
export const redirects = defineMiddleware(async ({ request, redirect }, next) => {
const response = await next();
if (response.status === 404) {
const { pathname } = new URL(request.url);
const redirectTarget = getRedirectTarget(pathname);
if (redirectTarget) {
return redirect(redirectTarget.url, redirectTarget.statusCode);
}
}
return response;
});
2 changes: 1 addition & 1 deletion src/pages/api/preview/enter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { APIRoute } from 'astro';
import { hashSecret, previewCookieName } from '../../../middleware';
import { hashSecret, previewCookieName } from '@middleware/preview';
import { PUBLIC_IS_PRODUCTION } from 'astro:env/server';

export const prerender = false;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/preview/exit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { APIRoute } from 'astro';
import { previewCookieName } from '../../../middleware';
import { previewCookieName } from '@middleware/preview';
import { cookiePath } from './enter';

export const prerender = false;
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@components/*": ["src/components/*"],
"@layouts/*": ["src/layouts/*"],
"@lib/*": ["src/lib/*"],
"@middleware/*": ["src/middleware/*"],
"@pages/*": ["src/pages/*"],
"@root/*": ["./*"],
"@src/*": ["src/*"]
Expand Down

0 comments on commit 8af3de9

Please sign in to comment.