Dynamic Page Metadata with React and Express.js

Use Case: Loading metadata from a database to be used in a page.

Chances are that you're not both the developer of your website or application AND the creator of content. It's likely that you're developing your app to be used and managed by either a client or a non-programming team member.

If that's true of you, then ideally you'd set up your application to be easily managed by these non-developers with minimal ongoing support required from you in terms of page content and metadata.

I've built a CMS in Node and Express that my clients use to manage their content. You can read more about that and see screenshots in this other post. However, today I want to walk through how to pass metadata from Express, fetch it in React and assign it to page metadata in a layout.

Passing metadata from Express.js

To get the metadata information from our database and sent via our API, we need to set up our MySQL connection, pull the data and send it from the Express.js router.

Loading database credentials

Our application needs access to our database, so we're going to set up a config.json file to hold our credentials:

JSON

{
	"production": {
		"database": "my_database",
		"dbUser": "root",
		"dbUserPassword": "password"
	}
}

and load the contents as global variables via config.js:

JavaScript

const config = require('./config.json');

const defaultConfig = config.production;
global.gConfig = defaultConfig

All that's going on here is we're setting the database credentials in JSON then requiring and setting them equal to global.gConfig. To use these credentials, we'll require the config.js file and use the object properties.

MySQL module

We need to set up our MySQL connection. We'll do that be installing the MySQL NPM module (npm i mysql) in our project and setting it up and exporting it like so:

JavaScript

const config = require('../config/config.js')
const mysql = require('mysql');
const pool  = mysql.createPool({
	connectionLimit : 10,
	host            : 'localhost',
	user            : global.gConfig.dbUser,
	password        : global.gConfig.dbUserPassword,
	database        : global.gConfig.database,
	multipleStatements: true
})
module.exports = { mysql, pool }

You can see that we're requiring our new global configuration object and the newly installed mysql component. Then we're setting up a connection via our credentials and exporting the connection object.

Metadata route middleware

Since each public page needs its own metadata, we'll set up a middleware function in Express to pull each page's metadata depending on its url:

JavaScript

const db = require('../config/db')

const static = (req, res, next) => {

	var url = req.originalUrl

	const sql = `SELECT * FROM meta WHERE url = '${url}'`
	db.pool.query(sql1, function (error, results) {
		req.args = { meta: results }
		next()
	})
}

module.exports = static

We've put this middleware in an independent file - static.js - and exported it as static.

In our meta database table we've assigned a column to hold the page's URL so that we could call it with the originalUrl property available in Express's request object. We'll then assign the results of the MySQL query equal to a variable inside of a new property - args - that we'll assign to the request object.

API Headers

Now that we've pulled and assigned the metadata from the database, we'll need to set up a route to respond to the request. However, before we can do that we need to approve the API call coming from our front-end, React site.

For this, we'll set up another piece of middleware functionality:

JavaScript

const headers = (req, res, next) => {
	const origin = (req.headers.origin == 'http://localhost:3000') ? 'http://localhost:3000' : 'https://mywebsite.com'
	res.setHeader('Access-Control-Allow-Origin', origin)
	res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE')
	res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type')
	res.setHeader('Access-Control-Allow-Credentials', true)
	next()
}

module.exports = headers

The headers middleware will first determine the origin of the request. Since we want our application to work in both production and development, we'll set origin equal to the result of a conditional statement. If the origin is our localhost (development), then set it equal to that, if not, set it equal to the domain of our website (production).

The reason why we need to do this is because the HTML header Access-Control-Allow-Origin accepts only 1 url - we can't submit an array or comma-separated values, unfortunately.

We'll then set up the other default settings, call the next() function at the end of the middleware and then export the module as headers.

(Note: you may need to pre-flight your request. See this post about how I pre-flighted my API POST request from my React app.)

Route API

Now we can set up our Express route to receive and respond to the request.

JavaScript

const router = require('express').Router()
const headers = require('./middleware/headers')
const static = require('./middleware/staticPage')
const db = require('../config/db')

router.get('/home', headers, static, (req, res) => {
	res.send(req.args)
})

We'll first require the necessary modules and middleware functions, then, in this instance, if the requested route is '/home' we'll called the 2 middleware functions and then respond with res.send() and include our req.args property containing the page's metadata.

The reason we're not using the response res.render() is because we only need to respond with the data that will be pulled in from our React application.

