Defining redirects for Next.js applications
Nov. 1, 2023
--

Defining redirects for Next.js applications

During a website redesign, modifying the URL path of a page may be necessary.

For example, you may want to relocate the contact page to the “about us” section. This change should result in the page that is currently accessed at the /contact path being accessed at the new /about-us/contact path.

For a change like this, web teams must consider that the original link might still be in use, for example, on social media or other websites. To avoid losing traffic from these sources – and causing frustration for users – we use redirects.

When delivering content using the headless approach, managing redirects at scale can be a challenge.

This blog post will show how to create a Magnolia Content App that enables content authors to easily manage redirects for a headless environment and how to pass the defined redirects to a Next.js application using both REST and GraphQL methods.

Requirements

The easiest way to get started with Magnolia and Next.js is the minimal-headless-spa-demos tutorial. You can also explore our nextjs-redirect-demo project, where the Next.js application and Redirects Content App are already set up.

Creating the redirects app

For this tutorial, we assume that Magnolia and Next.js are up and running.

First, we need to define how redirects are created and stored to allow content authors to manage them.

Let's start by creating a new directory for the Magnolia Light Module called nextjs-redirect-lm. Within this directory, we will soon create a Content Type and a Content App for the redirects.

Creating the redirect content type

Magnolia Content Types define and describe the types and structure of content in your project. Let's define a new Content Type called redirect with three properties:

  • name: This serves as the unique identifier for each redirect.

  • rootPage: This property represents the current root page that is targeted for redirection. In other words, it is the source page from which users will be redirected.

  • redirect: This property represents the redirection rules. It is composed of multiple attributes: 1)from: This field represents the initial URL path from where the redirection originates. 2)to: This field represents the destination URL. It could either be an internal link within your site or an external link to a different site. 3) code: This field represents the HTTP status code that will be sent with the redirect.

Create the file /nextjs-redirect-lm/contentTypes/redirect.yaml for the above content model:

YAML
  datasource:
  workspace: redirects
  autoCreate: true

model:
  nodeType: lib:redirect
  properties:
    - name: name
    - name: rootPage
    - name: redirect
      type: redirectItem
      multiple: true
  subModels:
    - name: redirectItem
      properties:
        - name: from
        - name: to
          type: toItem
        - name: code
    - name: toItem
      properties:
        - name: field
        - name: internal
        - name: custom

Once we created the Content Type, let's create a Content App.

Creating the Redirects Content App

A Magnolia Content App is an app for managing structured content. It defines the layout of the Content Type’s properties and other properties. Let's define a new Content App called redirects-app.

Create the file /nextjs-redirect-lm/apps/redirects-app.yaml:

YAML
  !content-type:redirect
name: redirects-app
label: Redirects App

subApps:
  browser:
    actions:
      activate:
        availability:
          multiple: true
  detail:
    form:
      properties:
        name:
          $type: textField
          required: true
        rootPage:
          $type: pageLinkField
          showOptions: true
          textInputAllowed: true
          converterClass: info.magnolia.ui.editor.converter.JcrNodeToPathConverter
          required: true
        redirect:
          $type: jcrMultiField
          field:
            $type: compositeField
            properties:
              from:
                $type: textField
                required: true
              to:
                $type: switchableField
                field:
                  $type: comboBoxField
                  required: true
                  datasource:
                    $type: optionListDatasource
                    options:
                      - name: internal
                        value: internal
                      - name: custom
                        value: custom
                itemProvider:
                  $type: jcrChildNodeProvider
                forms:
                  - name: internal
                    properties:
                      internal:
                        $type: pageLinkField
                        showOptions: true
                        textInputAllowed: true
                        converterClass: info.magnolia.ui.editor.converter.JcrNodeToPathConverter
                        required: true
                  - name: custom
                    properties:
                      custom:
                        $type: textField
                        required: true
              code:
                $type: textField
                required: true

We have now defined the overall layout of our Content App.

