SEO Metadata Social System

by John Pennock 10/29/2024
A circuit that contains a large chip labeled 'metadata'

Intro

The goal of the custom metadata for NuxtContent SEO and Social sharing is to enable content authors to write a new blog post or article in a single Markdown .md file that contained all the appropriate metadata images and data for SEO and social sharing. Each blog or article created would automatically get common and article specific metadata so that it will look inviting in search results and on social media. The method applies to Nuxt.js NuxtContent module Markdown authored pages.

Further, social share buttons would automatically be generated for each blog or article. A reader could copy/paste the url by hand or using the social share buttons to include in their social media post.

The essential pattern

  1. In app.vue
    • create a metaDefaults data structure
    • useHead() to set SEO icons
    • useSeoMeta() to set global SEO Metadata values
    • use provide() to allow children components access to the metaDefaults
  2. In the page .vue file (I used the convenient catchall [...slug].vue) in the <script setup> section
  • use inject() to get metaDefaults from app.vue
  • use NuxtContent queryContent() to get the current page front-matter variables
  • Combine the metaDefaults with the page front-matter variables
  • useSeoMeta() to create custom meta tags for that page
  1. Each article or post .md file uses front-matter variables naming convention to get automatic SEO and Social metadata tagging

Global Setup

metaDefaults

The first item in app.vue of the <script setup> is to establish a constant Plain Old JavaScript Object (POJO) called metaDefaults which contains default values when not overridden by the Front-matter vars of the NuxtContent markdown page. And then use the provide vue method so that children components can use them.

const metaDefaults = {
  siteName: 'Pennock Projects',
  title: 'Pennock Projects',
  description: 'Pennock Projects is a software engineering blog about website and mobile applications, front end frameworks, backend API services, databases, and AI architecture by John Pennock',
  keywords: ['blog'],
  author: 'John Pennock',
  rootUrl: "https://pennockprojects.com",
  robots: 'index, follow',
  copyright: '© 2024 by John Pennock',
  ogType: 'article',
  imageRoot: '/images',
  image2x1: '/images/PennockProjectsFB.jpg',
  image2x1Width: 1200,
  image2x1Height: 600,
  image1x1: '/images/PennockProjectsLogo.png',
  image1x1Width: 800,
  image1x1Height: 800,
  image_alt: 'Pennock Projects Logo',
  twitterCard: 'summary_large_image',
  twitterSiteHandle: '@PennockProjects',
  twitterCreatorHandle: '@JohnPennock'
}

// allow children components readonly access to metadata defaults.
provide("metaDefaults", metaDefaults);

Global Metadata

A second item in app.vue setup was to use useHead() composable and useSeoMeta() composable to set common metadata for all pages. For example, I set the page title, htmlAttrs, robots and copyright metadata here.

Use a site like favicon.io to create favicons and web manifest files.

useHead({
  titleTemplate: (titleChunk) => {
    return (titleChunk && (titleChunk != metaDefaults.title)) ? `${titleChunk} - ${metaDefaults.title}` : metaDefaults.title;
  },
  htmlAttrs: {
    lang: 'en'
  }
  // ...
})

// Setting Global SEO on each page
useSeoMeta({
  robots: metaDefaults.robots,
  copyright: metaDefaults.copyright
})

Page .vue System

Metadata Defaults and Page

On each page you would use the vue inject method to get the metaDefaults from app.vue and NuxtContent queryContent method to obtain the page's markdown front-matter variables

const route = useRoute()
const metaDefaults = inject("metaDefaults");

const { data } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
let doc = data.value || {}

Page Specific Metadata

Next I combine the defaults with the page-specific front-matter variables and use the useSeoMeta() composable a second time (the first time in app.vue setup).

In the utility function, I check if the page front-matter variables exist, and if not, I use the defaults. For example, if the page doesn't have an og_title variable, I will use the title variable for the Open Graph title. If the page doesn't have a title variable, I will use the metaDefaults.title for the title. This allows each page to have custom metadata, but if the page doesn't specify it, it will fall back to the defaults.

