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

crawlsphere

v0.1.0

Published

Official React SDK for CrawlSphere: typed client, hooks, Next.js helpers, TipTap content renderer, and SEO meta helpers

Readme

@crawlsphere/react

Official React SDK for the CrawlSphere Blog API.

  • Typed client — isomorphic, works in React, Next.js, Node, Cloudflare Workers, Vercel Edge.
  • React hooksusePosts, usePost, useCreatePost, useUpdatePost, useDeletePost, useUploadImage, useGenerateBlogPost, useCompetitors.
  • Beautiful content renderer<BlogContent> turns TipTap JSON into real React elements (no dangerouslySetInnerHTML). Ships a polished CSS file, three built-in themes (light/dark/sepia), CSS-variable theming, per-slot classNames, and full components slot takeover.
  • Next.js helperscreateServerClient() with built-in ISR, postMetadata() for generateMetadata, postStructuredData() for JSON-LD.
  • Full TypeScript — every endpoint has input + response types.

Install

npm install @crawlsphere/react
# or
bun add @crawlsphere/react

Set your API key:

CRAWLSPHERE_API_KEY=cs_live_...
CRAWLSPHERE_API_URL=https://crawlsphere.com   # optional
CRAWLSPHERE_DOMAIN=yourdomain.com             # optional default

Quick start — React

import { CrawlSphereProvider, usePosts, BlogContent } from '@crawlsphere/react';

export function App() {
  return (
    <CrawlSphereProvider options={{ apiKey: 'cs_live_...', domain: 'mysite.com' }}>
      <PostList />
    </CrawlSphereProvider>
  );
}

function PostList() {
  const { data, isLoading, error } = usePosts({ limit: 10 });
  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <ul>
      {data!.posts.map((p) => (
        <li key={p.id}>
          <a href={`/blog/${p.slug}`}>{p.title}</a>
        </li>
      ))}
    </ul>
  );
}

Quick start — Next.js (App Router)

// app/blog/[slug]/page.tsx
import { createServerClient, BlogContent, postMetadata, postStructuredData } from '@crawlsphere/react';

const client = createServerClient({
  apiKey: process.env.CRAWLSPHERE_API_KEY!,
  domain: 'mysite.com',
  revalidate: 60,
});

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await client.getPost(slug);
  return postMetadata(post, { baseUrl: 'https://mysite.com' });
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await client.getPost(slug);
  const jsonLd = postStructuredData(post, { baseUrl: 'https://mysite.com' });

  return (
    <article>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <h1>{post.title}</h1>
      {post.subtitle && <p className="subtitle">{post.subtitle}</p>}
      <BlogContent content={post.content} />
    </article>
  );
}

Styling <BlogContent>

Import the stylesheet once (app entry or app/layout.tsx):

import '@crawlsphere/react/styles.css';

Three levels of control

// 1. Pick a theme + override CSS variables — zero-to-styled in one prop.
<BlogContent
  content={post.content}
  theme="dark"
  style={{
    '--cs-link': '#f472b6',
    '--cs-font-heading': 'ui-serif, Georgia, serif',
    '--cs-max-width': '72ch',
  } as React.CSSProperties}
/>

// 2. Per-slot className overrides — tweak specific nodes.
<BlogContent
  content={post.content}
  classNames={{
    heading: 'font-black tracking-tighter',
    link: 'text-red-600 no-underline',
    blockquote: 'border-pink-500',
  }}
/>

// 3. Component takeover — swap whole renderers.
import Image from 'next/image';
<BlogContent
  content={post.content}
  components={{
    image: (props) => <Image width={1200} height={630} {...props} />,
    pre: MySyntaxHighlighter,
  }}
/>

Variants

<BlogContent variant="default"   />  // cs-* classes + shipped styles.css
<BlogContent variant="prose"     />  // adds Tailwind `prose` + cs-* classes
<BlogContent variant="unstyled"  />  // zero classes — you own styling

Any Google Font, one prop

Pass a fonts prop and the SDK:

  1. Builds the correct Google Fonts CSS v2 URL.
  2. Renders a <link rel="stylesheet"> (React 19 hoists it to <head>).
  3. Sets --cs-font-body, --cs-font-heading, --cs-font-mono for you.
