npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

seord

v2.0.0

Published

Advanced SEO Analyzer

Readme

SEO Analyzer - SEOrd

SEOrd (pronounced "sword") is an advanced content SEO Analyzer library that allows you to perform a rapid SEO analysis on your rich text-ed content. SEOrd helps check the SEO friendliness of a website by looking at different factors such as keyword density, meta description, and link analysis (internal and external link).

I created this library because I had a hard time finding a good SEO analyzer library that can be used in Node.js. I hope this library will help you as well.

Note: This library analyzes the SEO of the given content on the basis of the standard on-page SEO rules. This library is just a tool to help you improve your SEO score or use it as a reference or a supplement to your webapps.

Install

npm i seord

Features

  • Rule-based engine: 22 focused SEO checks, each individually disableable
  • Weighted SEO score with a per-rule breakdown, so the number is explainable
  • Keyword density, frequency, and placement analysis
  • Sub keyword density analysis in content, title, and meta description
  • Title and meta description length checks
  • Headings structure analysis (single h1, h2/h3 presence, keywords in headings)
  • Internal and outbound link analysis, including duplicates
  • SEO messages: warnings, minor warnings, and good points
  • Fully typed (TypeScript), synchronous, and dependency-light (only cheerio)

Quick start

import { analyzeSeo } from "seord";

const content = {
    title: 'Does Progressive raise your rates after 6 months?',
    htmlText: `
        <h1>Does Progressive Raise Your Rates After 6 Months?</h1>
        <h2>How Are Car Insurance Rates Determined?</h2>
        <p>When it comes to car insurance, finding the right provider matters.
        Progressive is one of the largest insurers and progressive rates depend on
        many factors like driving history, location and coverage level.</p>
        <h3>How to Save Money on Car Insurance</h3>
        <p>You can save money by comparing rates and premiums across providers.
        Visit <a href="/guides/insurance">our insurance guide</a> or
        <a href="https://www.iii.org">the Insurance Information Institute</a>.</p>
    `,
    keyword: 'progressive',
    subKeywords: ['car insurance', 'rates'],
    metaDescription: 'Find out if Progressive raises your rates after 6 months and what factors can impact your insurance premiums. Learn how to save money on car insurance.',
};

const result = analyzeSeo(content, { siteDomainName: 'example.com' });

console.log(`SEO Score: ${result.seoScore}`);
console.log(`Keyword SEO Score: ${result.keywordSeoScore}`);
console.log(`Keyword Density: ${result.keyword.density.toFixed(2)}%`);
console.log(`Keyword Frequency: ${result.keyword.frequency}`);
console.log(`Word Count: ${result.words.total}`);
console.log(`Total Links: ${result.links.total}`);

console.log(`\nWarnings: ${result.messages.warnings.length}`);
result.messages.warnings.forEach((warning) => console.log(`  - ${warning}`));

console.log(`\nGood Points: ${result.messages.goodPoints.length}`);
result.messages.goodPoints.forEach((point) => console.log(`  - ${point}`));

console.log(`\nMinor Warnings: ${result.messages.minorWarnings.length}`);
result.messages.minorWarnings.forEach((minor) => console.log(`  - ${minor}`));

Output:

SEO Score: 84.18
Keyword SEO Score: 100
Keyword Density: 4.11%
Keyword Frequency: 3
Word Count: 73
Total Links: 2

Warnings: 3
  - Keyword density is too high. It is 4.11%, try decreasing it.
  - The density of sub keyword "car insurance" is too high in the content, i.e. 4.11%.
  - The density of sub keyword "rates" is too high in the content, i.e. 5.48%.

Good Points: 23
  - Good, your content has a keyword "progressive".
  - No keyword overstuffing.
  - Good, your content has sub keywords "car insurance, rates".
  - Title tag is 49 characters long.
  - Keyword density in title is 12.50%, which is good.
  - You have 1 sub keywords in title.
  - Meta description is 151 characters long.
  - Keyword density of meta description is 3.85%, which is good.
  - The density of sub keyword "car insurance" in meta description is 3.85%.
  - The density of sub keyword "rates" in meta description is 3.85%.
  - You have 3 headings.
  - Nice. You have h1 tag, which is essential.
  - Nice. You have h2 tag, which is essential.
  - You have h3 tag, which is good.
  - Keyword "progressive" found in H1 tag "Does Progressive Raise Your Rates After 6 Months?".
  - Sub keyword "rates" found in H1 tag "Does Progressive Raise Your Rates After 6 Months?", which is good.
  - Sub keyword "car insurance" found in H2 tag "How Are Car Insurance Rates Determined?", which is good.
  - Sub keyword "rates" found in H2 tag "How Are Car Insurance Rates Determined?", which is good.
  - Sub keyword "car insurance" found in H3 tag "How to Save Money on Car Insurance", which is good.
  - You have 1 internal links.
  - You have 1 outbound links.
  - No duplicate internal links.
  - No duplicate outbound links.

Minor Warnings: 5
  - Meta description does not start with keyword. It starts with "find out if progress", try starting with keyword. Not starting with keyword is not a big issue, but it is recommended to start with keyword.
  - Keyword "progressive" not found in H2 tag "How Are Car Insurance Rates Determined?".
  - Keyword "progressive" not found in H3 tag "How to Save Money on Car Insurance".
  - Sub keyword "car insurance" not found in H1 tag "Does Progressive Raise Your Rates After 6 Months?".
  - Sub keyword "rates" not found in H3 tag "How to Save Money on Car Insurance".

API

analyzeSeo(content, options?)

