301 redirects stored in WordPress database

Nice. Thanks Mario!

I have kept thinking about this as well and I realized that the current solution to know when there is a 301 redirection in the server is actually storing the information in Frontity as well, in state.source.data.

I think we should use the same system for all the 301 redirections, which means that the redirections created in Frontity also should end up being stored in state.source.data.

So if people want to add redirections stored in Frontity now, they can do it this way:

  • Single redirection populating state.source.data directly:

    const state = {
      source: {
        data: {
          "/old-url": {
            isReady: true,
            isRedirection: true,
            is301: true,
            redirectionStatus: 301,
            location: "/new-url",
          },
        },
      },
    };
    

    This manual population is something I have started recommending recently to create custom pages that are not present in WordPress: How to create Custom Pages? because it’s simpler than creating a code handler.

  • Pattern/RegExp redirection using a handler:

    const categoryRedirection = {
      pattern: "/category/:slug",
      priority: 5,
      func: ({ link, params }) => {
        state.source.data[link].isReady = true;
        state.source.data[link].isRedirection = true;
        state.source.data[link].is301 = true;
        state.source.data[link].redirectionStatus = 301;
        state.source.data[link].location = `/categoria/${params.slug}`;
      },
    };
    

That way all the redirections will be stored in the same place and when packages want to know if there is a redirection in a URL, they can simply check state.source.data.

The hypothetical @frontity/redirections package can have simpler settings, like these:

const settings = {
  packages: [
    {
      name: "@frontity/redirections",
      state: {
        redirections: {
          "/some-post": "/other-post",
          "/some/custom/:url": "/other/custom/${params.url}",
          "/more/custom/:url": {
            destination: "/other/custom/${params.url}",
            status: 302,
          },
          "RegExp:\\/some-post?photo=(?<id>[^&$]+)>":
            "/some-post/photo-${params.id}",
        },
      },
    },
  ],
};

But internally, it will add that info to state.source.data either manually (for single redirects) or with handlers (for patterns/RegExps) to be consistent with the server redirections.

So with the current implementation, we have added:

  • Support in tiny-router for:

    • Checking if the data object is a redirection in the server
      If it is, changing the ctx.status in the same way it is changing it for errors.

    • Checking if the data object is a redirection in the client
      If it is, it does an additional actions.router.set to move to the correct URL.

  • Support in wp-source for “server-stored” or “source-stored” redirections:

    • Checking if the request matches a regexp or is a 404
      If it is, doing an additional request against the state.source.url to see if there is a redirection and storing it in the data object for other packages to do what they need (like the router).

Also, @santosguillamot has talked about the possibility of creating other redirection packages in the future. That package would also add redirections to the data objects, although they won’t be related to the source, but to other origins (Frontity settings, CSV, REST API requests…).

So that can be translated to:

  • tiny-router: Support for changing the routes in both client and server when there are redirections stored in data objects.
  • wp-source: Support for checking redirections in the source and storing them in data objects.
  • other redirection packages: Support for checking redirections in other origins and storing them in data objects.

Due to that, it doesn’t make sense to have the settings for the “source-stored” redirections in state.router.redirections anymore. I think we should move them to state.source.redirections. Other packages will have their settings stored in state.thePackage.redirections or whatever they feel like.

1 Like

Another use case that just came to our mind and I don’t know how it would work with the planned solution. When you create a redirect, you might want to keep the utm campaigns parameters.

Let’s imagine I create a page with the slug /gutenberg-and-frontity/ and I start promoting it. It is posted in an external site with a utm_campaign to track it -> /gutenberg-and-frontity?utm_campaign=external-site.

Then, for SEO reasons for example, I decide to change the slug to /gutenberg-and-react/ and redirect the old slug to this new one. I cannot ask all the external sites to change the link, but I still want to keep the utm_campaign in my Analytics.

This means that the link
/gutenberg-and-frontity?utm_campaign=external-site
should be redirected to
/gutenberg-and-react?utm_campaign=external-site

The default WordPress redirects don’t keep the parameters, which could be a problem for this use case. But for example, the Redirection plugin allows you to keep them, so it doesn’t affect the utm_campaign. I assume other plugins like Yoast also allow you to do the same.

Is this something that would keep working as expected with the planned solution?

Yes, I think so. We won’t do anything special here. That means that if you have configured your redirections to pass the queries, they will. If not, they will not.

We had a conversation about this in the PR: https://github.com/frontity/frontity/pull/601#discussion_r526045152

1 Like

