Validate frontmatter Markdown SEO metadata in NodeJS

Many websites today are built using static site builders and markdown with embed data: the JAM Stack. Markdown alone is useful for writing content. Adding Frontmatter YAML headers to Markdown allows us to embed meta data for static site builders. To validate this content to SEO we will share a test.

Hugo setup

Say you have a website built with Hugo. You might want to read all the content and check that the title and description properties of the markdown are of an appropriate length. We can do so using Jest and Node JS.

Install deps

We need a few node dependencies for this examples:

npm install glob-promise glob front-matter

Test with Jest

Let’s write a test that loads all the Markdown files in the content folder and parses their markdown frontmatter. We then return a result for various tests such as description length. We use Jest to run the tests:

/**
 * Test to load content, read the markdown frontmatter and validate SEO recommended lengths etc
 */
const { join } = require("path");
const glob = require("glob-promise");
const { readFile } = require("fs/promises")
const fm = require('front-matter')

const CONTENT_DIR = join(__dirname, "..", "content")
const States = {
    OK: "OK",
    FAIL: "FAIL",
    TOO_LONG: "TOO_LONG",
    TOO_SHORT: "TOO_SHORT",
}

function testSize(string, min, max) {
    let res = States.OK;
    if (!string) {
        res = States.TOO_SHORT;
    }
    else if (string.length < min) {
        res = States.TOO_SHORT;
    }
    else if (string.length > max) {
        res = States.TOO_LONG;
    }
    return {
        min, max, result: res, length: string?.length ?? 0
    }
}


async function getContentFiles() {
    return glob(join(CONTENT_DIR, '**/*.md'))
}
async function scoreMeta(path) {
    const content = await readFile(path, "utf-8")
    let data;
    try {
        data = fm(content)
    } catch (e) {
        throw new Error("Read error " + path, e.message)
    }
    const { title, description } = data.attributes
    const h1Missing = data.body?.indexOf("# ") > -1 ? States.OK : States.FAIL
    return {
        path: path,
        metaDescriptionLength: testSize(description, 110, 160),
        metaTitleLength: testSize(title,  50, 60),
        h1Missing
    }
}
describe("seo of content", () => {
    test("meta description length", async () => {
        const files = await getContentFiles()
        expect(files.length).toBeGreaterThan(0);
        const scores = await Promise.all(files.map(file=> scoreMeta(file)));
        for(let score of scores) {
            const metaDescription = score.metaDescriptionLength
            expect(metaDescription.result, `META DESCRIPTION ${metaDescription.result}: ${score.path} found 
            ${metaDescription.length} wanted between ${metaDescription.min} - ${metaDescription.max}`).toEqual(States.OK)
        }
    })
})

Execute jest to run the test.