Synchronous. Returns a SeoAnalysisResult.

content (required)

| Field | Type | Description | | --- | --- | --- | | title | string | The page title (the <title> tag text) | | htmlText | string | The HTML content to analyze | | keyword | string | The main keyword to check for | | subKeywords | string[] | Secondary keywords (pass [] for none) | | metaDescription | string | The meta description text |

The input object is never mutated. Matching is case-insensitive.

options (optional)

| Option | Type | Description | | --- | --- | --- | | siteDomainName | string | Your domain, e.g. "example.com". Absolute links containing it count as internal; without it, only relative links (/, ./, ../, #) count as internal. | | rules | Record<string, "off"> | Disable individual rules by id, e.g. { "heading-h3-presence": "off" }. Disabled rules produce no messages and are excluded from the score entirely. |

Rules reference

Every check is a rule with an id and a weight. Failing severity is what the rule reports when the check fails.

| Rule id | Checks | Weight | Failing severity | | --- | --- | --- | --- | | keyword-presence | A main keyword is provided | 10 | warning | | keyword-overstuffing | Keyword density is not above 5% | 8 | warning | | keyword-density | Keyword density between 0.46% and 1.1% | 10 | warning | | sub-keywords-presence | Sub keywords are provided | 4 | minor | | sub-keyword-density | Each sub keyword density between 0.12% and 0.9% | 6 | warning / minor | | title-length | Title between 40 and 70 characters | 8 | warning | | title-keyword | Main keyword appears in the title | 8 | warning | | title-sub-keywords | At least one sub keyword appears in the title | 4 | minor | | meta-description-length | Meta description between 100 and 160 characters | 8 | warning | | meta-description-keyword-density | Keyword density in meta description between 2% and 5% | 6 | warning | | meta-description-starts-with-keyword | Meta description starts with the keyword | 2 | minor | | meta-description-sub-keywords | Each sub keyword density in meta description between 2% and 5% | 4 | warning / minor | | headings-presence | At least one heading tag exists | 6 | warning | | heading-single-h1 | Exactly one h1 tag | 8 | warning | | heading-h2-presence | At least one h2 tag | 4 | warning | | heading-h3-presence | At least one h3 tag | 2 | minor | | headings-keyword | Keyword appears in each heading | 4 | minor | | headings-sub-keywords | Sub keywords appear in each heading | 2 | minor | | links-internal-count | At least one unique internal link per 300 words | 6 | warning | | links-outbound-count | At least one unique outbound link per 400 words | 4 | warning | | links-internal-duplicates | Not more than one duplicate internal link | 2 | minor | | links-outbound-duplicates | Not more than one duplicate outbound link | 2 | minor |

Rules that do not apply are skipped without penalty. For example, sub keyword rules are skipped when subKeywords is empty, and keyword rules are skipped when keyword is empty.

Scoring

seoScore is a weighted ratio: each rule that passes earns its full weight, a minor warning earns half, and a warning earns nothing. The score is earned weight divided by the total weight of all applicable rules, times 100. Disabled and skipped rules leave the denominator, so turning a rule off never penalizes (or inflates) your score. Rules that produce several results (one per sub keyword or per heading) are averaged so their influence never grows with input size.

keywordSeoScore is a separate density-based figure (keyword density in content and title, plus sub keyword presence), capped at 100.

Result shape

interface SeoAnalysisResult {
    seoScore: number;
    keywordSeoScore: number;
    messages: { warnings: string[]; minorWarnings: string[]; goodPoints: string[] };
    rules: Array<{ id: string; status: "good" | "warning" | "minor"; weight: number; message: string }>;
    keyword: { density: number; frequency: number; inTitle: KeywordDensity };
    subKeywords: { density: KeywordDensity[]; inTitle: KeywordDensity[] };
    links: { total: number; internal: LinksGroup; outbound: LinksGroup };
    words: { total: number; title: number };
}

interface KeywordDensity { keyword: string; density: number; position?: number }
interface LinksGroup { all: Link[]; duplicate: Link[]; unique: Link[] }
interface Link { text: string; href: string }

You do not need TypeScript to use this library. The above is just to show the data structure.

Migrating from 0.0.x

Version 2.0.0 is a clean break from the SeoCheck class API:

  • new SeoCheck(contentJson, domain) + await seoCheck.analyzeSeo() becomes analyzeSeo(content, { siteDomainName: domain }). The call is synchronous; drop the await.
  • strictMode is gone. The old non-strict default treated your <title> as an extra <h1>, which produced a false "more than one h1 tag" warning on pages with exactly one real h1 (issue #4). The title is now never counted as a heading; title checks are their own rules.
  • languageCode and countryCode are no longer accepted; they were never used.
  • titleSEO.keywordWithTitle is now keyword.inTitle, titleSEO.subKeywordsWithTitle is subKeywords.inTitle, titleSEO.wordCount is words.title, wordCount is words.total, keywordDensity is keyword.density, keywordFrequency is keyword.frequency, subKeywordDensity is subKeywords.density, and totalLinks/internalLinks/outboundLinks live under links.
  • seoScore is now weighted (see Scoring above), so the number will differ from 0.0.x for the same content. messages keeps the same three-bucket shape.

Issue Reporting

If you have found a bug or if you have a feature request, please report them at this repository issues section.

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

License

This project is licensed under the MIT license. Please see the LICENSE file for more information.

Please note that this library is not affiliated with Google or any other search engine.

It does not guarantee the SEO score calculated by this library will be the same as the SEO score calculated by Google or any other search engine.