export const setSEO = (metaPage, metaDefaults, routePath) => {

  let doc = metaPage || {}
  let oTitle = (doc.og_title || doc.title)
  let x_title = (doc.x_title || doc.title)
  let x_image = doc.x_image || doc.image || metaDefaults.image2x1
  let seoData = {}

  let keywords = doc && doc.keywords && Array.isArray(doc.keywords) ? metaDefaults.keywords.concat(doc.keywords) : metaDefaults.keywords.concat([]);
  
  seoData.author = doc.author || metaDefaults.author
  seoData.creator = metaDefaults.creator
  seoData.keywords = keywords.toString()
  seoData.og_title = (oTitle && oTitle != metaDefaults.title) ? `${metaDefaults.title} ${oTitle}` : metaDefaults.title
  seoData.x_title = (x_title && x_title != metaDefaults.title) ? `${metaDefaults.title} ${x_title}` : metaDefaults.title
  seoData.description = doc.description || metaDefaults.description;
  seoData.og_description = doc.og_description || doc.description || metaDefaults.description
  seoData.x_description = doc.x_description || doc.description || metaDefaults.description
  seoData.og_image = doc.og_image || doc.image || metaDefaults.image2x1
  seoData.og_image_alt = doc.og_image_alt || doc.image_alt || metaDefaults.image_alt
  // Note: X/Twitter will not show the static image unless the static non-js version has a full url.
  seoData.x_image = metaDefaults.rootUrl + x_image
  seoData.x_image_alt  = doc.x_image_alt || doc.image_alt || metaDefaults.image_alt
  seoData.ogUrl = metaDefaults.rootUrl + doc._path 
  seoData.x_card = doc.x_card || metaDefaults.twitterCard
  seoData.x_creator_handle = doc.x_creator_handle || metaDefaults.twitterCreatorHandle

  let headInput = {
    link: [
      {
        rel: 'canonical',
        href: metaDefaults.rootUrl + routePath,
      },
    ],
  }

  let seoInput = {
    description: seoData.description,
    author: seoData.author,
    keywords: seoData.keywords,
    creator: seoData.creator,
    ogType: metaDefaults.ogType,
    og_title: seoData.og_title,
    og_description: seoData.og_description,
    og_image: seoData.og_image,
    og_image_alt: seoData.og_image_alt,
    ogSiteName: metaDefaults.siteName,
    ogUrl: seoData.ogUrl,
    twitterTitle: seoData.x_title,
    twitterDescription: seoData.x_description,
    twitterImage: seoData.x_image,
    twitterimage_alt: seoData.x_image_alt,
    twitterCard: seoData.x_card,
    twitterSite: metaDefaults.twitterSiteHandle,
    twitterCreator: seoData.x_creator_handle
  }
  
  return {
    headInput,
    seoInput
  }
}

Typically, you might want to extract this snippet into a utility function, but for my purposes since I used [...slug].vue catchall file, which handles 99% of my pages, I didn't need to.

Social Share Buttons

Since each page has custom metadata, I also wanted convenience buttons to quickly share the page on social media. Stefano Bartoletti Nuxt Social Share module was a good and easy as following the instructions to add the module. Then add the component into the page template.

<template>
/<!-- snip -->
  <SocialShare
    v-for="network in ['facebook', 'x', 'linkedin', 'email']"
    :key="network"
    :label="false"
    :network="network"
    :styled="true"
  />
<!-- snip -->
</template>

Page .md Usage

Base Metadata Variables

The basic set of front-matter variables that each .md page should contain.

Variable keyHTML elementPurpose
title<title></title>NuxtContent metadata
descriptionname="description"NuxtContent metadata
authorname="author"SEO metadata
imageproperty="og:image" name="twitter:image"Social metadata
image_altproperty="og:image:alt" name="twitter:image:alt"Social metadata

Base App Variables

Variable keyKey Valuesdescription
date_createdYYYY-MM-DDused in blog and articles
is_toctrue or false(default)whether page will have a table of contents
is_manual_imagetrue or false(default)whether page will manually insert article image
format"List/Code/Cheat Sheet/How-to/etc."explains the organization and tone

Base Template

---
title: Template Title
description: "The Templates's description"
is_toc: true
date_created: 2024-05-03
author: Template Article Author in text, i.e. John Pennock
image: "" # "Social Image relative or absolute link - ideally 1200 x 600 or 1200 x 630"
image_alt: "" #Template Image Alt Text description"
---

Extended Metadata Variables

The extended set of front-matter variables that each .md page could contain.

Variable keyHTML elementPurpose
date_modifiedN/Aa date for article updated
editorN/Aeditor who updated article
keywordsname="keywords"SEO metadata
og_titleproperty="og:title"FB/Open Graph metadata
og_descriptionproperty="og:description"FB/Open Graph metadata
og_imageproperty="og:image"FB/Open Graph metadata
og_image_altproperty="og:image:alt"FB/Open Graph metadata
x_titlename="twitter:title"X/Twitter metadata
x_descriptionname="twitter:description"X/Twitter metadata
x_imagename="twitter:image"X/Twitter metadata
x_image_altname="twitter:image:alt"X/Twitter metadata
x_cardname="twitter:card"X/Twitter metadata
x_creator_handlename="twitter:creator"X/Twitter metadata