Note that the rootPage property has a pageLinkField type and that a redirect property consists of a from, to, and code field.

Also, notice that the to property if of a switchableField type with two possible options:

  • internal

  • custom

You may consider extending the Content App by a description and field validators ensuring that field input is entered in the correct format and length.

At this point, the new app should be visible in Magnolia:

Adding redirects

Now, open the Redirects App and click the "Add item" button. You should see the following form:

Go ahead and create your first redirect using the parameters from the screenshot.

Fetching data

How can we now access the data we just entered in the Content App?

There are two ways to fetch the data from a Content App: using a REST delivery endpoint or a GraphQL API.

Fetching data from the REST delivery endpoint

Return to the Light Module folder and create a new file to create the REST delivery endpoint.

/nextjs-redirect-lm/restEndpoints/delivery/redirects_v1.yaml:

YAML
  class: info.magnolia.rest.delivery.jcr.v2.JcrDeliveryEndpointDefinition
workspace: redirects
depth: 3
includeSystemProperties: false
bypassWorkspaceAcls: true
limit: 50
systemProperties:
  - mgnl:lastModified
  - mgnl:created

The endpoint is available immediately after creating its configuration and we can access the data on this URL: http://localhost:8080/magnoliaAuthor/.rest/delivery/redirects/v1/?rootPage%5Blike%5D=%2Fnextjs-redirect-demo%25

Note the ?rootPage[like]=/nextjs-redirect-demo% in the URL? This part allows us to filter the results using request parameters.

If you defined a redirect in the previous step, the response should look like this:

JSON:

JavaScript
  {
    "total": 1,
    "offset": 0,
    "limit": 50,
    "results": [
        {
            "@name": "Contact-Page-Redirect",
            "@path": "/Contact-Page-Redirect",
            "@id": "4c7c577e-71b8-47a6-8fe2-c94fc496f11d",
            "@nodeType": "mgnl:content",
            "rootPage": "/nextjs-redirect-demo",
            "name": "Contact Page Redirect",
            "mgnl:lastModified": "2022-10-31T14:04:11.398+01:00",
            "mgnl:created": "2022-10-31T14:04:11.397+01:00",
            "redirect": {
                "@name": "redirect",
                "@path": "/Contact-Page-Redirect/redirect",
                "@id": "a36b3c4a-904a-4540-841c-da953baf52b6",
                "@nodeType": "mgnl:contentNode",
                "mgnl:lastModified": "2022-10-31T14:04:11.413+01:00",
                "mgnl:created": "2022-10-31T14:04:11.413+01:00",
                "redirect0": {
                    "@name": "redirect0",
                    "@path": "/Contact-Page-Redirect/redirect/redirect0",
                    "@id": "1c59c77f-c6c6-4c4f-8346-22056a1ecf93",
                    "@nodeType": "mgnl:contentNode",
                    "from": "/contacts",
                    "code": "301",
                    "mgnl:lastModified": "2022-10-31T14:04:11.437+01:00",
                    "mgnl:created": "2022-10-31T14:04:11.437+01:00",
                    "to": {
                        "@name": "to",
                        "@path": "/Contact-Page-Redirect/redirect/redirect0/to",
                        "@id": "c5789397-05ba-45b3-b7ff-1d810760c3e1",
                        "@nodeType": "mgnl:contentNode",
                        "internal": "/nextjs-redirect-demo/about-us/contacts",
                        "mgnl:lastModified": "2022-10-31T14:04:11.439+01:00",
                        "field": "internal",
                        "mgnl:created": "2022-10-31T14:04:11.439+01:00",
                        "@nodes": []
                    },
                    "@nodes": ["to"]
                },
                "@nodes": ["redirect0"]
            },
            "@nodes": ["redirect"]
        }
    ]
}

Fetching data from the GraphQL API

Earlier, we defined a Content Type using our content model. Using the GraphQL API at http://localhost:8080/magnoliaAuthor/.graphql, you can query specific data by sending a POST request with the query in the body specifying the content properties you want to fetch.

