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

@syted/next

v0.7.0

Published

Render your Syted articles on your own Next.js site. Turnkey pages with reading progress, sticky table of contents and CTA rail, sitemap and llms.txt handlers, or just the data if you want your own design.

Readme

@syted/next

Render your Syted articles on your own Next.js site. The posts live on yourdomain.com/blog, not on a hosted subdomain, which is what search engines reward.

Two ways to use it. Pick one.

Turnkey: we render the pages

One file. Works with the App Router.

npm i @syted/next
// app/blog/[[...slug]]/page.tsx
import { createBlogPage } from '@syted/next'

const blog = createBlogPage({
  title: 'Blog',
  siteUrl: 'https://acme.com',
  cta: {
    eyebrow: 'Acme',
    title: 'Ship your changelog in minutes',
    description: 'Free while you are under 100 users.',
    buttonLabel: 'Start free',
    href: '/signup',
  },
})

export default blog.Page
export const generateStaticParams = blog.generateStaticParams
export const generateMetadata = blog.generateMetadata
# .env.local — server side only
SYTED_API_KEY=ab_live_...

What you get:

  • A reading progress bar pinned to the top of the window
  • A sticky table of contents on the left, built from the article's own headings
  • Your call to action on the right, and again at the end of the article
  • Index page, article pages, canonical URLs, Open Graph and Twitter cards
  • BlogPosting and BreadcrumbList JSON-LD

No CSS framework required. The styles ship with the component and everything is a CSS variable. The two you will actually want are options:

createBlogPage({
  accent: '#2563eb',            // links, active TOC item, buttons, CTA background
  font: 'var(--font-inter)',
})

Anything else goes through vars, applied inline so it wins over any stylesheet, no matter the load order:

createBlogPage({
  vars: {
    '--ab-fg': '#0a0a0a',
    '--ab-muted': '#6b7280',
    '--ab-border': '#e5e7eb',
    '--ab-radius': '4px',
    '--ab-max': '1200px',       // full grid width
  },
})

Options

| Option | Default | What it does | | --- | --- | --- | | title | 'Blog' | Index heading and breadcrumb label | | description | — | Line under the index heading | | basePath | '/blog' | Where the blog is mounted | | siteUrl | — | Needed for absolute URLs in the breadcrumb JSON-LD | | cta | — | Card in the right rail, reused at the end of the article | | footerCta | falls back to cta | Say something different at the end | | toc | true | Set false to drop the left rail | | tocLabel | 'On this page' | | | headerOffset | 0 | Height of your sticky header, in pixels | | theme | 'light' | 'auto' follows the visitor's dark mode | | accent | near-black | Links, active TOC item, buttons, CTA background | | font | inherited | Font family for the blog | | vars | — | Any other --ab-* variable, applied inline | | pageSize | 9 | Articles per index page. The index is paginated: page 2 and beyond live at <basePath>/page/2, handled by the same catch-all route. Before 0.7.0 this was a hard cap of 50 and the fifty-first article was listed nowhere. | | classes | — | Extra classes on root, index, post, prose, … |

If your site has a sticky header, pass its height. The progress bar sits under it and anchors stop landing behind it:

createBlogPage({ headerOffset: 64 })

Data only: you render it

For teams who want their own design.

import { createClient } from '@syted/next/client'

const blog = createClient()

const { articles, total } = await blog.list({ limit: 20, tag: 'seo' })
const article = await blog.get('some-slug')   // article.body is Markdown
const { tags } = await blog.tags()
const entries = await blog.sitemap()          // merge into your sitemap.ts

The pieces are exported individually, so you can take the parts you want:

import { renderArticle, ReadingProgress, TableOfContents } from '@syted/next'

const { html, headings } = renderArticle(article.body)
// headings: [{ depth: 2, text: 'Why it matters', id: 'why-it-matters' }, …]
// html already carries the matching ids, so the TOC links resolve

renderMarkdown(body) returns just the HTML if you render your own navigation.

Provenance marker

Syted checks every day that your published articles are actually being served, and tells you when one is not. To do that it has to recognise its own page in the HTML you return.

Turnkey does this for you: the blog root carries data-syted-article="<id>". So does the jsonLd object the API returns — its @id is https://syted.ai/a/<id> — so if you inject article.jsonLd in a <script type="application/ld+json">, you are already covered and there is nothing to add.