The variables that start with og or x that are for 'title', 'description', 'image', and 'image_alt' are for overriding the base variable when you what something specific for either Open Graph/Facebook or X/Twitter metadata. For example, you could create a custom title, description, and image for X/Twitter if you set these. Normally all of the metadata are set to the same value from the base variables.

There are a lot more variables available for useSeoMeta(), but these are the ones illustrated here.

Extended App Variables

Variable keyFormatdescription
versionMajor.Minor (X.X)for version tracking

Extended Template

---
title: Template Title
description: "The Templates's description"
content_type: "'guide', 'hub', 'index', 'article', 'post', 'cheat sheet', 'how-to'"
keywords: [NuxtContent, Vue_js, nuxt, Metadata, SEO, SocialShare, OpenGraph]
is_toc: true
version: 1.0  #optional for tracking
date_created: 2024-05-03 10:00:00
author: Template Article Author
date_modified: 2024-05-03 10:00:00  # optional for when updated, date_created is required
editor: Template Article Editor # optional, for second author
image: Template Image Link, ideally 2x1
image_alt: Template Image Alt Text description
og_title: Template Open Graph Title
og_description: Template Open Graph Description
og_image: Template Open Graph Image Link - ideally 1200 x 630
og_image_alt: Template Open Graph Image Alt - use only if 'image_alt' not sufficient
x_title: Template Twitter Title
x_description: Template Twitter Description
x_image: Template Twitter Image Link - ideally 1200 x 600 for large card, or 800 x 800 square for summary
x_image_alt: Template Twitter Image Alt - use only if 'image_alt' not sufficient
x_card: Template Twitter Card - 'summary' (default) or 'summary_large_image' 
x_creator_handle: Template Twitter Creator handle, default @JohnPennock
---

Direct Method

Using the method in this article, I intentionally defined a programmatic way to set the variables using custom variables names, but it is important to note that these values can be generated directly through the head: key in the front-matter variables directly without resort to using useSeoMeta() overtly. see SEO Metadata Cheat Sheet

---
head:
  meta:
    - name: 'keywords'
      content: 'blog, John Pennock'
    - name: 'robots'
      content: 'index, follow'
    - name: 'author'
      content: 'John Pennock'
    - name: 'copyright'
      content: '© 2024 John Pennock'
    - name: 'og:title'
      content: 'Pennock Projects Blog'
    - name: 'og:description'
      content: 'Pennock Projects is a blog about software engineering by John Pennock'
    - name: 'og:image'
      content: '/images/PennockProjectsLogo.png'
    - name: 'og:url'
      content: 'https://blog-git-master-john-pennocks-projects.vercel.app/'
    - name: 'twitter:title'
      content: 'Pennock Projects Blog'
    - name: 'twitter:description'
      content: 'Pennock Projects is a blog about software engineering by John Pennock'
    - name: 'twitter:image'
      content: '/images/PennockProjectsLogo.png'
    - name: 'twitter:card'  
      content: 'summary'
---

Front-matter Variables

SEO Title and Description

Front-matter Injection

Front-matter variables declared at the top can be symbolically inserted into the Markdown content by using {{ <variable key> }}. For example, if you have a front-matter variable defined as name: "Mohonri Moriancumur" you could inject it in the body with {{ name }}. This would insert the full name Mohonri Moriancumur everywhere you did this.

HTML Output Example

<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="/apple-touch-icon.png">
<link rel="icon" sizes="32x32" type="image/png" href="/favicon-32x32.png">
<link rel="icon" sizes="16x16" type="image/png" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<meta name="robots" content="index, follow">
<meta name="copyright" content="© 2024 by John Pennock">
<link rel="canonical" href="https://pennockprojects.com/">
<meta name="author" content="John Pennock">
<title>Pennock Projects</title>
<meta name="description" content="John Pennock's software development blog and portfolio">
<meta property="og:title" content="Pennock Projects">
<meta property="og:description" content="John Pennock's software development blog and portfolio">
<meta property="og:image" content="/images/PennockProjectsFB.jpg">
<meta property="og:image:alt" content="Pennock Projects Logo">
<meta property="og:site_name" content="Pennock Projects">
<meta property="og:url" content="https://pennockprojects.com/">
<meta property="og:type" content="article">
<meta name="twitter:title" content="Pennock Projects">
<meta name="twitter:description" content="John Pennock's software development blog and portfolio">
<meta name="twitter:image" content="https://pennockprojects.com/images/PennockProjectsFB.jpg">
<meta name="twitter:image:alt" content="Pennock Projects Logo">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@PennockProjects">
<meta name="twitter:creator" content="@JohnPennock"></head>