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

@justin-netage/netage-seo-client

v0.1.0

Published

SEO toolkit for Vite sites: JSON-LD schema builders, meta/OG/Twitter tags, sitemap + robots.txt, plus React and Vue adapters.

Downloads

69

Readme

@justin-netage/netage-seo-client

A batteries-included SEO toolkit for Vite sites.

  • JSON-LD schema builders for Organization, WebSite (sitelinks searchbox), WebPage, Article/BlogPosting/NewsArticle, Product (offer / rating / review), BreadcrumbList, FAQPage, LocalBusiness, Person, Event, Service, VideoObject, HowTo.
  • Meta tag helpers for Open Graph, Twitter Cards, canonical, hreflang, robots, pagination (rel=prev/rel=next), icons, theme-color, etc.
  • Vite plugin that injects site-wide tags into index.html, and emits sitemap.xml and robots.txt at build time.
  • Framework adapters for React (<Seo /> component + useSeo() hook) and Vue (useSeo() composable), each with per-route merge + unmount cleanup.
  • Runtime DOM updater (applySeo) for vanilla SPAs — it reconciles tags it owns (marker data-nseo) without clobbering anything you hand-authored in index.html.
  • Works behind the Netage proxy hub's prerender.io middleware — the tags you inject are rendered to HTML for crawlers with zero extra setup.

Install

npm install @justin-netage/netage-seo-client
# optional peers if you want the adapters:
npm install react vue

Quick start (Vite + React)

1. Configure site-wide defaults + sitemap in vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { seoPlugin } from '@justin-netage/netage-seo-client/vite';
import { organization, website } from '@justin-netage/netage-seo-client/schema';

const BASE_URL = 'https://www.example.com';

