xref: /expo/docs/pages/_error.tsx (revision bb8f4f99)
1import * as Sentry from '@sentry/browser';
2import React from 'react';
3
4import { VERSIONS } from '~/constants/versions';
5
6const REDIRECT_SUFFIX = '?redirected';
7
8type State = {
9  notFound: boolean;
10  redirectPath?: string;
11  redirectFailed: boolean;
12};
13export default class Error extends React.Component<object, State> {
14  state: State = {
15    notFound: false,
16    redirectPath: undefined,
17    redirectFailed: false,
18  };
19
20  componentDidMount() {
21    this._maybeRedirect();
22  }
23
24  _maybeRedirect = () => {
25    if (typeof window === 'undefined') {
26      return;
27    }
28
29    const { pathname } = window.location;
30
31    if (window.location.search === REDIRECT_SUFFIX) {
32      Sentry.captureMessage(`Redirect failed`);
33      this.setState({ redirectFailed: true });
34      return;
35    }
36
37    let redirectPath = pathname;
38
39    // index.html is no longer a thing in our docs
40    if (pathIncludesIndexHtml(redirectPath)) {
41      redirectPath = redirectPath.replace('index.html', '');
42    }
43
44    // Remove the .html extension if it is included in the path
45    if (pathIncludesHtmlExtension(redirectPath)) {
46      redirectPath = redirectPath.replace('.html', '');
47    }
48
49    // Unsure why this is happening, but sometimes URLs end up with /null in
50    // the last path part
51    // https://docs.expo.io/versions/latest/sdk/overview/null
52    if (endsInNull(redirectPath)) {
53      redirectPath = redirectPath.replace(/null$/, '');
54    }
55
56    // Add a trailing slash if there is not one
57    if (redirectPath[redirectPath.length - 1] !== '/') {
58      redirectPath = `${redirectPath}/`;
59    }
60
61    // A list of pages we know are renamed and can redirect
62    if (RENAMED_PAGES[redirectPath]) {
63      redirectPath = RENAMED_PAGES[redirectPath];
64    }
65
66    // Catch any unversioned paths which are also renamed
67    if (redirectPath.startsWith('/versions/latest/')) {
68      const unversionedPath = redirectPath.replace('/versions/latest', '');
69      if (RENAMED_PAGES[unversionedPath]) {
70        redirectPath = RENAMED_PAGES[unversionedPath];
71      }
72    }
73
74    // Check if the version is documented, replace it with latest if not
75    if (!isVersionDocumented(redirectPath)) {
76      redirectPath = replaceVersionWithLatest(redirectPath);
77    }
78
79    // Remove versioning from path if this section is no longer versioned
80    if (isVersionedPath(redirectPath) && !pathRequiresVersioning(redirectPath)) {
81      redirectPath = removeVersionFromPath(redirectPath);
82    }
83
84    // Catch any redirects to sdk paths without versions and send to the latest version
85    if (redirectPath.startsWith('/sdk/')) {
86      redirectPath = `/versions/latest${redirectPath}`;
87    }
88
89    // If a page is missing for react-native paths we redirect to react-native docs
90    if (redirectPath.match(/\/versions\/.*\/react-native\//)) {
91      const pathParts = redirectPath.split('/');
92      const page = pathParts[pathParts.length - 2];
93      redirectPath = `https://reactnative.dev/docs/${page}`;
94    }
95
96    // Remove version from path if the version is still supported, to redirect to the root
97    if (isVersionedPath(redirectPath) && isVersionDocumented(redirectPath)) {
98      redirectPath = `/versions/${getVersionFromPath(redirectPath)}/`;
99    }
100
101    if (redirectPath !== pathname) {
102      this.setState({ redirectPath });
103      return;
104    }
105
106    // We are confident now that we can render a not found error
107    this.setState({ notFound: true });
108    Sentry.captureMessage(`Page not found (404)`);
109  };
110
111  componentDidUpdate(prevProps: object, prevState: State) {
112    if (prevState.redirectPath !== this.state.redirectPath && typeof window !== 'undefined') {
113      // Let people actually read the carefully crafted message and absorb the
114      // cool emoji selection, they can just click through if they want speed
115      setTimeout(() => {
116        window.location.href = `${this.state.redirectPath}?redirected`;
117      }, 1200);
118    }
119  }
120
121  render() {
122    return (
123      <div
124        style={{
125          display: 'flex',
126          flex: 1,
127          alignItems: 'center',
128          justifyContent: 'center',
129          height: '100vh',
130          flexDirection: 'column',
131        }}>
132        {this._renderContents()}
133      </div>
134    );
135  }
136
137  _renderContents = () => {
138    const styles = {
139      description: {
140        textAlign: 'center' as const,
141        maxWidth: 450,
142        marginHorizontal: 30,
143        lineHeight: '1.7em',
144      },
145      link: {
146        textAlign: 'center' as const,
147        marginTop: 20,
148      },
149    };
150
151    if (this.state.redirectPath) {
152      return (
153        <>
154          <h1>��️‍♀️️</h1>
155          <p style={styles.description}>
156            Hold tight, we are redirecting you to where we think this URL was intended to take you!
157          </p>
158          <p style={styles.link}>
159            <a id="redirect-link" href={this.state.redirectPath}>
160              Click here to possibly go there more quickly!
161            </a>
162          </p>
163        </>
164      );
165    } else if (this.state.redirectFailed) {
166      return (
167        <>
168          <h1>��️</h1>
169          <p style={styles.description} id="__redirect_failed">
170            We took an educated guess and tried to direct you to the right page, but it seems that
171            did not work out! Maybe it doesn't exist anymore! ��
172          </p>
173          <p style={styles.link}>
174            <a href="/">Go to the Expo documentation, you can try searching there</a>
175          </p>
176        </>
177      );
178    } else if (this.state.notFound) {
179      return (
180        <>
181          <h1>��</h1>
182          <p style={styles.description} id="__not_found">
183            <strong style={{ fontWeight: 'bold' }}>Uh oh, we couldn't find this page!</strong> We've
184            made note of this and will investigate, but it's possible that the page you're looking
185            for no longer exists!
186          </p>
187          <p style={styles.link}>
188            <a href="/">Go to the Expo documentation, you can try searching there</a>
189          </p>
190        </>
191      );
192    } else {
193      // Render nothing statically
194    }
195  };
196}
197
198function getVersionFromPath(path: string) {
199  const pathParts = path.split(/\//);
200  // eg: ["", "versions", "v32.0.0", ""]
201  return pathParts[2];
202}
203
204// Filter unversioned and latest out, so we end up with v34, etc.
205const supportedVersions = VERSIONS.filter(v => v.match(/^v/));
206
207// Return true if the version is still included in documentation
208function isVersionDocumented(path: string) {
209  return supportedVersions.includes(getVersionFromPath(path));
210}
211
212function pathIncludesHtmlExtension(path: string) {
213  return !!path.match(/\.html$/);
214}
215
216function pathIncludesIndexHtml(path: string) {
217  return !!path.match(/index\.html$/);
218}
219
220const VERSION_PART_PATTERN = `(latest|unversioned|v\\d+\\.\\d+.\\d+)`;
221const VERSIONED_PATH_PATTERN = `^\\/versions\\/${VERSION_PART_PATTERN}`;
222const SDK_PATH_PATTERN = `${VERSIONED_PATH_PATTERN}/sdk`;
223const REACT_NATIVE_PATH_PATTERN = `${VERSIONED_PATH_PATTERN}/react-native`;
224
225// Check if path is valid (matches /versions/some-valid-version-here/)
226function isVersionedPath(path: string) {
227  return !!path.match(new RegExp(VERSIONED_PATH_PATTERN));
228}
229
230// Replace an unsupported SDK version with latest
231function replaceVersionWithLatest(path: string) {
232  return path.replace(new RegExp(VERSION_PART_PATTERN), 'latest');
233}
234
235/**
236 * Determine if the path requires versioning, if not we can remove the versioned prefix from the path.
237 * The following paths require versioning:
238 *   - `/versions/<version>/sdk/**`, pages within the Expo SDK docs.
239 *   - `/versions/<version>/react-native/**`, pages within the React Native API docs.
240 *   - `/versions/<version>/`, the index of a specific Expo SDK version.
241 * All other paths shouldn't require versioning, some of them are:
242 *   - `/versions/<version>/workflow/expo-cli/, moved outside versioned folders.
243 *   - `/versions/<version>/guides/assets/, moved outside versioned folders.
244 */
245function pathRequiresVersioning(path: string) {
246  const isExpoSdkPage = path.match(new RegExp(SDK_PATH_PATTERN));
247  const isExpoSdkIndexPage = path.match(new RegExp(VERSIONED_PATH_PATTERN + '/$'));
248  const isReactNativeApiPage = path.match(new RegExp(REACT_NATIVE_PATH_PATTERN));
249
250  return isExpoSdkIndexPage || isExpoSdkPage || isReactNativeApiPage;
251}
252
253function removeVersionFromPath(path: string) {
254  return path.replace(new RegExp(VERSIONED_PATH_PATTERN), '');
255}
256
257// Not sure why this happens but sometimes the URL ends in /null
258function endsInNull(path: string) {
259  return !!path.match(/\/null$/);
260}
261
262// Simple remapping of renamed pages, similar to in deploy.sh but in some cases,
263// for reasons I'm not totally clear on, those redirects do not work
264const RENAMED_PAGES: Record<string, string> = {
265  '/introduction/project-lifecycle/': '/introduction/managed-vs-bare/',
266  '/guides/': '/workflow/exploring-managed-workflow/',
267  '/versions/latest/sdk/': '/versions/latest/',
268  '/versions/latest/sdk/overview/': '/versions/latest/',
269  '/guides/building-standalone-apps/': '/distribution/building-standalone-apps/',
270  '/guides/genymotion/': '/workflow/android-studio-emulator/',
271  '/workflow/upgrading-expo/': '/workflow/upgrading-expo-sdk-walkthrough/',
272  '/workflow/create-react-native-app/': '/workflow/glossary-of-terms/#create-react-native-app',
273  '/expokit/': '/expokit/overview/',
274  '/guides/detach/': '/expokit/eject/',
275  '/expokit/detach/': '/expokit/eject/',
276
277  // Lots of old links pointing to guides when they have moved elsewhere
278  '/guides/configuration/': '/workflow/configuration/',
279  '/guides/expokit/': '/expokit/overview/',
280  '/guides/publishing/': '/workflow/publishing/',
281  '/guides/linking/': '/workflow/linking/',
282  '/guides/up-and-running/': '/get-started/installation/',
283  '/guides/debugging/': '/workflow/debugging/',
284  '/guides/logging/': '/workflow/logging/',
285  '/introduction/troubleshooting-proxies/': '/guides/troubleshooting-proxies/',
286  '/introduction/running-in-the-browser/': '/guides/running-in-the-browser/',
287
288  // Changes from redoing the getting started workflow, SDK35+
289  '/workflow/up-and-running/': '/get-started/installation/',
290  '/introduction/additional-resources/': '/next-steps/additional-resources/',
291  '/introduction/already-used-react-native/': '/workflow/already-used-react-native/',
292  '/introduction/community/': '/next-steps/community/',
293  '/introduction/installation/': '/get-started/installation/',
294  '/versions/latest/overview/': '/versions/latest/',
295  '/versions/latest/introduction/installation/': '/get-started/installation/',
296  '/workflow/exploring-managed-workflow/': '/introduction/walkthrough/',
297
298  // Move overview to index
299  '/versions/v37.0.0/sdk/overview/': '/versions/v37.0.0/',
300
301  // Errors and debugging is better suited for getting started than tutorial
302  '/tutorial/errors/': '/get-started/errors/',
303
304  // Additional redirects based on Sentry (04/28/2020)
305  '/next-steps/installation/': '/get-started/installation/',
306  '/guides/release-channels/': '/distribution/release-channels/',
307
308  // Redirects based on Next 9 upgrade (09/11/2020)
309  '/api/': '/versions/latest/',
310
311  // Redirect to expand Expo Accounts and permissions
312  '/guides/account-permissions/': '/accounts/personal/',
313
314  // Redirects based on Sentry (11/26/2020)
315  '/guides/push-notifications/': '/push-notifications/overview/',
316  '/guides/using-fcm/': '/push-notifications/using-fcm/',
317
318  // Renaming a submit section
319  '/submit/submit-ios': '/submit/ios/',
320  '/submit/submit-android': '/submit/android/',
321
322  // Fundamentals had too many things
323  '/workflow/linking/': '/guides/linking/',
324  '/workflow/how-expo-works/': '/guides/how-expo-works/',
325
326  // Archive unused pages
327  '/guides/notification-channels/': '/archived/notification-channels/',
328};
329