Okay, now we've finished with our Express API side of things. Now we need to pull in and render the front-end React side.

Let's go in order of the events ...

Page Module

Most React application frameworks (i.e. Next.js) start by pulling in the requested page's module. If a user is attempting to visit a site's 'contact' page then React will look for the contact.js file located within the necessary directory (i.e. 'pages').

So that's where we start. Since our example is focused on the home page and React will default to looking for the index.js file, we need to initiate the loading of our page content there.

JavaScript

import Layout from '../modules/Layout'

export default () => (
	<Layout page='home'>
		<p>This is the home page</p>
	</Layout>
)

For our example, we're just setting the page content as a paragraph stating This is the home page. But what will lead to the loading of our page's metadata is that we're putting the paragraph inside of the module Layout and assigning the prop value of page equal to 'home'.

Let's take a look at Layout.js ...

Fetching metadata in React Layout

To allow for scaleability and modularity, we want to set up a default layout for our front-end pages to pull in things like metadata from the API without needing to set this up for each page individual. For that, we've set up a layout class.

JavaScript

import React from 'react';

import Head from './Head'
import Header from './Header'
import Footer from './Footer'

class Layout extends React.Component {

	constructor(props) {
		super(props)
		this.state = {
			meta: {}
		}
	}

	render() {

		const className = this.props.page.replace(/\//g, '-')

		return (
			<div className={'page page-' + className}>
				<Head />
				<Header />
				<div className='page-content'>
					{children}
				</div>
				<Footer />
			</div>
		)
	}
}

export default Layout

In the return method you can see that we've set up our default pages with a head, header and footer tag. We also put everything inside of a div with a dynamic class name equivalent to the page prop passed to the Layout module (formatted for nested routes).

You can also see that we've imported the necessary files at the top and set up a constructor function to handle our props and the state of the class.

Now let's get the metadata from the API:

JavaScript

...

async componentDidMount(req) {
	const res = await fetch(process.env.api_endpoint + '/' + this.props.page)
	const json = await res.json()
	this.setState({
		meta: json.meta
	})
}

render() {
	...
	return (
		<div className={'page page-' + className}>
		<Head meta={this.state.meta} />

		...

We've added 2 things. First, the asynchronous function componentDidMount that will be called after our Layout class has been rendered to the DOM and second, a props property called meta that will send the data from the class to the Head module for rendering.

In componentDidMount, we're fetching the data from our API Express application by requesting the '/home' page (/ + this.props.page). Then we're turning the data into JSON and setting the classes meta state equal to the JSON object's meta property.

Assigning metadata using Helmet

Now the only thing we have left to do is render the information inside of our Head module.

JavaScript

import React from 'react';
import {Helmet} from "react-helmet";

export default (props) => {
	return (
		<Helmet>
			<meta charSet="utf-8" />
			<title>{props.meta.title}</title>
			<meta name='description' content={props.meta.description} />
			<meta property='og:locale' content='en_US' />
			<meta property='og:type' content='website' />
			<meta property='og:title' content={props.meta.title} />
			<meta property='og:description' content={props.meta.description} />
			<meta property='og:image' content={'/static/media/' + props.meta.image} />
			<meta property='og:url' content=`https://mywebsite.com${url}` />
			<meta name='twitter:card' content='summary_large_image' />
			<meta name='twitter:site' content='@mywebsite' />
			<meta name='twitter:creator' content='@mywebsite' />
		</Helmet>
	)
}

Let's address the Helmet module. React Helmet is a module available in NPM that allows you to make changes to your document's head. Because React applications aren't fully loaded upon initial page render but slightly afterward once a script gets called, the head tag has already been established. Helmet allows us to make changes to the contents of the document's head that will be recognized by both user's and bots (i.e. search engines).

So, we're putting all of our meta and title tags inside of the Helmet module and accessing our metadata via the props.meta object. Above is generally what I use for meta tags. They work for search as well as social media sharing.

You could take things a step further and include JSON for search engine schema:

JavaScript


<script type='application/ld+json'>
{
	"@context": "http://schema.org",
	"@type": "BlogPosting",
	"author": {props.meta.author},
	"headline": {props.meta.title},
	"about": {props.meta.description},
	"inLanguage": "en-US",
	"keywords": {props.meta.keywords}
}
</script>

Be sure to also put that (and any other meta you'd want to include) inside of the Helmet module tags.

Tweet me @tylerewillis

Or send an email:

And support me on Patreon