export default defineConfig({
  plugins: [
    react(),
    seoPlugin({
      default: {
        baseUrl: BASE_URL,
        titleTemplate: '%s | Example Co',
        defaultTitle: 'Example Co — widgets since 2012',
        description: 'Independent widget manufacturer based in Cape Town.',
        lang: 'en',
        themeColor: '#0b5fff',
        openGraph: {
          type: 'website',
          siteName: 'Example Co',
          locale: 'en_US',
          images: [{ url: `${BASE_URL}/og-default.png`, width: 1200, height: 630 }],
        },
        twitter: { card: 'summary_large_image', site: '@examplecoza' },
        icons: [
          { rel: 'icon', href: '/favicon.ico' },
          { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' },
        ],
        jsonLd: [
          organization({
            name: 'Example Co',
            url: BASE_URL,
            logo: `${BASE_URL}/logo.png`,
            sameAs: [
              'https://twitter.com/examplecoza',
              'https://www.linkedin.com/company/example-co',
            ],
          }),
          website({
            name: 'Example Co',
            url: BASE_URL,
            searchUrl: `${BASE_URL}/search?q={search_term_string}`,
          }),
        ],
      },
      sitemap: {
        baseUrl: BASE_URL,
        entries: [
          { loc: '/', changefreq: 'weekly', priority: 1.0 },
          { loc: '/about', changefreq: 'monthly', priority: 0.6 },
          { loc: '/contact', changefreq: 'monthly', priority: 0.5 },
        ],
      },
      robots: {
        groups: [{ userAgent: '*', allow: ['/'], disallow: ['/admin'] }],
      },
    }),
  ],
});

npm run build now emits dist/sitemap.xml and dist/robots.txt alongside the rest of your assets, and your index.html gets a <head> populated with defaults (title, meta, JSON-LD, canonical, OG/Twitter, icons, etc.).

2. Per-page SEO in React

import { Seo, breadcrumbs, article } from '@justin-netage/netage-seo-client/react';

export function BlogPost({ post }) {
  return (
    <>
      <Seo
        title={post.title}
        description={post.excerpt}
        canonical={`/blog/${post.slug}`}
        openGraph={{
          type: 'article',
          images: [{ url: post.coverImage, width: 1200, height: 630 }],
          article: {
            publishedTime: post.publishedAt,
            modifiedTime: post.updatedAt,
            authors: [post.author.name],
            tags: post.tags,
          },
        }}
        jsonLd={[
          article({
            type: 'BlogPosting',
            headline: post.title,
            description: post.excerpt,
            image: post.coverImage,
            datePublished: post.publishedAt,
            dateModified: post.updatedAt,
            author: { name: post.author.name, url: post.author.url },
            publisher: { name: 'Example Co', logo: '/logo.png' },
          }),
          breadcrumbs([
            { name: 'Home', url: '/' },
            { name: 'Blog', url: '/blog' },
            { name: post.title },
          ]),
        ]}
      />
      {/* …render the post… */}
    </>
  );
}

Quick start (Vue 3)

// main.ts — sitewide defaults via the Vite plugin (same as above)
<!-- BlogPost.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { useSeo, article, breadcrumbs } from '@justin-netage/netage-seo-client/vue';

const props = defineProps<{ post: Post }>();

useSeo(
  computed(() => ({
    title: props.post.title,
    description: props.post.excerpt,
    canonical: `/blog/${props.post.slug}`,
    jsonLd: [
      article({
        type: 'BlogPosting',
        headline: props.post.title,
        datePublished: props.post.publishedAt,
        author: { name: props.post.author.name },
        publisher: { name: 'Example Co', logo: '/logo.png' },
      }),
      breadcrumbs([
        { name: 'Home', url: '/' },
        { name: 'Blog', url: '/blog' },
        { name: props.post.title },
      ]),
    ],
  })),
);
</script>

Vanilla / framework-less SPA

import { applySeo, organization } from '@justin-netage/netage-seo-client';

applySeo({
  title: 'Pricing',
  description: 'Plans for solo, team, and enterprise.',
  canonical: 'https://example.com/pricing',
  jsonLd: organization({ name: 'Example Co', url: 'https://example.com' }),
});

applySeo returns a dispose() that removes every tag it added — wire it into your router's leave hook if you don't want tags from a previous route to linger.

JSON-LD builders

All builders are pure functions that return a plain JsonLdNode you can pass via jsonLd: on any config. They all default @context to https://schema.org.

| Builder | Use for | | ------------------ | --------------------------------------------------------------- | | organization | Company identity (homepage). Unlocks knowledge-panel data. | | localBusiness | Brick-and-mortar / service-area biz. Drives Maps + local pack. | | website | Site root; pair with searchUrl for sitelinks searchbox. | | webpage | Generic page — useful for AboutPage / ContactPage subtypes. | | article | Blog / news / tech articles. Pass type: "BlogPosting" etc. | | product | E-commerce product. Add offers + aggregateRating for stars. | | breadcrumbs | <BreadcrumbList> — shows as path in search snippets. | | faqPage | Expanded FAQ rich result. Only emit if content is visible. | | person | Author / team member pages. | | event | Concerts, webinars, conferences. Honors online/offline mode. | | service | Professional services / SaaS offerings. | | video | VideoObject with thumbnail, clips, live-broadcast support. | | howto | Step-by-step guides with supplies/tools/cost. |

Sitemap + robots.txt

Outside of the Vite plugin you can generate either file imperatively — handy for server-rendered / dynamic sitemaps:

import { buildSitemap, buildRobots } from '@justin-netage/netage-seo-client';

const xml = buildSitemap({
  baseUrl: 'https://www.example.com',
  entries: [
    { loc: '/', changefreq: 'weekly', priority: 1.0 },
    {
      loc: '/blog/hello-world',
      lastmod: new Date(),
      images: [{ loc: '/covers/hello.jpg', title: 'Hello cover' }],
      alternates: [{ hreflang: 'en', href: '/blog/hello-world' }],
    },
  ],
});

const txt = buildRobots({
  groups: [{ userAgent: '*', allow: ['/'], disallow: ['/private'] }],
  sitemaps: ['https://www.example.com/sitemap.xml'],
});

Integration with the Netage proxy hub

If your site sits behind netage-proxy-hub, you get SEO benefits for free:

  1. Crawler HTML via prerender.io. The hub detects bot User-Agents (Googlebot, Facebookbot, Twitterbot, Slackbot, ChatGPT, ClaudeBot, etc.) and routes them through service.prerender.io — which fetches your SPA, executes JS, and returns fully-rendered HTML. Every <meta>, <link>, and <script type="application/ld+json"> this package injects ends up in that rendered HTML. No extra setup.
  2. Canonical-domain rewriting. The hub's prerender middleware rewrites <link rel="canonical"> / og:url to match the customer-facing hostname so crawlers don't see your SPA origin. Set baseUrl in this package to your customer-facing domain and the two agree.
  3. Permissive default robots.txt. The hub serves User-agent: *\nAllow: / on every hostname unless you opt out — social preview bots (Facebook, Twitter, LinkedIn, Slack) never get blocked by accident. If you emit your own robots.txt via this plugin, it takes precedence once deployed (the plugin's file is served from your SPA origin, which the hub proxies).

API reference

renderSeoHtml(config, options?)

Serialize a config to an HTML string suitable for <head>. Used by the Vite plugin and any SSR you wire up yourself.

applySeo(config, doc = document)

Mount a config into a live document. Returns a dispose().

mergeSeo(base, override)

Shallow-merge with deep-merge for openGraph / twitter. Arrays are replaced; if you want to extend meta / jsonLd, spread yourself.

buildSitemap(options) / buildSitemapIndex(baseUrl, sitemaps)

Pure functions returning XML strings. Supports image:image and xhtml:link hreflang extensions.

buildRobots(options)

Pure function returning a robots.txt string. Sane default is User-agent: *\nAllow: /\n.

seoPlugin(options) (Vite)

{
  default?: SeoConfig;
  sitemap?: { baseUrl: string; entries: SitemapEntry[]; filename?: string };
  robots?: RobotsOptions & { filename?: string; includeSitemap?: boolean };
}

Good SEO hygiene this package encourages

  • One canonical per page. The runtime upserts <link rel="canonical"> instead of appending duplicates.
  • Absolute URLs in OG/JSON-LD. Pass baseUrl once and relative canonical / og:url / og:image get resolved for you.
  • Title wrap template. titleTemplate: "%s | Site" keeps titles consistent without repeating the site name on every page.
  • Reconcile, don't duplicate. Runtime tags are marked data-nseo so the package never clobbers tags you hand-wrote in index.html, and it never accumulates stale tags across route changes.
  • Safe JSON-LD escaping. <script> bodies have </script> and <!-- escaped to prevent HTML-injection via author content.

License

MIT © Netage