For example, query all redirects where the rootPage property starts with a specific string:

JSON

JavaScript
  {
    redirects (filter: "@rootPage LIKE '/nextjs-redirect-demo%'"){
        redirect {
            from
            to {
                field
                internal
                custom
            }
            code
        }
    }
}

The GraphQL API responds with a JSON object that mirrors the structure of your query, filled with the requested data. You can then use this data directly in your application.

Calling all frontend developers

Use the headless approach for Magnolia. Check out all of our headless documentation.

Parsing the data

To use the redirect data in Next.js, we need to parse it.

Parsing data from the REST delivery API

As REST doesn't allow us to choose which properties to fetch, we need to do this manually.

JavaScript
  const nodeName = "/nextjs-redirect-demo"
const REST_API = "http://localhost:8080/magnoliaAuthor/.rest/delivery/redirects/v1"

async function parseRedirectsREST() {
    const redirects = await fetchAPI(`${REST_API}/?rootPage%5Blike%5D=${nodeName}%25`);
    return redirects.results.flatMap(parseRedirectItem);
}

function parseRedirectItem(item) {
    const rootPage = item.rootPage.replace(nodeName, "");
    return item.redirect["@nodes"].map((redirectNode) => ({
        from: rootPage + item.redirect[redirectNode].from,
        to: parseRedirectTo(item.redirect[redirectNode].to),
        code: item.redirect[redirectNode].code,
    }));
}

function parseRedirectTo(to) {
    return {
        field: to.field,
        custom: to.custom,
        internal: replaceNodeName(to.internal),
    };
}

function replaceNodeName(url) {
    return url?.replace(nodeName, "/")?.replace("//", "/");
}

