Skip to content

Commit

Permalink
Merge pull request #209 from kkeene1/canary
Browse files Browse the repository at this point in the history
feat: redirects
  • Loading branch information
tj-kev committed Dec 21, 2022
2 parents bc1bb7a + 8a89107 commit 4c162a5
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 7 deletions.
22 changes: 22 additions & 0 deletions configs/webpack.worker.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,35 @@ const defaultLoaders = require("./loaders");
const { nanoid } = require("nanoid");
const fs = require("fs");

const CONFIG_FILE = "flareact.config.js";

const dev = !!process.env.WORKER_DEV;
const isServer = true;
const projectDir = process.cwd();
const flareact = flareactConfig(projectDir);

const outPath = path.resolve(projectDir, "out");

if (flareact.redirects) {
async function writeConfigFile() {
const redirects = await flareact.redirects();
fs.writeFileSync(
`${projectDir}/node_modules/flareact/src/worker/${CONFIG_FILE}`,
`export const config = { redirects: ${JSON.stringify(
redirects,
null,
4
)}}`
);
}
writeConfigFile();
} else {
fs.writeFileSync(
`${projectDir}/node_modules/flareact/src/worker/${CONFIG_FILE}`,
`export const config = {}`
);
}

const buildManifest = dev
? {}
: fs.readFileSync(
Expand Down
43 changes: 43 additions & 0 deletions docs/custom-webpack-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,46 @@ module.exports = {
},
};
```

## Redirects

With redirect you can redirect an incoming request path to a different destination path.

Example usage of `redirects`:

```js
module.exports = {
async redirects() {
return [
{
source: '/about',
destination: '/',
permanent: true,
},
]
},
};
```
It expects an array to be returned holding objects with source, destination, and permanent properties:

- `source` is the incoming request path pattern.
- `destination` is the location you want to route to. This can either be a path or a full url.
- `permanent` if the redirect is permanent or not.

URL parameters provided in the source request can be transferred to the redirect like so:

```js
module.exports = {
async redirects() {
return [
{
source: '/blog/[slug]',
destination: '/news/[slug]',
permanent: true,
},
]
},
};
```

Note that if redirects for a page are defined in both the edge props and the custom webpack config then the latter will be given priority.
43 changes: 43 additions & 0 deletions docs/data-fetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,49 @@ export default function Posts({ posts, notFound }) {
}
```

## Redirects

Dynamic redirects can be returned if required, for example to prevent an unauthenticated user from accessing a secure page.

```js
export async function getEdgeProps() {
const signedIn = await isSignedIn();

if (!signedIn) {
return {
redirect: {
destination: "/",
permanent: false
}
};
}

return {
props: {}
}
}
```

The redirect object should contain two parameters:

- `destination` is the location you want to route to. This can either be a path or a full url.
- `permanent` if the redirect is permanent or not.

Redirects will work on both hard and soft loads. URL parameters can be used in the destination provided they are part of the original request url.

```js
export async function getEdgeProps() {
return {
redirect: {
destination: "/news/[slug]",
permanent: true
}
};
}
```

Note that if redirects for a page are defined in both the edge props and the custom webpack config then the latter will be given priority.

## Additional Notes

