Defining redirects for Next.js applications
Magnolia in action
Take 12 minutes and a coffee break to discover how Magnolia can elevate your digital experience.
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
Magnolia 6.2.25 or higher
Webooks module 1.0.0 or higher
REST Client module 2.0.4 or higher
GraphQL module 1.1.2 or higher
Next.js application
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:
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:
!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:
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:
{
"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
{
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.
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.
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
[
{
"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.
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
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.