metalsmith-seo
v0.4.0
Published
A metalsmith plugin for SEO optimization including sitemap generation, meta tags, and more.
Maintainers
Readme
metalsmith-seo
Inspired by metalsmith-sitemap, the plugin provides SEO optimization for Metalsmith with metadata generation, social media tags, and structured data including Open Graph tags, Twitter Cards, JSON-LD structured data, and sitemap generation.
This Metalsmith plugin is under active development. The API is stable, but breaking changes may occur before reaching 1.0.0.
Features
Core SEO Optimization:
- HTML Head Optimization - Meta tags, canonical URLs, robots directives
- Open Graph Tags - Social media sharing with Facebook, LinkedIn, etc.
- Twitter Cards - Rich Twitter previews with automatic card type detection
- JSON-LD Structured Data - Article, Product, Organization, WebPage schemas
- Sitemap Generation - Complete sitemap.xml with auto-calculation of priority, changefreq, and lastmod
- Robots.txt Management - robots.txt generation and sitemap coordination
Smart Automation:
- Content Analysis - Auto-detects content type (article, product, page)
- Metadata Derivation - Single source feeds all formats (title → og:title, twitter:title, JSON-LD headline)
- Fallback Chains - Defaults from site.json, frontmatter, or content analysis
- Site.json Integration - Integration with existing Metalsmith site configuration
Developer Experience:
- ESM/CommonJS Support - Works in any Node.js environment
- Minimal Configuration - Works great with just a hostname
- Comprehensive Testing - 94% test coverage with real-world scenarios
Installation
npm install metalsmith-seoUsage
Quick Start
Minimal Setup
ESM (ES Modules):
import Metalsmith from 'metalsmith';
import seo from 'metalsmith-seo';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
Metalsmith(__dirname)
.use(
seo({
hostname: 'https://example.com',
})
)
.build();CommonJS:
const Metalsmith = require('metalsmith');
const seo = require('metalsmith-seo');
Metalsmith(__dirname)
.use(
seo({
hostname: 'https://example.com',
})
)
.build();This simple configuration automatically generates:
- Complete HTML meta tags
- Open Graph tags for social sharing
- Twitter Card tags
- JSON-LD structured data
- sitemap.xml with intelligent priority/changefreq/lastmod values
- robots.txt (with sitemap reference)
With site.json Integration (Recommended)
Create data/site.json:
{
"name": "My Awesome Site",
"title": "My Site - Welcome",
"description": "The best site on the internet",
"url": "https://example.com",
"locale": "en_US",
"twitter": "@mysite",
"organization": {
"name": "My Company",
"logo": "https://example.com/logo.png"
}
}Then use the plugin:
import Metalsmith from 'metalsmith';
import metadata from '@metalsmith/metadata';
import seo from 'metalsmith-seo';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
Metalsmith(__dirname)
.use(metadata({ site: 'data/site.json' }))
.use(seo()) // Automatically uses site.json values!
.build();Or if your site metadata is nested differently:
import Metalsmith from 'metalsmith';
import metadata from '@metalsmith/metadata';
import seo from 'metalsmith-seo';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
// If metadata is at metadata().data.site instead of metadata().site
Metalsmith(__dirname)
.use(
metadata({
data: {
site: 'data/site.json',
},
})
)
.use(
seo({
metadataPath: 'data.site', // Tell plugin where to find site metadata
})
)
.build();Frontmatter Integration
Add SEO data to any page. The plugin intelligently extracts metadata from multiple locations:
---
title: 'My Blog Post'
date: 2024-01-15
seo:
title: 'Advanced SEO Techniques - My Blog'
description: 'Learn how to optimize your site for search engines'
image: '/images/seo-guide.jpg'
type: 'article'
---Card Object Support
The plugin also extracts metadata from card objects (commonly used for blog post listings):
---
layout: pages/sections.njk
draft: false
seo:
title: 'Override Title for SEO' # Highest priority
description: 'SEO-specific description'
card:
title: 'Architecture Philosophy' # Used if not in seo object
date: '2025-06-02'
author:
- Albert Einstein
- Isaac Newton
image: '/assets/images/sample9.jpg'
excerpt: 'This starter embodies several key principles...'
---Metadata Extraction Priority:
seoobject (highest priority - explicit SEO overrides)cardobject (for blog posts and content cards)- Root level properties
- Configured defaults
- Site-wide defaults (from site.json)
- Auto-generated content
Author Fallback Chain:
When no author is specified in frontmatter, the plugin uses siteOwner from your site.json as a fallback, ensuring all content has proper attribution for SEO and social media.
Result: Comprehensive SEO markup automatically generated:
<!-- Basic Meta -->
<title>Advanced SEO Techniques - My Blog</title>
<meta name="description" content="Learn how to optimize your site for search engines" />
<link rel="canonical" href="https://example.com/blog/advanced-seo" />
<!-- Open Graph -->
<meta property="og:title" content="Advanced SEO Techniques - My Blog" />
<meta property="og:type" content="article" />
<meta property="og:image" content="https://example.com/images/seo-guide.jpg" />
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Advanced SEO Techniques - My Blog" />
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Advanced SEO Techniques - My Blog",
"image": "https://example.com/images/seo-guide.jpg",
"datePublished": "2024-01-15",
"author": { "@type": "Person", "name": "Site Author" }
}
</script>Site.json Configuration
The plugin integrates seamlessly with your existing site.json configuration:
Complete site.json Example
{
"name": "My Awesome Site",
"title": "My Site - Home Page",
"description": "The default description for all pages",
"url": "https://example.com",
"locale": "en_US",
"defaultImage": "/images/default-og.jpg",
"twitter": "@mysite",
"facebookAppId": "123456789",
"siteOwner": "Your Name",
"organization": {
"name": "My Company",
"logo": "https://example.com/logo.png",
"sameAs": [
"https://twitter.com/mycompany",
"https://facebook.com/mycompany",
"https://linkedin.com/company/mycompany"
],
"contactPoint": {
"telephone": "+1-555-123-4567",
"contactType": "customer service"
}
},
"social": {
"twitterCreator": "@author",
"locale": "en_US"
},
"sitemap": {
"changefreq": "weekly",
"priority": 0.8
}
}Site.json Property Mapping
| site.json Property | SEO Usage | Example |
| ------------------ | ------------------------ | --------------------------- |
| url | Hostname for all URLs | https://example.com |
| name / title | Site name in Open Graph | og:site_name |
| description | Default meta description | <meta name="description"> |
| defaultImage | Default social image | og:image, twitter:image |
| locale | Content language | og:locale |
| twitter | Twitter site handle | twitter:site |
| facebookAppId | Facebook integration | fb:app_id |
| siteOwner | Default author fallback | <meta name="author"> |
| organization | Company info | JSON-LD Organization schema |
Configuration Precedence
The plugin uses this priority order:
- Page frontmatter (
seoproperty) - Highest priority - Plugin options - Override site defaults
- site.json values - Site-wide defaults
- Intelligent fallbacks - Auto-generated from content
Plugin Options
Basic Configuration
.use(seo({
hostname: 'https://example.com', // Required if not in site.json
// Global defaults for all pages
defaults: {
title: 'My Site',
description: 'Default page description',
socialImage: '/images/default-og.jpg'
},
// Social media configuration
social: {
siteName: 'My Site',
twitterSite: '@mysite',
twitterCreator: '@author',
facebookAppId: '123456789',
locale: 'en_US'
},
// JSON-LD structured data
jsonLd: {
organization: {
name: 'My Company',
logo: 'https://example.com/logo.png'
}
}
}))Advanced Options
.use(seo({
hostname: 'https://example.com',
// Customize where to find site metadata
metadataPath: 'site', // Default: 'site' (can be 'data.site' or any path)
// Customize frontmatter property name
seoProperty: 'seo', // Default: 'seo'
// Fallback property mappings
fallbacks: {
title: 'title',
description: 'excerpt',
image: 'featured_image',
author: 'author.name'
},
// Sitemap configuration
sitemap: {
output: 'sitemap.xml',
auto: true, // Default: true (intelligent auto-calculation)
changefreq: 'weekly', // Override auto-calculation
priority: 0.8, // Override auto-calculation
omitIndex: false
},
// Robots.txt configuration
robots: {
generateRobots: true, // Generate robots.txt if none exists
addSitemapReference: true, // Add sitemap reference to existing robots.txt
disallowPaths: ['/admin/', '/private/'], // Paths to disallow
userAgent: '*' // User agent directive
},
// Performance options
batchSize: 10, // Process files in batches
enableSitemap: true, // Generate sitemap.xml
enableRobots: true // Generate/update robots.txt
}))SEO Property Reference
Core SEO Properties (Frontmatter)
seo:
# Essential properties (covers 90% of SEO needs)
title: 'Page-specific title'
description: 'Page-specific description'
image: '/images/page-image.jpg'
# Content type (auto-detected if not specified)
type: 'article' # article, product, page, local-business
# URL and indexing
canonicalURL: 'https://example.com/custom-url'
robots: 'index,follow' # Default: "index,follow"
noIndex: false # Exclude from search engines
# Dates (auto-detected from frontmatter if available)
publishDate: '2024-01-15'
modifiedDate: '2024-01-20'
# Author and content metadata
author: 'John Doe'
keywords: ['seo', 'metalsmith', 'optimization']Content Type Detection
The plugin automatically detects content type:
- Article: Has
dateandauthorortags - Product: Has
priceorskuproperties - Local Business: Has
addressorphone - Page: Default fallback
Output Examples
Blog Article
Input:
---
title: 'Ultimate SEO Guide'
date: 2024-01-15
author: 'Jane Smith'
tags: ['seo', 'marketing']
seo:
description: 'Complete guide to SEO optimization'
image: '/images/seo-guide.jpg'
---Generated SEO:
<title>Ultimate SEO Guide</title>
<meta name="description" content="Complete guide to SEO optimization" />
<meta property="og:type" content="article" />
<meta property="og:article:author" content="Jane Smith" />
<meta property="og:article:published_time" content="2024-01-15" />
<meta property="og:article:tag" content="seo" />
<meta property="og:article:tag" content="marketing" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Ultimate SEO Guide",
"author": { "@type": "Person", "name": "Jane Smith" },
"datePublished": "2024-01-15",
"keywords": ["seo", "marketing"]
}
</script>Product Page
Input:
---
title: 'Amazing Widget'
price: '$99.99'
seo:
description: 'The best widget money can buy'
image: '/images/widget.jpg'
type: 'product'
---Generated SEO:
<title>Amazing Widget</title>
<meta property="og:type" content="product" />
<meta property="og:price:amount" content="99.99" />
<meta property="og:price:currency" content="USD" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Amazing Widget",
"offers": {
"@type": "Offer",
"price": "99.99",
"priceCurrency": "USD"
}
}
</script>Robots.txt Management
The plugin intelligently handles robots.txt files:
Automatic Generation
If no robots.txt exists, the plugin generates a basic one:
User-agent: *
Disallow:
Sitemap: https://example.com/sitemap.xmlSmart Coordination with Existing Files
If robots.txt already exists, the plugin:
- Preserves existing content - Never overwrites your custom directives
- Adds sitemap reference - Automatically adds sitemap URL if missing
- Avoids duplicates - Won't add multiple sitemap references
Example - Before:
User-agent: *
Disallow: /admin/
Disallow: /private/Example - After plugin processing:
User-agent: *
Disallow: /admin/
Disallow: /private/
Sitemap: https://example.com/sitemap.xmlCustom Robots.txt Configuration
.use(seo({
hostname: 'https://example.com',
robots: {
generateRobots: true, // Generate if missing (default: true)
addSitemapReference: true, // Add sitemap to existing (default: true)
disallowPaths: ['/admin/', '/api/'], // Paths to disallow
userAgent: 'Googlebot' // Specific user agent (default: '*')
}
}))Generated output:
User-agent: Googlebot
Disallow: /admin/
Disallow: /api/
Sitemap: https://example.com/sitemap.xmlDisabling Robots.txt Processing
.use(seo({
hostname: 'https://example.com',
enableRobots: false // Skip robots.txt processing entirely
}))Sitemap Generation
Sitemap Configuration Options
All sitemap options are configured under the sitemap property:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| output | string | 'sitemap.xml' | Filename for the generated sitemap |
| pattern | string | '**/*.html' | Glob pattern to match files for inclusion |
| auto | boolean | false | Enable automatic priority and changefreq calculation |
| changefreq | string | - | Default change frequency (always, hourly, daily, weekly, monthly, yearly, never) |
| priority | number | - | Default priority (0.0 to 1.0) |
| lastmod | Date|string | - | Default last modified date for all files |
| omitIndex | boolean | false | Remove /index.html from URLs (e.g., about/index.html → about/) |
| urlProperty | string | 'canonical' | Frontmatter property name to read canonical URL overrides |
| modifiedProperty | string | 'lastmod' | Frontmatter property name to read last modified dates |
| privateProperty | string | 'private' | Frontmatter property to exclude files (if true, file is excluded) |
| priorityProperty | string | 'priority' | Frontmatter property name to read priority values |
| links | string | - | Property name for alternate language links (hreflang) |
URL Transformation Examples:
// Example 1: Default behavior (no transformation)
// File: about/index.html → URL: https://example.com/about/index.html
.use(seo({ hostname: 'https://example.com' }))
// Example 2: Clean URLs with omitIndex (recommended for permalink-style URLs)
// File: about/index.html → URL: https://example.com/about/
.use(seo({
hostname: 'https://example.com',
sitemap: { omitIndex: true }
}))
// Example 3: Permalink-style URLs (for use with metalsmith-permalinks)
// File: blog/my-post/index.html → URL: https://example.com/blog/my-post/
.use(seo({
hostname: 'https://example.com',
sitemap: { omitIndex: true }
}))Excluding Files from Sitemap:
---
title: 'Draft Page'
private: true # This page won't appear in sitemap
---Or use a custom property name:
.use(seo({
hostname: 'https://example.com',
sitemap: {
privateProperty: 'draft' // Exclude files with draft: true
}
}))Intelligent Auto-Calculation (Default)
By default, the plugin automatically calculates optimal values for sitemap entries:
.use(seo('https://example.com')) // Auto-calculation enabled by defaultWhat gets auto-calculated:
Priority (0.1-1.0) based on:
- File depth (shallower = higher priority)
- Content type (services/products get higher priority)
- Content age (recent updates get boost)
- Content length (substantial content gets boost)
Change Frequency based on:
- Content type (
/blog/= weekly,/about= yearly) - File modification patterns
- Content freshness analysis
- Content type (
Last Modified using:
- Accurate file system modification dates
- Frontmatter
dateorlastmodproperties - Only included when dates are reliable
Example auto-generated sitemap:
<url>
<loc>https://example.com/index.html</loc>
<lastmod>2024-01-15</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://example.com/blog/seo-guide/index.html</loc>
<lastmod>2024-01-10</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>Manual Override Options
Disable auto-calculation for minimal sitemaps:
.use(seo({
hostname: 'https://example.com',
sitemap: {
auto: false // Disable auto-calculation (minimal sitemap)
}
}))Set global defaults (auto-calculation disabled):
.use(seo({
hostname: 'https://example.com',
sitemap: {
auto: false,
changefreq: 'weekly',
priority: 0.8
}
}))Per-page overrides in frontmatter:
---
title: 'Important Page'
seo:
priority: 1.0 # Override auto-calculated priority
changefreq: 'daily' # Override auto-calculated frequency
lastmod: '2024-01-15' # Override file modification date
---Benefits of Auto-Calculation
Better SEO Performance:
- ✅ Accurate lastmod dates that Google trusts and uses
- ✅ Realistic priorities based on actual content importance
- ✅ Smart change frequencies based on content type patterns
Developer Experience:
- ✅ Zero configuration - works perfectly out of the box
- ✅ No manual maintenance - adapts as your site grows
- ✅ Override capability for special cases
Migration Guide
From metalsmith-sitemap
This plugin includes all metalsmith-sitemap functionality:
Before:
.use(sitemap({
hostname: 'https://example.com',
changefreq: 'weekly',
priority: 0.8
}))After:
.use(seo({
hostname: 'https://example.com',
sitemap: {
changefreq: 'weekly',
priority: 0.8
}
// Now you also get SEO optimization!
}))License
MIT License - see LICENSE file for details.
Contributing
Contributions welcome! Please read our contributing guidelines first.
Attribution
The sitemap functionality in this plugin was inspired by and adapted from:
- metalsmith-sitemap by ExtraHop (MIT License)
Related
- @metalsmith/metadata - For loading site.json
- metalsmith - The static site generator
