metafy-seo
Version:
Lightweight, dependency-free React components and utilities for managing SEO metadata (meta tags, Open Graph, Twitter Cards) with optional SSR markup generation.
352 lines (273 loc) • 10.6 kB
Markdown
# metafy-seo
> Lightweight React SEO toolkit: inject `<meta>`/`<link>`/`<title>` tags client-side, or generate SSR strings
metafy-seo makes managing SEO metadata in React effortless. Whether you need to update `<head>` on user navigation or emit static `<title>` and `<meta>` tags at build time, metafy’s two-pronged API has you covered:
- **Declarative client-side updates** via `<SeoTags>`
- **Server-side/SSG output** via `generateSeoMarkup`
Out of the box you get:
- Zero runtime dependencies, fully typed in TypeScript
- Built-in presets for site defaults, blog posts, and product pages
- Open Graph & Twitter Card support
- Automatic cleanup of injected tags on unmount
metafy works with Vite, Create React App, Next.js, Express—any React environment. Focus on your content; let metafy handle the SEO plumbing.
[](https://www.npmjs.com/package/metafy-seo)
[](https://npmjs.com/package/metafy-seo)
[](LICENSE)
## 🚀 Installation
```bash
npm install metafy-seo
# or
yarn add metafy-seo
```
## 🔧 Quick Start (Vite + React)
### Manual `<SeoTags>` Usage
```tsx
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { SeoTags } from 'metafy-seo'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
<>
<SeoTags
title="Hello Vite"
description="Manually injecting SEO tags with metafy-seo"
canonical="/"
openGraph={{
url: '/',
title: 'Hello Vite OG',
description: 'Open Graph description here',
images: [{ url: 'https://example.com/og.png', alt: 'Cover' }],
}}
twitter={{
card: 'summary_large_image',
site: '@yourhandle',
title: 'Hello Vite Twitter',
description: 'Twitter card description',
image: 'https://example.com/twitter.png'
}}
extraMeta={[
{ name: 'author', content: 'Nigel' },
{ property: 'article:tag', content: 'metafy-seo' }
]}
extraLinks={[
{ rel: 'stylesheet', href: '/seo-overrides.css' }
]}
/>
<App />
</>
)
```
Run the dev server and “View Source” to see your tags:
```bash
npm run dev
# or
yarn dev
```
### Using Built-in Presets
Instead of crafting each field manually, import a preset:
```tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { SeoTags, blogPostPreset } from 'metafy-seo'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
<>
<SeoTags
{...blogPostPreset({
title: 'Hello Vite',
description: 'Using metafy-seo with Vite + React',
slug: '/posts/hello-vite',
author: 'Nigel',
datePublished: '2025-07-16',
images: ['https://example.com/cover.png'],
siteName: 'My Blog'
})}
/>
<App />
</>
)
```
## 💻 SSR Example (Next.js)
Use `generateSeoMarkup()` to build head tags server-side:
```tsx
// pages/[slug].tsx
import Head from 'next/head'
import type { GetStaticProps } from 'next'
import { generateSeoMarkup, blogPostPreset } from 'metafy-seo'
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await fetchPost(params!.slug as string)
const seo = generateSeoMarkup(
blogPostPreset({
title: post.title,
description: post.excerpt,
slug: `/posts/${post.slug}`,
author: post.author,
datePublished: post.publishedAt,
images: post.images,
siteName: 'My Blog'
})
)
return { props: { post, seo } }
}
export default function PostPage({ post, seo }: any) {
return (
<>
<Head>
{/* dangerouslySetInnerHTML for SSR head tags */}
<div dangerouslySetInnerHTML={{ __html: seo }} />
</Head>
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
</>
)
}
```
## 🌐 Plain Express SSR
```js
// server.js
import express from 'express'
import fs from 'fs'
import path from 'path'
import { generateSeoMarkup, defaultPreset } from 'metafy-seo'
const app = express()
const template = fs.readFileSync(
path.resolve(__dirname, 'dist/index.html'),
'utf-8'
)
app.get('*', (req, res) => {
const seo = generateSeoMarkup(defaultPreset)
const html = template.replace(
'<head>',
`<head>\n${seo}`
)
res.send(html)
})
app.listen(3000, () => {
console.log('Server on http://localhost:3000')
})
```
## 📚 API Reference
### `<SeoTags {...config} />`
A React component that upserts `<title>`, `<meta>`, and `<link>` tags into `<head>` on mount and removes them on unmount.
| Prop | Type | Description |
| -------------- | ------------------ | -------------------------------------------- |
| `title` | `string` | Page title |
| `description` | `string` | Meta description |
| `canonical` | `string` | Canonical URL (rel="canonical") |
| `keywords` | `string[]` | Comma-separated keywords |
| `robots` | `string` | e.g. "index,follow" or "noindex,nofollow" |
| `viewport` | `string` | e.g. "width=device-width, initial-scale=1" |
| `themeColor` | `string` | e.g. "#ffffff" |
| `author` | `string` | Page author |
| `publisher` | `string` | Page publisher |
| `rating` | `string` | e.g. "5/5" |
| `revisitAfter` | `string` | e.g. "7 days" |
| `language` | `string` | e.g. "en-US" |
| `openGraph` | `OpenGraphConfig` | OG tags: `url`, `title`, `description`, etc. |
| `twitter` | `TwitterConfig` | Twitter card tags |
| `extraMeta` | `MetaEntry[]` | Additional `<meta>` entries |
| `extraLinks` | `LinkEntry[]` | Additional `<link>` entries |
### `generateSeoMarkup(config: SeoConfig): string`
Generates a string of `<title>`, `<meta>`, and `<link>` tags from your config. Ideal for SSR or static-site injection.
```js
import { generateSeoMarkup, defaultPreset } from 'metafy-seo'
const headHTML = generateSeoMarkup(defaultPreset)
```
### Built-in Presets
- **`defaultPreset: SeoConfig`**
Basic site-wide defaults: `title`, `description`, `robots`.
- **`blogPostPreset(opts: BlogPostOptions): SeoConfig`**
```ts
interface BlogPostOptions {
title: string
description: string
slug: string
author: string
datePublished: string
images?: string[]
siteName?: string
}
```
- **`productPreset(opts: ProductOptions): SeoConfig`**
```ts
interface ProductOptions {
name: string
description: string
url: string
images?: string[]
price: string
currency: string
availability: 'InStock' | 'OutOfStock' | 'PreOrder'
}
```
## 🛠️ Scripts
```jsonc
{
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && rollup -c",
"prepare": "npm run build"
}
}
```
- `clean` uses [rimraf](https://npmjs.com/package/rimraf) for cross-platform directory removal
- `prepare` runs on `npm publish` or when installing from Git
## 🤔 FAQ
**Q: Do I need to build on install?**
A: No—npm consumes your prebuilt `dist/` files. `prepare` only runs for Git installs or before publishing.
**Q: Can I update SEO tags dynamically at runtime?**
A: Yes, simply render `<SeoTags>` conditionally or update its props. The component’s effect watches `JSON.stringify(config)`, so prop changes will update head tags. For best performance, memoize your config object.
**Q: How do I prevent duplicate tags when using SSR and client-side injection?**
A: In your SSR pipeline, inject tags via `generateSeoMarkup()`. On the client, either skip `<SeoTags>` for those pages or mount it with the identical config so it replaces rather than duplicates tags.
**Q: Will `<SeoTags>` remove tags it didn’t add?**
A: No. It only cleans up tags it inserted (tracked internally). If you need broader cleanup, write a custom effect or rely entirely on SSR.
**Q: How can I include JSON-LD structured data?**
A: Use `extraMeta` to add a script tag:
```tsx
<SeoTags
extraMeta={[{
name: 'application/ld+json',
content: JSON.stringify(myJsonLdObject)
}]}
/>
```
**Q: Is there any performance overhead?**
A: The effect runs after mount and on config changes. Unless you remount constantly with very large configs, the impact is negligible. Memoizing your config can help.
**Q: Can I write unit tests for SEO tags?**
A: Absolutely. In a JSDOM environment (e.g. React Testing Library), render your component and assert on `document.head.querySelector('meta[name="description"]')`, etc.
**Q: What if I render multiple `<SeoTags>`?**
A: You can, but the last one wins on overlapping tags. It’s recommended to consolidate into a single `<SeoTags>` per page.
**Q: Can I use `generateSeoMarkup()` in Deno or edge runtimes?**
A: Yes. It’s pure JavaScript/TypeScript with no Node-only APIs, so you can generate head strings anywhere.
**Q: How do I handle environment-specific tags (dev vs prod)?**
A: Conditionally set fields in your config based on `process.env.NODE_ENV`, or wrap `<SeoTags>` in a component that reads your environment and toggles values accordingly.
## 🤝 Contributing
1. Fork & clone the repo
2. `npm install && npm run build`
3. Create a branch, commit your changes
4. Open a pull request
## 💬 Commit Format
This project uses [Conventional Commits](https://www.conventionalcommits.org) to automate semantic versioning.
Examples:
- `fix: correct meta property fallback`
- `feat: add productPreset`
- `chore: update docs`
- `BREAKING CHANGE: rename blogPostPreset → postPreset`
## 📜 License
ISC © Nigel Basarokwe