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

sanity-plugin-seo-analyser

v0.1.2

Published

A Yoast-style SEO analyser view for Sanity Studio v3+ — SEO, Readability, and Social tabs with full Portable Text support.

Readme

sanity-plugin-seo-analyser

A Yoast-style SEO analyser view for Sanity Studio v3, v4, and v5. Adds a dedicated SEO Analysis tab to any document type with three panels: SEO, Readability, and Social preview.

Features

  • SEO tab — keyphrase checks (title, meta description, introduction, subheadings, image alts, density, distribution, slug, competing links), duplicate keyphrase detection across documents, internal/outbound link counts, word count, SERP preview
  • Readability tab — sentence length, paragraph length, subheading distribution, consecutive sentences, transition word usage
  • Social tab — Facebook/LinkedIn OG preview and X (Twitter) card preview with actual image rendering
  • Fully recursive text extraction — works with standard Portable Text blocks and any custom block types
  • Configurable field names, document types, word count threshold, and custom block text keys
  • Works with Sanity v3, v4, and v5

Installation

npm install sanity-plugin-seo-analyser

Requirements

Your document schema needs an seo object field with these sub-fields (field names are configurable):

// Minimum required seo fields
{
  focusKeyword: string       // the keyphrase to analyse
  synonyms: string           // comma-separated synonyms (optional)
  metaTitle: string
  metaDescription: string
  ogImage: image
  socialTitle: string        // optional override for OG title
  socialDescription: string  // optional override for OG description
}

The document also needs:

  • title — string
  • slug — slug field ({ current: string })
  • content — Portable Text array

All field names are configurable if yours differ (see Config options).


Usage

In your sanity.config.ts, use createSeoAnalyser to create the view component, then add it as a Studio view:

import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { createSeoAnalyser } from 'sanity-plugin-seo-analyser'

const SeoAnalyser = createSeoAnalyser({
  siteHost: 'yoursite.com',
  documentTypes: ['post', 'article'],
})

export default defineConfig({
  // ...
  plugins: [
    structureTool({
      structure: (S) =>
        S.list()
          .title('Content')
          .items([
            S.listItem().title('Blog Posts').schemaType('post').child(
              S.documentTypeList('post').child((id) =>
                S.document().documentId(id).schemaType('post').views([
                  S.view.form().title('Content'),
                  S.view.component(SeoAnalyser).title('SEO Analysis'),
                ])
              )
            ),
          ]),
    }),
  ],
})

Config options

| Option | Type | Default | Description | |---|---|---|---| | siteHost | string | required | Your domain, e.g. "example.com". Used to classify internal vs outbound links. | | documentTypes | string[] | [] | Document types to search when checking for duplicate keyphrases, e.g. ["post", "article"]. | | seoFieldName | string | "seo" | Field name of the SEO object on your document. | | contentFieldName | string | "content" | Field name of your Portable Text array. | | titleFieldName | string | "title" | Field name of the document title. | | slugFieldName | string | "slug" | Field name of the slug object. | | minWords | number | 300 | Minimum recommended word count before a warning is shown. | | extraTextKeys | string[] | [] | Additional string field names to extract text from inside custom block types (merged with built-in defaults). | | extraStringArrayKeys | string[] | [] | Additional array-of-strings field names to extract from custom block types. |

Example with all options

const SeoAnalyser = createSeoAnalyser({
  siteHost: 'example.com',
  documentTypes: ['post', 'caseStudy', 'article'],
  seoFieldName: 'seo',
  contentFieldName: 'body',
  titleFieldName: 'heading',
  slugFieldName: 'slug',
  minWords: 500,
  extraTextKeys: ['subtitle', 'pullQuote'],
  extraStringArrayKeys: ['bulletPoints'],
})

Custom block types

The analyser recursively extracts text from any block type. By default it reads these field names: heading, subheading, title, description, text, quote, question, answer, label, value, leftHeading, rightHeading, caption, authorName, authorTitle, authorOrg.

If your custom blocks use different field names, pass them via extraTextKeys:

const SeoAnalyser = createSeoAnalyser({
  siteHost: 'example.com',
  extraTextKeys: ['summary', 'pullQuote', 'tagline'],
})

SEO checks performed

| Check | Description | |---|---| | Keyphrase length | Warns if keyphrase is longer than 6 words | | Keyphrase in SEO title | Checks presence and position (beginning = best) | | Keyphrase in meta description | Checks keyphrase or synonym presence | | Keyphrase in introduction | Checks first paragraph | | Keyphrase in subheadings | Checks H2/H3 headings | | Keyphrase in image alts | Checks alt text across all images | | Keyphrase density | Flags if below 0.5% or above 3% | | Keyphrase distribution | Checks if keyphrase appears in all thirds of the content | | Keyphrase in slug | Checks if slug contains keyphrase words | | Competing links | Flags anchor text that uses the keyphrase | | Previously used keyphrase | Live GROQ query to detect keyword cannibalization | | Single H1 | Warns if multiple H1 headings exist | | Internal links | Counts links to your own domain | | Outbound links | Counts links to external sites | | Images | Checks at least one image exists | | Text length | Warns if below minimum word count |

Readability checks performed

| Check | Description | |---|---| | Sentence length | Flags if more than 25% of sentences exceed 20 words | | Paragraph length | Flags paragraphs over 150 words | | Subheading distribution | Flags sections over 300 words without a subheading | | Consecutive sentences | Flags 3+ consecutive sentences starting with the same word | | Transition words | Flags if fewer than 20% of sentences use transition words |


License

MIT