A couple things to note about `getEdgeProps`:
Expand Down
5 changes: 4 additions & 1 deletion src/client/page-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default class PageLoader {
async loadPageProps(pagePath) {
const url = getPagePropsUrl(pagePath);
const res = await fetch(url);

return await res.json();
}

Expand Down Expand Up @@ -113,7 +114,9 @@ export default class PageLoader {
}

loadScript(path) {
const url = path
const prefix =
process.env.NODE_ENV === "production" ? "" : "http://localhost:8080";
const url = prefix + path;

if (document.querySelector(`script[src^="${url}"]`)) return;

Expand Down
3 changes: 3 additions & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const TEMPORARY_REDIRECT_STATUS = 307;
export const PERMANENT_REDIRECT_STATUS = 308;
export const CONFIG_FILE = 'flareact.config.js';
13 changes: 12 additions & 1 deletion src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ export function RouterProvider({
const { pageProps } = await pageLoader.loadPageProps(
normalizedAsPath
);

if (pageProps.redirect && pageProps.redirect.destination) {
if (pageProps.redirect.destination.startsWith("/")) {
router.push(pageProps.redirect.destination, pageProps.redirect.as);
} else {
window.location.href = pageProps.redirect.as;
}

return;
}

const revalidateSeconds = getRevalidateValue(pageProps);
const expiry = generatePagePropsExpiry(revalidateSeconds);

Expand Down Expand Up @@ -248,4 +259,4 @@ export function useRouter() {

export function normalizePathname(pathname) {
return pathname === "/" ? "/index" : pathname;
}
}
84 changes: 80 additions & 4 deletions src/worker/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { normalizePathname } from "../router";
import { getPage, getPageProps, PageNotFoundError } from "./pages";
import { getPage, getPageProps, PageNotFoundError, resolvePagePath } from "./pages";
import { render } from "./render";
import {
PERMANENT_REDIRECT_STATUS,
TEMPORARY_REDIRECT_STATUS,
} from "../constants";
import { config } from "./flareact.config";

const CACHEABLE_REQUEST_METHODS = ["GET", "HEAD"];

Expand All @@ -20,14 +25,27 @@ export async function handleRequest(event, context, fallback) {
}

try {
if (pathname.startsWith("/_flareact/props")) {
const pagePath = pathname.replace(/\/_flareact\/props|\.json/g, "");
const pagePath = pathname.replace(/\/_flareact\/props|\.json/g, "");

const normalizedPathname = normalizePathname(pagePath);
const resolvedPage = resolvePagePath(normalizedPathname, context.keys());
const resolvedPagePath = resolvedPage ? resolvedPage.pagePath : null;

let reducedRedirect;

if (config && typeof config.redirects !== "undefined") {
reducedRedirect = config.redirects.find(
(item) => item.source === normalizedPathname || item.source === resolvedPagePath
);
}

if (pathname.startsWith("/_flareact/props")) {
return await handleCachedPageRequest(
event,
context,
pagePath,
query,
reducedRedirect,
(_, props) => {
return new Response(
JSON.stringify({
Expand All @@ -44,7 +62,25 @@ export async function handleRequest(event, context, fallback) {
);
}

const normalizedPathname = normalizePathname(pathname);
if (reducedRedirect) {
const statusCode = reducedRedirect.permanent
? PERMANENT_REDIRECT_STATUS
: TEMPORARY_REDIRECT_STATUS;
let destination = reducedRedirect.destination;

if (resolvedPage && resolvedPage.params) {
for (const param in resolvedPage.params) {
const regex = new RegExp(`\\[${param}\\]`);

destination = destination.replace(regex, resolvedPage.params[param]);
}
}

const headers = {
Location: destination
};
return new Response(null, { status: statusCode, headers: headers });
}

if (pageIsApi(normalizedPathname)) {
const page = getPage(normalizedPathname, context);
Expand All @@ -70,6 +106,7 @@ export async function handleRequest(event, context, fallback) {
context,
normalizedPathname,
query,
null,
async (page, props) => {
const html = await render({
page,
Expand Down Expand Up @@ -111,8 +148,11 @@ async function handleCachedPageRequest(
context,
normalizedPathname,
query,
staticRedirect,
generateResponse
) {
const url = new URL(event.request.url);
const { pathname } = url;
const cache = caches.default;
const cacheKey = getCacheKey(event.request);
const cachedResponse = await cache.match(cacheKey);
Expand All @@ -122,6 +162,42 @@ async function handleCachedPageRequest(
const page = getPage(normalizedPathname, context);
const props = await getPageProps(page, query, event);

if (staticRedirect) {
props.redirect = staticRedirect;
}

/*
* Redirect value to allow redirecting in the edge. This is an optional value.
*/
if (props && typeof props.redirect !== "undefined") {
const { redirect = {} } = props;
const statusCode =
redirect.statusCode ||
(redirect.permanent
? PERMANENT_REDIRECT_STATUS
: TEMPORARY_REDIRECT_STATUS);

let asPath = redirect.destination;

if (page.params) {
for (const param in page.params) {
const regex = new RegExp(`\\[${param}\\]`);

asPath = asPath.replace(regex, page.params[param]);
}
}

if (!pathname.startsWith("/_flareact/props")) {
const headers = {
Location: asPath
};
return new Response(null, { status: statusCode, headers: headers });
} else {
redirect.as = asPath;
props.redirect = redirect;
}
}

let response = await generateResponse(page, props);

// Cache by default
Expand Down
3 changes: 2 additions & 1 deletion src/worker/pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export async function getPageProps(page, query, event) {
};

if (fetcher) {
const { props, notFound, customHeaders, revalidate } = await fetcher({
const { props, notFound, customHeaders, revalidate, redirect } = await fetcher({
params,
query: queryObject,
event,
Expand All @@ -162,6 +162,7 @@ export async function getPageProps(page, query, event) {
notFound,
customHeaders,
revalidate,
redirect,
};
}

Expand Down

0 comments on commit 4c162a5

Please sign in to comment.