@sidhxntt/seo-meta
v2.0.0
Published
Interactive CLI that merges SEO / social-meta tags into any HTML file. No AI calls — pure stdin prompts.
Maintainers
Readme
@sidhxntt/seo-meta
Interactive Node.js CLI that merges every SEO / social-meta tag into an existing HTML file. No AI calls. No network. Pure stdin prompts.
Features
| Category | Tags managed |
|---|---|
| Primary meta | <title>, description, keywords, author, canonical, robots, hreflang, charset, viewport |
| Favicons | 16px, 32px, 180px (apple), 192px, 512px, .ico |
| Open Graph | All standard OG tags + article:* tags |
| Twitter / X Card | All card variants (validated against the four legal values) |
| Theme / PWA | theme-color, color-scheme, apple-mobile-web-app-* |
| Geo | geo.region, geo.placename |
| Performance | preconnect, dns-prefetch |
| JSON-LD | Person, Organization, or Article — plus an optional WebSite node |
| Scripts | Extra <script src> tags with optional defer |
Everything is escaped (&, <, >, ", ') before it reaches an attribute or text node, and
JSON-LD is serialised with < escaped as < so no value can break out of the <script> block.
Requirements
- Node.js ≥ 18 (native ESM)
- One dependency:
node-html-parser
Install
npm install -g @sidhxntt/seo-meta
# or run it without installing
npx @sidhxntt/seo-meta --helpFrom a clone of this repo:
cd seo_meta
npm install
node src/index.js --helpUsage
# Interactive — prompts for everything
seo-meta
# Skip the file-path prompts (both --flag value and --flag=value work)
seo-meta --input ./index.html --output ./dist/index.html
seo-meta --input=./index.html --output=./dist/index.html
# Preview only: prints the resulting <head> to stdout, writes nothing
seo-meta --input ./index.html --dry-run
# Replay a previous run with zero prompts (CI-friendly)
seo-meta --config ./dist/index.seo-config.json --yesOptions
| Flag | Description |
|---|---|
| -i, --input <path> | Input HTML file. Validated before any other prompt. |
| -o, --output <path> | Output HTML file. Defaults to <name>.seo.html, never the input. |
| -c, --config <path> | Load a saved .seo-config.json and use it as prompt defaults. |
| -y, --yes | Non-interactive: take every value from --config, skip all prompts. |
| --dry-run | Print the resulting <head> to stdout; write nothing. |
| -h, --help | Show help. |
| -v, --version | Print the version. |
Unknown flags are an error, not silently ignored.
What happens to my HTML?
The <head> is parsed and merged — never replaced.
- The document is parsed with
node-html-parser(so a<head>mentioned inside a comment or an inline JS string is never mistaken for the real one). - If there is no
<head>, one is inserted as the first child of<html>— after<!DOCTYPE html>, inside the document, in the correct place. If there is no<html>either, a<head>is synthesised before<body>. <meta charset>and<meta name="viewport">are guaranteed to be present, charset first.- For each tag the tool generates, the matching existing tag is replaced in place (matched on
name=,property=,rel=+ discriminator,src=, or the tag name for<title>). Anything else — stylesheets, scripts, preloads, custom meta — is preserved byte-for-byte. - Tags with no existing counterpart are appended at the end of
<head>under a<!-- SEO meta tags managed by @sidhxntt/seo-meta -->marker.
Re-running the tool on its own output is idempotent: nothing is duplicated.
Safety
- The default output path is
<name>.seo.html— pressing Enter never overwrites your source. - Whenever an existing file is about to be overwritten, a
.bakcopy is written first and the path is printed. --dry-runwrites nothing at all — not even the config sidecar.
Saved config
After writing the HTML, a <output-name>.seo-config.json sidecar is written next to the output.
Feed it back in to replay the run:
seo-meta --config ./dist/index.seo-config.json --yespersonEmail is never persisted — it is PII, and this file is meant to be committed. The email
still lands in the generated JSON-LD if you supply one.
Validation
Values are validated and re-prompted on failure:
canonicalUrl,og:url, image URLs,sameAsentries → must be absolutehttp(s)URLstheme-color→ must be a hex colourtwitter:card→ must besummary,summary_large_image,app, orplayerog:image:width/height→ whole numbers- language tags → BCP-47-ish shape
- description → hard limit 320 chars, soft warning outside the 120–160 sweet spot
Empty values are omitted from the output entirely, so JSON-LD never ships "name": "".
--yes mode reports invalid config values as errors instead of prompting.
Non-TTY environments
If stdin is not a TTY (CI, piped input) and --yes is not set, the tool exits with a clear message
instead of hanging on readline.
Prompt reference
📁 File Paths — input / output HTML paths
🔤 Primary Meta — title, description, keywords, author, canonical, robots, lang, charset, viewport
📣 Open Graph — og:* tags + optional article:* tags (case-insensitive og:type match)
𝕏 Twitter / X Card — twitter:* tags
🎨 Theme & PWA — theme-color, PWA meta
🌍 Geo / Regional — geo.region, geo.placename
🖼️ Favicons — paths for all icon sizes
🧩 JSON-LD — Person / Organization / Article + optional WebSite
⚡ Performance Hints — preconnect, dns-prefetch URLs
📦 Extra Scripts — arbitrary <script src> tagsSample
sample/input.html is a document with no <head>. Running:
node src/index.js --input sample/input.html --output sample/output.htmlproduces sample/output.html — a valid document with <head> correctly nested inside <html>,
after the doctype — plus sample/output.seo-config.json.
License
MIT