If you render your own design and do not inject jsonLd, add the marker:

import { SytedArticleMarker } from '@syted/next'

<SytedArticleMarker article={article} />   // or articleId={article.id}

It renders a single <meta name="syted:article">, which React hoists into the <head>. Nothing is visible to a reader, no JavaScript is loaded, and nobody is tracked — it is a serial number, not a beacon.

Without it, Syted falls back to matching the article's title in your HTML. That still works, but a title is a human-readable string: if your template truncates it, appends your site name in a way that splits it across elements, or the article's title is rewritten while your cache still serves the old one, the check can report a perfectly healthy page as unreachable. The marker survives all of that, because it never changes.

Instant updates

Add the revalidation route so a newly published article appears immediately instead of waiting for the cache to expire:

// app/api/syted/revalidate/route.ts
export { POST } from '@syted/next'
SYTED_WEBHOOK_SECRET=...   # shown in your dashboard

Sitemap

Search Console only accepts a sitemap served by the domain it lists, so the blog's entries belong in your /sitemap.xml. The package puts them there.

No app/sitemap.ts yet? One line:

// app/sitemap.ts
export { sitemap as default } from '@syted/next'

Already have one? Spread the blog entries in:

// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { sitemapEntries } from '@syted/next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const pages = [{ url: 'https://acme.com', lastModified: new Date() }]
  return [...pages, ...(await sitemapEntries())]
}

Then submit https://acme.com/sitemap.xml in Search Console → Sitemaps.

Not on Next.js? Syted hosts a sitemap of your articles, current the moment an article is published. Add one line to your own robots.txt:

Sitemap: https://syted.ai/s/<your-site-id>/sitemap.xml

A robots.txt declaration is the one case where a sitemap may live on another host (cross submission, per sitemaps.org), so Google reads it without any code. Do not paste that syted.ai URL into Search Console directly — it is rejected there, because it is not served by your domain.

llms.txt

The plain-text index AI assistants read to find out what a site publishes. Same rule as the sitemap: it only counts served by the domain it describes. One line:

// app/llms.txt/route.ts
export { GET } from '@syted/next/llms'

It lists every published article with its title, absolute URL and one-line description, in the llmstxt.org format, and stays fresh through the same cached client as the rest of the package. Already have one? Append the blog section to yours:

// app/llms.txt/route.ts
import { llmsTxt } from '@syted/next/llms'

export async function GET() {
  const body = [mySections, await llmsTxt()].join('\n\n')
  return new Response(body, {
    headers: { 'content-type': 'text/plain; charset=utf-8' },
  })
}

llmsTxt() takes title, summary, sectionTitle, limit, siteUrl, basePath and extra if you want to word it yourself.

Which articles bring you customers

Turnkey counts two things on your own domain — a visit on every article page, and a click on your call to action — and shows them per article in your Syted dashboard, under Visibility. Traffic tells you who reads; this tells you who moves.

It also tells you who sent them. Each event carries the referrer reduced to one coarse word, so you can see the visits that came from ChatGPT, Perplexity, Gemini or Claude. That is the proof by traffic that assistants are recommending you, not just a citation count.

Nothing to install, no analytics account, no script. And nothing about the reader is recorded:

  • no cookie, no localStorage, nothing written on their machine;
  • no identifier, generated or derived — two visits by the same person are indistinguishable from two people;
  • never the referring URL. It is classified in the browser into one of ai_chatgpt, ai_perplexity, ai_gemini, ai_claude, ai_other, search, social, direct, other — and then thrown away. A referrer can carry a session token or someone's exact search terms; sending it would be tracking;
  • no IP, no user agent, nothing that could form a fingerprint;
  • what is kept is a count per article, per placement, per source, per day.

It is a counter, not tracking, so it adds no consent banner to your site. The value that travels to the browser is a public event token that can do exactly one thing — increment your own counters. Your API key never leaves your server.

Rendering your own pages in data-only mode? article.events carries what they need:

import { PageView, reportEvent } from '@syted/next'

<PageView events={article.events} slug={article.slug} />

<a href="/signup" onClick={() => reportEvent(article.events, article.slug, 'cta_footer')}>
  Start free
</a>

Notes

Your API key reads your articles, including unpublished drafts. Keep it server side: createClient() throws if it detects a browser, rather than leaking it.

MIT.