async function fetchAPI(url, options) {
    try {
        const response = await fetch(url, options);
        if (!response.ok) {
            throw new Error(`An error has occurred: ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error(error);
        return [];
    }
}

Parsing data from the GraphQL API

GraphQL allows us to specify the properties we want to fetch, making the parsing process easier.

JavaScript
  const nodeName = "/nextjs-redirect-demo"
const GraphQL_API = "http://localhost:8080/magnoliaAuthor/.graphql"

async function parseRedirectsGRAPHQL() {
    const options = {
        method: "POST",
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        body: JSON.stringify({
            query: `
                {
                    redirects (filter: "@rootPage LIKE '/nextjs-redirect-demo%'"){
                        redirect {
                            from
                            to {
                                field
                                internal
                                custom
                            }
                            code
                        }
                    }
                }
            `
        })
    };

    const response = await fetchAPI(GraphQL_API, options);
    let parsedRed = response?.data?.redirects[0]?.redirect || [];

    return parsedRed.map(item => {
        item.to.internal = replaceNodeName(item.to.internal);
        return item;
    });
}

function replaceNodeName(url) {
    return url?.replace(nodeName, "/")?.replace("//", "/");
}

async function fetchAPI(url, options) {
    try {
        const response = await fetch(url, options);
        if (!response.ok) {
            throw new Error(`An error has occurred: ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error(error);
        return [];
    }
}

Parsing result

Based on the data we’ve entered earlier, both functions parseRedirectsREST and parseRedirectsGRAPHQL generate the following array:

JSON

JavaScript
  [
  { 
    "from": "/contacts", 
    "to": {
      "field": "internal", 
      "internal": "/about-us/contacts", 
      "custom": null
    }, 
    "code": "301" 
  }
]

Checking for redirects using Next.js Middleware

Next.js Middleware has a great feature that allows us to run code before a request is completed. We can use it to check for redirects.

To do so, create a middleware.js file in your Next.js application’s root directory.

The below example works with both REST and GraphQL responses. Please comment out the fetching option you don’t want to use.

JavaScript
  import {NextResponse} from "next/server";
const nodeName = process.env.NODE_NAME
const redirect_REST_API = process.env.MGNL_API_REST_REDIRECTS
const redirect_GRAPHQL_API = process.env.MGNL_API_GRAPHQL_REDIRECTS
let storedRedirects = [];

// Fetches redirects using REST or GraphQL
async function fetchRedirects() {
    // This function fetches redirects and can be configured to use either the REST or the GraphQL endpoint

    //return parseRedirectsREST(redirect_REST_API);
    return parseRedirectsGRAPHQL(redirect_GRAPHQL_API)
}

// Parses redirects fetched from REST API
async function parseRedirectsREST(url) {
    const redirects = await fetchAPI(`${url}/?rootPage%5Blike%5D=${nodeName}%25`);
    return redirects.results.flatMap(parseRedirectItem);
}

// Parses redirects fetched from GraphQL API
async function parseRedirectsGRAPHQL(url) {
    const options = {
        method: "POST",
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        },
        body: JSON.stringify({
            query: `
                {
                    redirects (filter: "@rootPage LIKE '/nextjs-redirect-demo%'"){
                        redirect {
                            from
                            to {
                                field
                                internal
                                custom
                            }
                            code
                        }
                    }
                }
            `
        })
    };

    const response = await fetchAPI(url, options);
    let parsedRed = response?.data?.redirects[0]?.redirect || [];

    return parsedRed.map(item => {
        item.to.internal = replaceNodeName(item.to.internal);
        return item;
    });
}

// Parses a single redirect item
function parseRedirectItem(item) {
    const rootPage = item.rootPage.replace(nodeName, "");
    return item.redirect["@nodes"].map((redirectNode) => ({
        from: rootPage + item.redirect[redirectNode].from,
        to: parseRedirectTo(item.redirect[redirectNode].to),
        code: item.redirect[redirectNode].code,
    }));
}

// Parses 'to' field in a redirect
function parseRedirectTo(to) {
    return {
        field: to.field,
        custom: to.custom,
        internal: replaceNodeName(to.internal),
    };
}

// Replaces nodeName in a url
function replaceNodeName(url) {
    return url?.replace(nodeName, "/")?.replace("//", "/");
}

async function fetchAPI(url, options) {
    try {
        const response = await fetch(url, options);
        if (!response.ok) {
            throw new Error(`An error has occurred: ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error(error);
        return [];
    }
}

export async function middleware(request) {
    if (request.nextUrl.pathname === "/update-redirects" || storedRedirects?.length === 0) {
        storedRedirects = await fetchRedirects();
        return NextResponse.next();
    }

    const redirect = storedRedirects.find(r => r.from === request.nextUrl.pathname);
    if (redirect) {
        const url = redirect.to.field === 'internal'
            ? new URL(redirect.to.internal, request.url)
            : new URL(redirect.to.custom);
        return NextResponse.redirect(url, parseInt(redirect.code));
    }

    return NextResponse.next();
}

Updating redirect data using a webhook

How can we trigger an update of the list of redirects in Next.js every time a redirect is created or changed in Magnolia?

We can use a webhook that gets triggered by a Published and Unpublished event. So, let's go back to the Light Module directory to create the webhook configuration.

/nextjs-redirect-lm/webhooks/webhookConfig_1.yaml

YAML
  name: webhook1
url: http://localhost:3000/update-redirects
method: GET
enabled: true

events:
  - name: contentPublished
    eventType: Published
    entity: redirect
  - name: contentUnpublished
    eventType: Unpublished
    entity: redirect

From now on, every time a redirect is published or un-published in the Content App, the webhook http://localhost:3000/update-redirects will be called to update the list of redirects in Next.js.

Checking your redirects

Open http://localhost:3000/contacts to verify that you are redirected to http://localhost:3000/about-us/contacts. Does it work? Congratulations!

To see the redirect code, open the developer tools and note the redirect

If you got lost or you want to get straight into the code, you can find a working demo on Git.

Hope this tutorial helps! You can check out all of our headless documentation here.