<BlogContent
  content={post.content}
  fonts={{
    body: 'Lora',
    heading: 'Playfair Display',
    mono: 'JetBrains Mono',
    weights: [400, 500, 700, 900],  // optional; default [400,500,600,700]
    display: 'swap',                 // optional; default 'swap'
  }}
/>

Any of the ~1,500 families on fonts.google.com works — just pass the exact family name.

Already loading a font via next/font/google or a global stylesheet? Skip the auto-injected <link>:

<BlogContent fonts={{ body: 'Inter', skipLink: true }} content={post.content} />

Need the link tag rendered somewhere else (e.g. your root layout's <head>)?

import { GoogleFontsLink } from '@crawlsphere/react';

<head>
  <GoogleFontsLink fonts={{ body: 'Inter', heading: 'Playfair Display' }} />
</head>

Curated pairings live in the FONT_PAIRINGS export — drop one straight into the fonts prop:

import { BlogContent, FONT_PAIRINGS } from '@crawlsphere/react';

<BlogContent content={post.content} fonts={FONT_PAIRINGS.softSerif} />
// editorial | magazine | newsprint | docs | terminal |
// softSerif | brutalist | retro | elegant | friendly

Built-in themes

Eight ready-to-use reading aesthetics — pass any of them to theme:

| theme | vibe | when to use | |--------------|--------------------------------------------|--------------------------------------------| | light | Neutral sans, clean defaults | Default. General-purpose. | | dark | Same, dark palette | Dark mode / night reading. | | sepia | Warm serif, paper-tone background | Reading mode. Longform. | | editorial | Charter serif body, sans headings, roomy | Personal essays, Medium-style posts. | | docs | Inter sans, tight, purple accent | Technical docs, API references, Stripe/Linear vibe. | | newsprint | All-serif, narrow column, indented paras | News / opinion. Classic broadsheet feel. | | terminal | Monospace everything, phosphor-on-black | Engineering posts, changelogs, CLI tools. | | magazine | Huge condensed headings, high contrast | Launch posts, manifestos, statement pieces.|

<BlogContent content={post.content} theme="editorial" />
<BlogContent content={post.content} theme="docs" />
<BlogContent content={post.content} theme="terminal" />

Themes are just [data-theme="…"] selectors in the shipped CSS — so you can add your own (theme="brand") by writing a matching CSS block that overrides the variables.

Headings automatically get id="slugified-title" for deep links.


AI agent cheat sheet

Copy this entire block into your assistant (Claude, GPT, Cursor, the @crawlsphere/mcp-server, whatever) when you want it to design the look of a blog post for you. It contains every lever the SDK exposes.

You are styling a `<BlogContent>` component from `@crawlsphere/react`.
You have four props to work with (use the cheapest one that does the job):

### 1. `theme` — single-word aesthetic
One of: `light` (default), `dark`, `sepia`, `editorial`, `docs`,
`newsprint`, `terminal`, `magazine`.

- `editorial`  — Medium-style serif body, sans headings, 18px, roomy.
- `docs`       — Stripe/Linear: Inter, tight 15px, purple accent, code-heavy.
- `newsprint`  — NYT broadsheet: all-serif, narrow, indented paragraphs.
- `terminal`   — hacker: monospace, GitHub-dim palette, `# `/`## ` prefixes.
- `magazine`   — display: 3.5rem Archivo Black headings, red accent.
- `sepia`      — warm paper, reading-mode.
- `dark`       — neutral dark palette.

### 2. `fonts` — ANY Google Font (~1,500 families)
```tsx
fonts={{
  body?:    string,         // e.g. 'Lora', 'Inter', 'Source Serif 4'
  heading?: string,         // defaults to body when omitted
  mono?:    string,         // e.g. 'JetBrains Mono', 'IBM Plex Mono'
  weights?: number[],       // default [400,500,600,700]
  display?: 'swap' | 'block' | 'fallback' | 'auto' | 'optional',
  skipLink?: boolean,       // true if the font is already loaded elsewhere
}}
```
The SDK auto-renders the `<link rel="stylesheet">` and wires the CSS vars.

Curated pairings (import `FONT_PAIRINGS`):
- `editorial`  { body: 'Source Serif 4', heading: 'Inter' }
- `magazine`   { body: 'Inter',          heading: 'Archivo Black' }
- `newsprint`  { body: 'Lora',           heading: 'Playfair Display' }
- `docs`       { body: 'Inter',          heading: 'Inter' }
- `softSerif`  { body: 'Fraunces',       heading: 'Fraunces' }
- `brutalist`  { body: 'IBM Plex Sans',  heading: 'Space Grotesk' }
- `retro`      { body: 'Work Sans',      heading: 'Space Mono' }
- `elegant`    { body: 'Cormorant Garamond', heading: 'Cormorant Garamond' }
- `friendly`   { body: 'Nunito',         heading: 'Quicksand' }

### 3. `style` — CSS custom properties, single prop
Every visible property is a CSS variable. Set as many as needed:
```
--cs-font-body         --cs-text             --cs-quote-bg
--cs-font-heading      --cs-text-muted       --cs-quote-accent
--cs-font-mono         --cs-heading          --cs-quote-text
--cs-font-size         --cs-link             --cs-code-text
--cs-line-height       --cs-link-hover       --cs-code-bg
--cs-max-width         --cs-strong           --cs-code-border
--cs-letter-spacing    --cs-rule             --cs-pre-text
                                              --cs-pre-bg
--cs-gap-block         --cs-caption          --cs-radius
--cs-gap-heading-top
--cs-gap-heading-bottom
```

### 4. `classNames` — per-slot Tailwind/utility overrides
```
root  paragraph  heading  h1 h2 h3 h4 h5 h6
bulletList  orderedList  listItem
blockquote  codeBlock  code  pre
horizontalRule  hardBreak
image  figure  figcaption
link  strong  em  underline  strike
```

### 5. `components` — React component takeover for any slot
```tsx
components={{
  image: (props) => <Image {...props} />,   // e.g. next/image
  pre:   MySyntaxHighlighter,
  link:  MyLink,                             // useful for router links
}}
```

## Workflow
1. Pick the closest `theme` for the overall vibe.
2. Add `fonts` if the user asked for a specific typography feel.
3. Tweak with `style` CSS variables for colors / column width / spacing.
4. Only reach for `classNames` or `components` for changes the above can't
   express.

Emit the smallest possible diff that achieves the user's intent —
ideally a single prop.

Client API

import { createClient } from '@crawlsphere/react';

const client = createClient({ apiKey: 'cs_live_...', domain: 'mysite.com' });

await client.listPosts({ status: 'published', limit: 20 });
await client.getPost('my-slug');
await client.createPost({ title: 'Hello', content: { type: 'doc', content: [] } });
await client.updatePost('my-slug', { status: 'published' });
await client.deletePost('my-slug');
await client.uploadImage(file);
await client.generateBlogPost({ competitorId: 'abc', mode: 'competitive' });
await client.listCompetitors();
await client.createCompetitor({ name: 'Acme', website: 'https://acme.com' });

All methods support AbortSignal:

const ctrl = new AbortController();
client.listPosts({}, { signal: ctrl.signal });
ctrl.abort();

Hooks

| Hook | Type | Description | |------|------|-------------| | usePosts(params?) | query | Paginated list of posts | | usePost(slug) | query | Single post by slug (includes pre-rendered SEO) | | useCompetitors(params?) | query | List competitors | | useCreatePost() | mutation | Create a post | | useUpdatePost() | mutation | Update a post | | useDeletePost() | mutation | Delete a post | | useUploadImage() | mutation | Upload image to CrawlSphere storage | | useGenerateBlogPost() | mutation | AI-generate a blog post from a competitor | | useCreateCompetitor() | mutation | Create a competitor |

Query hooks return { data, error, isLoading, isFetching, refetch }. Mutation hooks return { data, error, isLoading, <action>, reset }.

Error handling

import {
  CrawlSphereError,
  AuthenticationError,
  NotFoundError,
  InsufficientCreditsError,
} from '@crawlsphere/react';

try {
  await client.generateBlogPost({ competitorId: 'abc' });
} catch (err) {
  if (err instanceof InsufficientCreditsError) {
    console.log(`Need ${err.required}, have ${err.credits}`);
  } else if (err instanceof NotFoundError) {
    // 404
  } else if (err instanceof CrawlSphereError) {
    console.log(err.status, err.code, err.data);
  }
}

Environment variables

The client automatically reads these if the matching option isn't passed:

  • CRAWLSPHERE_API_KEY
  • CRAWLSPHERE_API_URL (default: https://crawlsphere.com)
  • CRAWLSPHERE_DOMAIN

License

MIT © CrawlSphere