Enhance Website Visibility: SEO Metadata And Sitemap
For a content-focused website, Search Engine Optimization (SEO) is essential. It enables users to find and access your content via search engines like Google or Bing, which is the aim of this blog. If this is not your objective, feel free to skip this article.
Note
Search Engine Optimization (SEO) enhances the quality and quantity of website traffic from search engines. It focuses on organic traffic instead of direct or paid traffic.
The Fundamentals
Let us begin by integrating basic metadata into our website.
Initially, we must specify the language of our website by adding the lang
key in the config.mts
file:
import { defineConfig } from 'vitepress'
export default defineConfig({
lang: 'en-US',
})
Next, we establish the title and description of our website by inserting the title
and description
keys in the config.mts
file:
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
title: 'Garabit',
description: 'A place that reflects my thoughts and ideas',
})
The title will also serve as the suffix for each page's title. For instance, the title of the blog page will be Blog | Garabit
.
We can verify these additions by examining the generated HTML code in the .vitepress/dist
directory.
<!DOCTYPE html>
<html lang="en-US" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Blog | Garabit</title>
<meta name="description" content="The latest articles, tutorials, and resources on web development, programming, and more.">
<meta name="generator" content="VitePress v1.4.1">
<!-- ... -->
</head>
</html>
Note
VitePress incorporates its own metadata into the generated HTML code, resulting in more meta
tags than we manually added.
Static Metadata
Let's now add some static metadata to enhance SEO capabilities. These metadata apply uniformly across all website pages.
To add static metadata, insert the head
key in the config.mts
file:
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
head: [
['meta', { name: 'twitter:site', content: '@soubiran_' }], // Please, change this before deploying
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { property: 'og:image:width', content: '1200' }],
['meta', { property: 'og:image:height', content: '630' }],
['meta', { property: 'og:image:type', content: 'image/png' }],
['meta', { property: 'og:site_name', content: 'Garabit' }],
['meta', { property: 'og:type', content: 'website' }],
],
})
These metadata are utilized by social platforms like Twitter and Facebook to display a preview of the website when shared. The og
prefix indicates Open Graph, a protocol that enables web pages to become rich objects in a social network.
Note
Future articles will explore automatic Open Graph image generation for blog posts.
Dynamic Metadata
For optimal SEO, each page should feature an Open Graph title, description, and canonical URL. These are dynamic metadata and vary for each page.
We have two methods for adding dynamic metadata to our site:
- In each Markdown file's frontmatter: Though simple, this method is not the most maintainable. Adding new metadata requires updating every Markdown file and writing YAML with nested arrays, which can be cumbersome.
---
title: My Awesome Blog Post
description: A blog post that will change your life
head:
- - meta
- property: og:title
content: My Awesome Blog Post
- - meta
- property: og:description
content: A blog post that will change your life
---
Due to redundancy and complexity, I wish to avoid this method.
Thankfully, a second method exists:
- In the
config.mts
file: A more maintainable solution involves using thetransformPageData
option to insert dynamic metadata into each page, modifying the data prior to rendering.
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
transformPageData: (page, { siteConfig }) => {
// Initialize the `head` frontmatter if it doesn't exist.
pageData.frontmatter.head ??= []
// Add basic meta tags to the frontmatter.
pageData.frontmatter.head.push(
[
'meta',
{
property: 'og:title',
content:
pageData.frontmatter.title || pageData.title || siteConfig.site.title,
},
],
[
'meta',
{
name: 'twitter:title',
content:
pageData.frontmatter.title || pageData.title || siteConfig.site.title,
},
],
[
'meta',
{
property: 'og:description',
content:
pageData.frontmatter.description || pageData.description || siteConfig.site.description,
},
],
[
'meta',
{
name: 'twitter:description',
content:
pageData.frontmatter.description || pageData.description || siteConfig.site.description,
},
],
)
},
})
This approach is efficient, allowing for the addition of multiple dynamic metadata without modifying every Markdown file or duplicating metadata. In that case, we define some fallbacks in case the metadata is missing.
Note
Should you wish to add more dynamic metadata, you can do so in the same manner as the Open Graph metadata using this technique.
Canonical URL
The canonical URL specifies the preferred version of a web page, preventing duplicate content issues. Without it, search engines may struggle to identify the same page regardless of URL variations, potentially delaying indexing. This situation can occur more frequently than anticipated. For instance, a blog post shared in a newsletter could include a tracking parameter, leading the search engine to index the URL with the parameter instead of the original URL, or worse, not at all. Thus, it is crucial to add a canonical URL to each page.
In the transformPageData
function, we can access the path
of the page to infer the canonical URL.
Initially, install the ufo package, a URL manipulation library by UnJS:
pnpm add -D ufo
Next, employ this package to craft the canonical URL:
import { joinURL, withoutTrailingSlash } from 'ufo'
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
transformPageData: (page) => {
// Initialize the `head` frontmatter if it doesn't exist.
pageData.frontmatter.head ??= []
// ...
// Create the canonical URL
pageData.frontmatter.head.push([
'link',
{
rel: 'canonical',
href: joinURL(
'https://garabit.barbapapazes.dev', // Modify this prior to deployment
withoutTrailingSlash(pageData.filePath.replace(/(index)?\.md$/, '')),
),
},
])
pageData.frontmatter.head.push([
'meta',
{
property: 'og:url',
content: joinURL(
'https://garabit.barbapapazes.dev', // Modify this prior to deployment
withoutTrailingSlash(pageData.filePath.replace(/(index)?\.md$/, '')),
),
},
])
},
})
The joinURL
function combines the website's base URL with the page's path, addressing the /
to prevent //
in the URL. The withoutTrailingSlash
function removes the trailing slash from the page's path. If you prefer a trailing slash, withTrailingSlash
can be used alternatively. Additionally, index.md
or .md
must be removed from the path to generate a clean URL.
Each page now features a canonical URL. Inspect the generated HTML code to verify the implementation.
<!DOCTYPE html>
<html lang="en-US" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>My Awesome Blog Post | Garabit</title>
<meta name="description" content="A blog post that will change your life">
<meta name="twitter:site" content="@soubiran_">
<meta name="twitter:card" content="summary_large_image">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:type" content="image/png">
<meta property="og:site_name" content="Garabit">
<meta property="og:type" content="website">
<meta property="og:url" content="https://garabit.barbapapazes.dev">
<meta property="og:title" content="My Awesome Blog Post">
<meta name="twitter:title" content="My Awesome Blog Post">
<meta property="og:description" content="A blog post that will change your life">
<meta name="twitter:description" content="A blog post that will change your life">
<link rel="canonical" href="https://garabit.barbapapazes.dev/my-awesome-blog-post">
<!-- ... -->
</head>
</html>
Concerning clean URLs, you might notice the .html
extension at our URLs' end. VitePress generates HTML files for each Markdown file, simplifying deployment since the path /my-exceptional-blog-post.html
directly correlates to the my-exceptional-blog-post.html
file. However, this isn't very SEO-friendly or visually appealing. By setting cleanUrls
to true
in the config.mts
file, we can eliminate the .html
extension from the URL:
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
cleanUrls: true,
})
When this setting is enabled, the server must be configured to serve the /my-exceptional-blog-post
URL with the my-exceptional-blog-post.html
file, without a redirect. Most deployment services like Cloudflare Pages, Vercel, or Netlify accommodate this. Custom servers may need reverse proxy configuration adjustments.
Warning
Always ensure the canonical URL matches the server-served URL. If you opt not to enable this setting, append the .html
extension to the canonical URL.
Sitemap
A sitemap assists search engines in indexing a website. It's an XML file listing all website URLs, allowing search engines to efficiently discover new URLs without entirely crawling the site.
VitePress simplifies sitemap generation. Just add three lines of code in the config.mts
file:
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
sitemap: {
hostname: 'https://garabit.barbapapazes.dev', // Please, change this before deploying
},
})
This will produce a sitemap.xml
file in the .vitepress/dist
directory when building the website with pnpm run build
. You can submit this file to the SEO console of your preferred search engine.
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url>
<loc>https://garabit.barbapapazes.dev/blog.html</loc>
</url>
<url>
<loc>https://garabit.barbapapazes.dev/blog/getting-started-with-vue3-and-vite.html</loc>
</url>
<url>
<loc>https://garabit.barbapapazes.dev/blog/getting-started-with-laravel.html</loc>
</url>
<url>
<loc>https://garabit.barbapapazes.dev/</loc>
</url>
</urlset>
And there you have it, the sitemap is primed for inclusion in the SEO console of your choice.
Layout Automation
To conclude this section, let's streamline our blog's article layout. Previously, we needed to manually set the layout
key to blog-show
in each article's frontmatter. While not challenging, this step is forgettable and repetitive across articles, much like metadata.
To automate this, reuse the transformPageData
option to insert the layout
key into the frontmatter of each article, verifying if the filePath
is located in the src/blog
directory.
import { defineConfig } from 'vitepress'
export default defineConfig({
// ...
transformPageData: (page) => {
// Set layout for blog articles
if (pageData.filePath.startsWith('blog/')) {
pageData.frontmatter.layout = 'blog-show'
}
},
})
This approach allows for removing the layout
key from each article's frontmatter, reducing oversight risks and minimizing redundancy. This exemplifies the strength of automation!