The PR (https://github.com/frontity/frontity/pull/601) has finally been merged.

I will write the final implementation and I will make a video explaining how they work in the next days :slightly_smiling_face:

6 Likes

This feature has been released :tada: : Read the full Release Announcement.

I have created a demo to explain briefly how it works and how to configure it:

5 Likes

I will write the Final Implementation as soon as possible :slightly_smiling_face:

1 Like

Great! Let us know (@documentation-team) once you have written the Final Implementation so we can check it for documentation purposes

Hi guys, I’m having some trouble making this work, and I think it is because I’m still using a beta version of the wp-source package because of the infinite scroll. Is there a release of the beta packages that includes the redirects?

The infinite scroll PR is merged in dev. We didn’t do a release today because the unit tests started failing for an unrelated issue that is already fixed in this PR.

So if you can wait until tomorrow… :sweat_smile:

Sure thing. Please let me know when it’s released! :slight_smile:

We did the release yesterday, although we haven’t prepared the announcement yet. It’s ready :slightly_smiling_face:

1 Like

We have seen that our hosting provider, in this case Pantheon, change the HEAD methods and use GET instead. This way, if you are using the Embedded mode and the setting redirections: "404", it causes and Infinite loop that breaks the site as explained in this diagram:

If your hosting isn’t changing the HEAD method, it just works fine. We aren’t sure why Pantheon is doing this, but we think we should solve it anyway. We have made this video explaining the issue and the possible solutions:

And this is the diagram we have used. Let us know your thoughts :slightly_smiling_face: .

We should also add another safety measure: Do not return a 301 which redirects again to the same link.

That could happen if the domains are different and the WordPress domain is redirected to the Frontity domain.

The logic should be something like this: if the redirection is internal and the pathname is the same, don’t do the redirection.

const linkParams = new URL(link);
const locationParams = new URL(location);

const linkURI = linkParams.path + linkParams.search;
const locationURI = locationURI.path + locationURI.search;

// Only return the redirection if the location is different.
if (!isExternal && linkURI !== locationURI) {
  return redirection;
}

I haven’t tested this code, it’s just an example.

@luisherranz I can confirm that the infinite loop happens when visiting a non existent url. Not sure what other cases I should test, but if you go to https://frontity.es.aleteia.org/2021/02/23/fake/ for us it enters in a loop.

By the way, congrats on this solution, it seems to work with all our use cases, very nice job! :smiley:

Thanks Edu! :grinning_face_with_smiling_eyes:

Ok. We’ll let you know once it is solved.

I have created an issue for this → Infinite loop with 404 redirections in Embedded mode in some hostings · Issue #743 · frontity/frontity · GitHub

I labeled it as priority: high but I wasn’t sure if it should be considered critical. So if you have a different opinion just let me know.

Final Implementation

Pull Request

https://github.com/frontity/frontity/pull/601

Rationale

Many users store their 30x redirections in the WordPress database, e.g. via a Redirection plugin. WordPress redirects a user to a new link, for example after a user renames a post and tries to access it using the “old” link. However, no such functionality exists out-of-the-box for the REST API. Since Frontity uses the REST API to retrieve content, it needs a way of handling such redirections. For a bit more in-depth explanation, I recommend watching the Demo (also linked at the end).

General mechanism

The redirections functionality works by making an additional request to the WordPress instance to check if a redirection exists for a particular URL. By WordPress instance, we mean the location of your WordPress installation, not the Frontity app. Normally the state.source.url points to that location.

We introduce a single setting state.source.redirections which dictates under what circumstances should Frontity make that request to the check if a redirection exists. The state.source.redirections accepts the following values:

  • "no" - Does not handle redirections at all. This is the default.

  • "all" - Always make an additional request to the WordPress instance to check if there exists a redirection. This means that every time you navigate to a new link, Frontity will make 2 requests: one to the REST API to try to fetch the content and another one to the WordPress instance to check if a redirection exists. Frontity will wait for both requests to finish before proceeding.

  • "404" - Only send the additional request to the WordPress instance if the original request to the REST API has returned a 404. This would happen for example if try to access a post that has been renamed.

  • string - A string that contains a regex pattern. The string must start with RegExp:. This pattern will be matched against the current route and if matched, Frontity will make an additional request to the WordPress instance to check if there exists a redirection for that route. Note that the shorthand character classes will have to be escaped, so for example instead of \d, you will need to write \\d.

  • string[] - An array of strings, which can contain the “404” value as well as any number of strings starting with "RegExp:" which represent regular expressions. An additional request will be sent to Wordpress to check for the redirection if any of the regular expressions match the current route. If the array also contains a "404", an additional request will also be made if the original request has returned a 404.

Usage

:warning: In order for the redirections to work correctly you will need to set up CORS headers in your WordPress installation. If you are using the Redirections plugin, it’s quite simple:

In order to use the redirections, there is no need to install any new npm package. There is a new property exposed by the wp-source package, state.source.redirections which is used to handle the redirections. The recommended way of using it is by setting it in your frontity.settings.js file.

state.source.redirections accepts the options outlined above and described by those types

Examples of valid values:

// frontity.settings.js

{
  name: "@frontity/wp-source",
  state: {
    source: {
      url: "https://test.frontity.org",

      // always check if there exists a redirection.
      redirections: "all",

      // match the url `/some-post` exactly
      redirections: "/some-post/",

      // match urls like `/some-post/1`, `/some-post/2`, etc.
      redirections: "RegExp:/some-post/(\\d*)",

      // match urls like: /some-post/42, /some-otherpost/5
      redirections: "RegExp:/post-(\\w*)/(\\d*)",

      // match a combination of multiple options
      redirections: ["404", "/some-post/", "RegExp:/another-post/(\\d)"],
    },
  },
}

Alternative usage

Redirections work internally by assigning a special RedirectionData object to state.source.data[link] (details below in Technical details).

A consequence of that is that you can alternatively define redirections directly in the state or using a custom handler as mentioned by Luis previously.

  • Single redirection populating state.source.data with RedirectionData directly:

    const state = {
      source: {
        data: {
          "/old-url/": {
            isReady: true,
            isRedirection: true,
            is301: true,
            redirectionStatus: 301,
            isExternal: false,
            location: "/new-url",
          },
        },
      },
    };
    
  • A custom handler which assigns properties of RedirectionData object for the current route:

    const categoryRedirection = {
      pattern: "/category/:slug",
      priority: 5,
      func: ({ link, params }) => {
        state.source.data[link].isReady = true;
        state.source.data[link].isRedirection = true;
        state.source.data[link].is301 = true;
        state.source.data[link].redirectionStatus = 301;
        state.source.data[link].isExternal = false;
        state.source.data[link].location = `/categoria/${params.slug}`;
      },
    };
    

Note on functionalities

  • The redirections support not only redirecting to other pages in your WordPress site but also redirecting to external pages.

  • You can define 301, 302, 307 or 308 Redirections.

  • We respect the settings of the Redirections plugin with respect to the query parameters:

Technical details

The redirections are first handled inside of actions.source.fetch(). If the setting for state.source.redirections contains an “eager” value (for example if it equals "all") it is fetched before calling the handler for the current route. “Fetching a redirection” refers to making a request to the WordPress instance to check if a redirection exists for a particular URL.

If a redirection is not “eager” (for example if state.source.redirections is "404") then we fetch the redirection only after the request to the REST API has returned a 404.

The actual logic for fetching the redirection from the WordPress instance is different on the client and server because of platform differences.

In both cases, if a redirection does exist for a a particular route, Frontity populates state.source.data[link] with a RedirectionData object instead of a “typical” Data object like PostData or AuthorData. This object contains all the information about a redirection that Frontity needs in order to handle it. You can check the type of RedirectionData to see all of its properties.

Once a RedirectionData object is in the state, the behavior of Frontity is different on client and server.

On the server, we check the data object for the current route in the beforeSSR() action of tiny-router. If that object contains a redirection, Frontity will set the correct HTTP status, and redirect using ctx.redirect(data.location) where data.location contains the final URL .

On the client, we listen to changes to the state.source.data object and if the data object for current route contains a redirection, Frontity calls actions.router.set(data.location) where data.location contains the final URL.

The above flow looks something like:

Demo

A demo has been created by Mario:

1 Like

@mmczaplinski Thanks for the Final Implementation info. It’s really helpful and allowed me to understand pretty well how the redirections work. The Demo video is also very useful.

Just one question

Fow what I understand this feature works w/ the default redirections managed internally by wordpress (for example when we rename a slug) and for those defined w/ the Redirection plugin (that it seems to the popular choice for most WordPress users)

Will this feature work w/ some other redirections plugins?


Just to provide some feedback I think this is very detailed and technical explanation really useful to understand how the use the feature and how it works internally. I don’t think we’ll explain the technical details in the docs, but I love they’re explained here because they can be easily referenced from the docs