Add external redirect infrastructure (#550)
Summary: This patch extends our routing infrastructure to add support for _external_ redirects. It does not include dedicated support for site-internal redirects. Test Plan: Add an external redirect to `routeData`, like the following: ```diff diff --git a/src/app/routeData.js b/src/app/routeData.js index 83dff72..eaba130 100644 --- a/src/app/routeData.js +++ b/src/app/routeData.js @@ -36,6 +36,15 @@ const routeData /*: $ReadOnlyArray<RouteDatum> */ = [ title: "SourceCred explorer", navTitle: "Explorer", }, + { + path: "/discord-invite", + contents: { + type: "EXTERNAL_REDIRECT", + redirectTo: "https://discord.gg/tsBTgc9", + }, + title: "SourceCred Discord invite", + navTitle: null, + }, ]; exports.routeData = routeData; ``` Then: - run `yarn build`, and: - verify that the appropriate `index.html` file is correctly generated; - verify that opening the `index.html` file in a browser redirects you to the appropriate destination, even with JavaScript disabled; - verify that the link in the body of the HTML page is correct (easier to do if you remove the `<meta>` tag) - run `yarn start`, and: 1. use the React DevTools to change the “Explorer” link’s `to` prop from `/explorer` to `/discord-invite`; 2. click the link; and 3. verify that you are properly redirected. wchargin-branch: add-external-redirect
This commit is contained in:
parent
24a950629a
commit
8e062592ae
|
@ -0,0 +1,26 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class ExternalRedirect extends React.Component<{|
|
||||||
|
+redirectTo: string,
|
||||||
|
|}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h1>Redirecting…</h1>
|
||||||
|
<p>
|
||||||
|
Redirecting to:{" "}
|
||||||
|
<a href={this.props.redirectTo}>{this.props.redirectTo}</a>
|
||||||
|
</p>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// The server-rendered copy of this page will have a meta-refresh
|
||||||
|
// tag, but someone could still plausibly navigate to this page with
|
||||||
|
// the client-side renderer. In that case, we should redirect them.
|
||||||
|
window.location.href = this.props.redirectTo;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,16 +4,38 @@ import React from "react";
|
||||||
import {IndexRoute, Route} from "react-router";
|
import {IndexRoute, Route} from "react-router";
|
||||||
|
|
||||||
import Page from "./Page";
|
import Page from "./Page";
|
||||||
|
import ExternalRedirect from "./ExternalRedirect";
|
||||||
import {routeData} from "./routeData";
|
import {routeData} from "./routeData";
|
||||||
|
|
||||||
export function createRoutes() {
|
export function createRoutes() {
|
||||||
return (
|
return (
|
||||||
<Route path="/" component={Page}>
|
<Route path="/" component={Page}>
|
||||||
{routeData.map(({path, component}) => {
|
{routeData.map(({path, contents}) => {
|
||||||
|
switch (contents.type) {
|
||||||
|
case "PAGE":
|
||||||
if (path === "/") {
|
if (path === "/") {
|
||||||
return <IndexRoute key={path} component={component()} />;
|
return <IndexRoute key={path} component={contents.component()} />;
|
||||||
} else {
|
} else {
|
||||||
return <Route key={path} path={path} component={component()} />;
|
return (
|
||||||
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={path}
|
||||||
|
component={contents.component()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "EXTERNAL_REDIRECT":
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={path}
|
||||||
|
component={() => (
|
||||||
|
<ExternalRedirect redirectTo={contents.redirectTo} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new Error((contents.type: empty));
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -9,7 +9,9 @@
|
||||||
/*::
|
/*::
|
||||||
type RouteDatum = {|
|
type RouteDatum = {|
|
||||||
+path: string,
|
+path: string,
|
||||||
+component: () => React$ComponentType<{||}>,
|
+contents:
|
||||||
|
| {|+type: "PAGE", +component: () => React$ComponentType<{||}>|}
|
||||||
|
| {|+type: "EXTERNAL_REDIRECT", +redirectTo: string|},
|
||||||
+title: string,
|
+title: string,
|
||||||
+navTitle: ?string,
|
+navTitle: ?string,
|
||||||
|};
|
|};
|
||||||
|
@ -18,13 +20,19 @@ type RouteDatum = {|
|
||||||
const routeData /*: $ReadOnlyArray<RouteDatum> */ = [
|
const routeData /*: $ReadOnlyArray<RouteDatum> */ = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
contents: {
|
||||||
|
type: "PAGE",
|
||||||
component: () => require("./HomePage").default,
|
component: () => require("./HomePage").default,
|
||||||
|
},
|
||||||
title: "SourceCred",
|
title: "SourceCred",
|
||||||
navTitle: "Home",
|
navTitle: "Home",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/explorer",
|
path: "/explorer",
|
||||||
|
contents: {
|
||||||
|
type: "PAGE",
|
||||||
component: () => require("./credExplorer/App").default,
|
component: () => require("./credExplorer/App").default,
|
||||||
|
},
|
||||||
title: "SourceCred explorer",
|
title: "SourceCred explorer",
|
||||||
navTitle: "Explorer",
|
navTitle: "Explorer",
|
||||||
},
|
},
|
||||||
|
@ -40,6 +48,7 @@ function resolveRouteFromPath(path /*: string */) /*: ?RouteDatum */ {
|
||||||
};
|
};
|
||||||
return routeData.filter(matches)[0] || null;
|
return routeData.filter(matches)[0] || null;
|
||||||
}
|
}
|
||||||
|
exports.resolveRouteFromPath = resolveRouteFromPath;
|
||||||
|
|
||||||
function resolveTitleFromPath(path /*: string */) /*: string */ {
|
function resolveTitleFromPath(path /*: string */) /*: string */ {
|
||||||
const route = resolveRouteFromPath(path);
|
const route = resolveRouteFromPath(path);
|
||||||
|
|
|
@ -5,18 +5,30 @@ import React from "react";
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import {match, RouterContext} from "react-router";
|
import {match, RouterContext} from "react-router";
|
||||||
|
|
||||||
|
import Page from "./Page";
|
||||||
|
import ExternalRedirect from "./ExternalRedirect";
|
||||||
import {createRoutes} from "./createRoutes";
|
import {createRoutes} from "./createRoutes";
|
||||||
import {resolveTitleFromPath} from "./routeData";
|
import {resolveRouteFromPath, resolveTitleFromPath} from "./routeData";
|
||||||
import dedent from "../util/dedent";
|
import dedent from "../util/dedent";
|
||||||
|
|
||||||
export default function render(
|
export default function render(
|
||||||
locals: {+path: string, +assets: {[string]: string}},
|
locals: {+path: string, +assets: {[string]: string}},
|
||||||
callback: (error: ?mixed, result?: string) => void
|
callback: (error: ?mixed, result?: string) => void
|
||||||
): void {
|
): void {
|
||||||
|
const path = locals.path;
|
||||||
|
{
|
||||||
|
const route = resolveRouteFromPath(path);
|
||||||
|
if (route && route.contents.type === "EXTERNAL_REDIRECT") {
|
||||||
|
return renderRedirect(route.contents.redirectTo);
|
||||||
|
} else {
|
||||||
|
return renderStandardRoute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStandardRoute() {
|
||||||
const bundlePath = locals.assets["main"];
|
const bundlePath = locals.assets["main"];
|
||||||
const url = locals.path;
|
|
||||||
const routes = createRoutes();
|
const routes = createRoutes();
|
||||||
match({routes, location: url}, (error, redirectLocation, renderProps) => {
|
match({routes, location: path}, (error, redirectLocation, renderProps) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
callback(error);
|
callback(error);
|
||||||
} else if (renderProps) {
|
} else if (renderProps) {
|
||||||
|
@ -31,7 +43,7 @@ export default function render(
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<title>${resolveTitleFromPath(url)}</title>
|
<title>${resolveTitleFromPath(path)}</title>
|
||||||
<style>${require("./index.css")}</style>
|
<style>${require("./index.css")}</style>
|
||||||
<style data-aphrodite>${css.content}</style>
|
<style data-aphrodite>${css.content}</style>
|
||||||
</head>
|
</head>
|
||||||
|
@ -45,7 +57,37 @@ export default function render(
|
||||||
} else {
|
} else {
|
||||||
// This shouldn't happen because we should only be visiting
|
// This shouldn't happen because we should only be visiting
|
||||||
// the right routes.
|
// the right routes.
|
||||||
throw new Error(`unexpected 404 from ${url}`);
|
throw new Error(`unexpected 404 from ${path}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRedirect(redirectTo: string) {
|
||||||
|
const component = (
|
||||||
|
<Page>
|
||||||
|
<ExternalRedirect redirectTo={redirectTo} />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
const {html, css} = StyleSheetServer.renderStatic(() =>
|
||||||
|
ReactDOMServer.renderToStaticMarkup(component)
|
||||||
|
);
|
||||||
|
const page = dedent`\
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<meta http-equiv="refresh" content="0;url=${redirectTo}" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
|
<title>${resolveTitleFromPath(path)}</title>
|
||||||
|
<style>${require("./index.css")}</style>
|
||||||
|
<style data-aphrodite>${css.content}</style>
|
||||||
|
</head>
|
||||||
|
<body style="overflow-y:scroll">
|
||||||
|
<div id="root">${html}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
callback(null, page);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue