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 Page from "./Page";
|
||||
import ExternalRedirect from "./ExternalRedirect";
|
||||
import {routeData} from "./routeData";
|
||||
|
||||
export function createRoutes() {
|
||||
return (
|
||||
<Route path="/" component={Page}>
|
||||
{routeData.map(({path, component}) => {
|
||||
if (path === "/") {
|
||||
return <IndexRoute key={path} component={component()} />;
|
||||
} else {
|
||||
return <Route key={path} path={path} component={component()} />;
|
||||
{routeData.map(({path, contents}) => {
|
||||
switch (contents.type) {
|
||||
case "PAGE":
|
||||
if (path === "/") {
|
||||
return <IndexRoute key={path} component={contents.component()} />;
|
||||
} else {
|
||||
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>
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
/*::
|
||||
type RouteDatum = {|
|
||||
+path: string,
|
||||
+component: () => React$ComponentType<{||}>,
|
||||
+contents:
|
||||
| {|+type: "PAGE", +component: () => React$ComponentType<{||}>|}
|
||||
| {|+type: "EXTERNAL_REDIRECT", +redirectTo: string|},
|
||||
+title: string,
|
||||
+navTitle: ?string,
|
||||
|};
|
||||
|
@ -18,13 +20,19 @@ type RouteDatum = {|
|
|||
const routeData /*: $ReadOnlyArray<RouteDatum> */ = [
|
||||
{
|
||||
path: "/",
|
||||
component: () => require("./HomePage").default,
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./HomePage").default,
|
||||
},
|
||||
title: "SourceCred",
|
||||
navTitle: "Home",
|
||||
},
|
||||
{
|
||||
path: "/explorer",
|
||||
component: () => require("./credExplorer/App").default,
|
||||
contents: {
|
||||
type: "PAGE",
|
||||
component: () => require("./credExplorer/App").default,
|
||||
},
|
||||
title: "SourceCred explorer",
|
||||
navTitle: "Explorer",
|
||||
},
|
||||
|
@ -40,6 +48,7 @@ function resolveRouteFromPath(path /*: string */) /*: ?RouteDatum */ {
|
|||
};
|
||||
return routeData.filter(matches)[0] || null;
|
||||
}
|
||||
exports.resolveRouteFromPath = resolveRouteFromPath;
|
||||
|
||||
function resolveTitleFromPath(path /*: string */) /*: string */ {
|
||||
const route = resolveRouteFromPath(path);
|
||||
|
|
|
@ -5,47 +5,89 @@ import React from "react";
|
|||
import ReactDOMServer from "react-dom/server";
|
||||
import {match, RouterContext} from "react-router";
|
||||
|
||||
import Page from "./Page";
|
||||
import ExternalRedirect from "./ExternalRedirect";
|
||||
import {createRoutes} from "./createRoutes";
|
||||
import {resolveTitleFromPath} from "./routeData";
|
||||
import {resolveRouteFromPath, resolveTitleFromPath} from "./routeData";
|
||||
import dedent from "../util/dedent";
|
||||
|
||||
export default function render(
|
||||
locals: {+path: string, +assets: {[string]: string}},
|
||||
callback: (error: ?mixed, result?: string) => void
|
||||
): void {
|
||||
const bundlePath = locals.assets["main"];
|
||||
const url = locals.path;
|
||||
const routes = createRoutes();
|
||||
match({routes, location: url}, (error, redirectLocation, renderProps) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else if (renderProps) {
|
||||
const component = <RouterContext {...renderProps} />;
|
||||
const {html, css} = StyleSheetServer.renderStatic(() =>
|
||||
ReactDOMServer.renderToString(component)
|
||||
);
|
||||
const page = dedent`\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<title>${resolveTitleFromPath(url)}</title>
|
||||
<style>${require("./index.css")}</style>
|
||||
<style data-aphrodite>${css.content}</style>
|
||||
</head>
|
||||
<body style="overflow-y:scroll">
|
||||
<div id="root">${html}</div>
|
||||
<script src="${bundlePath}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
callback(null, page);
|
||||
const path = locals.path;
|
||||
{
|
||||
const route = resolveRouteFromPath(path);
|
||||
if (route && route.contents.type === "EXTERNAL_REDIRECT") {
|
||||
return renderRedirect(route.contents.redirectTo);
|
||||
} else {
|
||||
// This shouldn't happen because we should only be visiting
|
||||
// the right routes.
|
||||
throw new Error(`unexpected 404 from ${url}`);
|
||||
return renderStandardRoute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderStandardRoute() {
|
||||
const bundlePath = locals.assets["main"];
|
||||
const routes = createRoutes();
|
||||
match({routes, location: path}, (error, redirectLocation, renderProps) => {
|
||||
if (error) {
|
||||
callback(error);
|
||||
} else if (renderProps) {
|
||||
const component = <RouterContext {...renderProps} />;
|
||||
const {html, css} = StyleSheetServer.renderStatic(() =>
|
||||
ReactDOMServer.renderToString(component)
|
||||
);
|
||||
const page = dedent`\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<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>
|
||||
<script src="${bundlePath}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
callback(null, page);
|
||||
} else {
|
||||
// This shouldn't happen because we should only be visiting
|
||||
// the right routes.
|
||||
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