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

@canvas-digital/blog-sdk

v2.3.7

Published

Unified Blog SDK — user (published blogs) and admin (CRUD) in one package.

Readme

@canvas-digital/blog-sdk

Unified React SDK for the Blog Platform — public blog (mode="user") and admin CMS (mode="admin") in one package.

Install in your host app (Next.js, Vite, CRA, etc.), import the CSS once, and render <BlogPlatform /> with credentials. Every API call sends email + apiKey (+ optional domain) as request headers.

| Mode | Audience | Auth | What you get | |------|----------|------|----------------| | mode="user" | Site visitors | Public API key | Blog list, post detail, categories, TOC, Top Related Blogs, comments UI | | mode="admin" | Editors / admins | Admin API key or email + password login | Full CMS with toasts, confirm dialogs, button loaders, and live list refresh |

Not included: Super-admin / platform console. Domains and keys are issued from your Blog Platform backend — not from this npm package.


Install

npm install @canvas-digital/blog-sdk
# or
yarn add @canvas-digital/blog-sdk
# or
pnpm add @canvas-digital/blog-sdk

Peer dependencies: react and react-dom ≥ 18.
Bundled: react-toastify, @tanstack/react-query, axios, react-select.

Import styles once in your app entry (required for admin + public layouts):

import "@canvas-digital/blog-sdk/style.css";
import { BlogPlatform } from "@canvas-digital/blog-sdk";

Quick start

Public blog (user)

import "@canvas-digital/blog-sdk/style.css";
import { BlogPlatform } from "@canvas-digital/blog-sdk";

export default function BlogPage() {
  return (
    <BlogPlatform
      mode="user"
      email="[email protected]"
      apiKey="bp_public_..."
      domain="example.com"
      basePath="/blog"
      branding={{
        name: "My Blog",
        primaryColor: "#e01eae",
        secondaryColor: "#19042d",
      }}
    />
  );
}

Admin CMS (skip login — pass key directly)

<BlogPlatform
  mode="admin"
  email="[email protected]"
  apiKey="bp_sdk_..."
  branding={{ name: "Admin Blog" }}
/>

Admin CMS (email + password login)

"use client"; // Next.js App Router

import { useState } from "react";
import "@canvas-digital/blog-sdk/style.css";
import { BlogPlatform } from "@canvas-digital/blog-sdk";

export default function AdminBlogPage() {
  const [email, setEmail] = useState("");
  const [apiKey, setApiKey] = useState("");
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  return (
    <main className="app-full" style={{ minHeight: "100dvh" }}>
      <BlogPlatform
        mode="admin"
        email={email}
        apiKey={apiKey}
        isAuthenticated={isAuthenticated}
        branding={{ name: "Admin Blog" }}
        onLoginSuccess={(creds) => {
          setEmail(creds.email);
          setApiKey(creds.apiKey);
          setIsAuthenticated(true);
        }}
        onLogout={() => {
          setEmail("");
          setApiKey("");
          setIsAuthenticated(false);
        }}
      />
    </main>
  );
}

With isAuthenticated={false}, the SDK shows a login form. On success it calls POST {apiUrl}/admin/login, then opens the dashboard. Log out appears on the Profile page.

Host tip: give the admin wrapper a real height (min-height: 100dvh or a full-height layout). The admin shell fills its parent.


Props reference

Required / common

| Prop | Type | Default | Notes | |------|------|---------|--------| | mode | "user" \| "admin" | — | Chooses public blog vs admin CMS | | email | string | — | Required for user. Optional for admin when isAuthenticated={false} (login supplies it) | | apiKey | string | — | Public key (bp_public_…) for user; admin key (bp_sdk_…) for admin. Optional when using login form |

Optional

| Prop | Type | Default | Notes | |------|------|---------|--------| | domain | string | — | Sent as domain / X-Client-Domain headers | | apiUrl | string | from env / default | Override API base (usually omit; use env instead). Prefer HTTPS in production | | timeoutMs | number | 3600000 | Request timeout (1 hour) | | basePath | string | "/blog" | User mode. URL prefix for list/detail. Must match a host catch-all route | | branding | object | — | name, subtitle, logo, primaryColor, secondaryColor, className, css | | theme | object | — | User UI theme (layout, colors, fonts) | | adminTheme | object | — | Admin UI theme | | className | string | — | Wrapper class | | publicOrigin | string | — | Admin. Origin for Preview links (e.g. https://yoursite.com) | | isAuthenticated | boolean | true | Admin only. false → show login form | | onLoginSuccess | (creds) => void | — | Admin only. { email, apiKey } after login | | onLogout | () => void | — | Admin only. Fired when user logs out |


User mode (public blog)

What it does

  • Blog index (hero, trust band, post grid, load more)
  • Post detail (hero, prose with justified paragraphs, sticky TOC on laptop+, comments UI)
  • Top Related Blogs band above the host footer (3 list-style cards + View all)
  • Category filter via /blog/category/{slug}
  • Client-side navigation between list ↔ detail (no full page reload on card click)
  • Scroll-to-top on list ↔ detail navigation
  • TOC clicks scroll only — they do not append heading slugs to the URL

Detail page load order

  1. Post API → article + comments UI
  2. Related API → “Top Related Blogs” section (own loader until response)
  3. List-hero background loads in parallel and does not block related

Top Related Blogs

Full-width section matching list gutters and card style:

┌─────────────────────────────────────────────┐
│  Top Related Blogs              View all →  │
│  ┌──────┐  ┌──────┐  ┌──────┐               │
│  │ card │  │ card │  │ card │               │
│  └──────┘  └──────┘  └──────┘               │
└─────────────────────────────────────────────┘

API: GET /blogs/{blog_id}/related?limit=3
Fallback order on the backend: same category → same tags → random published posts.

Standalone component (optional):

import { RelatedBlogs } from "@canvas-digital/blog-sdk";

<RelatedBlogs
  config={{
    apiUrl: process.env.NEXT_PUBLIC_BLOG_API_URL!,
    publicApiKey: process.env.NEXT_PUBLIC_BLOG_PUBLIC_KEY!,
    email: process.env.NEXT_PUBLIC_BLOG_EMAIL!,
    domain: process.env.NEXT_PUBLIC_BLOG_DOMAIN,
  }}
  blog={{ id: postId, category_id: categoryId }}
  limit={3}
  title="Top Related Blogs"
  viewAllHref="/blog"
  getHref={(b) => `/blog/${b.slug}`}
/>

URL shape

With basePath="/blog" (default):

| Path | View | |------|------| | /blog | List | | /blog/{post-slug} | Post detail | | /blog/category/{category-slug} | Filtered list |

Critical: host catch-all route

The SDK updates the browser URL with the History API. Direct links and refresh only work if your host app serves the same page for every path under basePath.

Without a catch-all, https://yoursite.com/blog/my-post returns 404 even though /blog works.

Do not remount on pathname

// ❌ Causes “Maximum update depth exceeded” and blank related section
<BlogPlatform key={pathname} mode="user" ... />

// ✅ Correct — SDK owns list ↔ detail URL sync
<BlogPlatform mode="user" basePath="/blog" ... />

Admin mode (CMS)

Auth options

  1. Pass credentials — isAuthenticated defaults to true; provide email + admin apiKey.
  2. Login form — isAuthenticated={false}; admin signs in with email + password. SDK calls POST /admin/login and uses the returned apiKey for subsequent Admin SDK calls.

After login

  • Persist email + apiKey from onLoginSuccess in host state (and optionally sessionStorage / cookies).
  • Keep rendering the same <BlogPlatform mode="admin" /> instance so the in-SDK session is not unmounted.
  • On logout, clear host state in onLogout.
  • Refresh-safe routes: the admin view (posts, editor, SEO, heroes, etc.) is restored from the URL / sessionStorage after reload — you stay on the same screen instead of jumping back to the dashboard.

UX built into admin

| Feature | Behavior | |---------|----------| | Success / error toasts | react-toastify top-right toasts on create, update, and delete | | Confirm dialog | In-app modal (not window.confirm); confirm button shows a spinner until the API finishes | | Button loaders | Create / Save / Delete / bulk actions show a spinner and stay disabled until the mutation settles | | Live list refresh | After delete/create/update, tables refetch from the API (no stale 30s cache) |

Example toast usage if you build custom admin screens on top of the SDK:

import { adminToast, AdminToastHost } from "@canvas-digital/blog-sdk";

function MyAdminScreen() {
  return (
    <>
      <AdminToastHost />
      <button
        type="button"
        onClick={async () => {
          try {
            await deleteSomething();
            adminToast.success("Deleted successfully.");
          } catch (e) {
            adminToast.error(e instanceof Error ? e.message : "Delete failed.");
          }
        }}
      >
        Delete
      </button>
    </>
  );
}

<BlogPlatform mode="admin" /> already mounts AdminToastHost — you do not need to add it for the built-in CMS.

Headers (after auth)

email: [email protected]
apiKey: bp_sdk_...
domain: example.com

Admin surfaces

Posts (list, quick edit, trash, publish), categories / tags / authors, media library, comments, users, brand settings, blog list heroes, redirects, robots.txt, GA4 / Search Console, import / export tools.


API base URL

| Priority | Source | |----------|--------| | 1 | apiUrl prop on <BlogPlatform /> | | 2 | Host env BLOG_API_URL (also accepts VITE_BLOG_API_URL / NEXT_PUBLIC_BLOG_API_URL) | | 3 | Built-in default http://app.yourwebsitemockup.net:8000/api/v1 |

# Production — use HTTPS to avoid mixed-content blocks on HTTPS sites
BLOG_API_URL=https://api.yourdomain.com/api/v1

# Local backend
BLOG_API_URL=http://localhost:8000/api/v1

| Backend | URL | |---------|-----| | Default API | http://app.yourwebsitemockup.net:8000/api/v1 | | Local API | http://localhost:8000/api/v1 | | Swagger | http://localhost:8000/docs | | Health | http://localhost:8000/health | | Related posts | GET /api/v1/blogs/{id}/related?limit=3 |

Restart the host dev server after changing env files.

SSL / mixed content: if your site is served over https://, the API must also be https://. An http:// API URL will be blocked by the browser and public/admin calls will fail silently or with network errors.


Deploy by technology (complete examples)

1) Next.js App Router (recommended)

Project layout

app/
  layout.tsx
  blog/
    [[...slug]]/
      page.tsx          ← public blog catch-all
  admin-blog/
    page.tsx            ← admin CMS
.env.local
next.config.mjs

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  env: {
    // Expose to the client bundle for the SDK
    BLOG_API_URL: process.env.BLOG_API_URL,
  },
};

export default nextConfig;

.env.local

BLOG_API_URL=https://api.yourdomain.com/api/v1

# Prefer server-only vars; pass keys into the client page carefully.
# For a public blog page, public key is OK on the client.
[email protected]
NEXT_PUBLIC_BLOG_PUBLIC_KEY=bp_public_...
NEXT_PUBLIC_BLOG_DOMAIN=www.yoursite.com

Public blog — app/blog/[[...slug]]/page.tsx

"use client";

import "@canvas-digital/blog-sdk/style.css";
import { BlogPlatform } from "@canvas-digital/blog-sdk";

export default function BlogCatchAllPage() {
  return (
    <main className="app-full">
      <BlogPlatform
        mode="user"
        email={process.env.NEXT_PUBLIC_BLOG_EMAIL!}
        apiKey={process.env.NEXT_PUBLIC_BLOG_PUBLIC_KEY!}
        domain={process.env.NEXT_PUBLIC_BLOG_DOMAIN}
        basePath="/blog"
        branding={{
          name: "Book Publishing Services Blog",
          primaryColor: "#e01eae",
          secondaryColor: "#19042d",
        }}
      />
    </main>
  );
}

Do not wrap <BlogPlatform /> with key={pathname} (or remount on every usePathname() change). The SDK already syncs list ↔ detail from the URL; remounting on pathname fights that and can cause “Maximum update depth exceeded”, abort related-post fetches, and a blank related section.

The [[...slug]] segment is required so these all hit the same page:

  • /blog
  • /blog/my-post-slug
  • /blog/category/animation

Admin — app/admin-blog/page.tsx

"use client";

import { useEffect, useState } from "react";
import "@canvas-digital/blog-sdk/style.css";
import { BlogPlatform } from "@canvas-digital/blog-sdk";

const STORAGE_KEY = "bp-admin-session";

type Creds = { email: string; apiKey: string };

export default function AdminBlogPage() {
  const [email, setEmail] = useState("");
  const [apiKey, setApiKey] = useState("");
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    try {
      const raw = sessionStorage.getItem(STORAGE_KEY);
      if (raw) {
        const parsed = JSON.parse(raw) as Creds;
        if (parsed.email && parsed.apiKey) {
          setEmail(parsed.email);
          setApiKey(parsed.apiKey);
          setIsAuthenticated(true);
        }
      }
    } catch {
      /* ignore */
    }
    setReady(true);
  }, []);

  if (!ready) return null;

  return (
    <main style={{ minHeight: "100dvh" }}>
      <BlogPlatform
        mode="admin"
        email={email}
        apiKey={apiKey}
        isAuthenticated={isAuthenticated}
        publicOrigin={typeof window !== "undefined" ? window.location.origin : undefined}
        branding={{ name: "Admin Blog" }}
        onLoginSuccess={(creds) => {
          setEmail(creds.email);
          setApiKey(creds.apiKey);
          setIsAuthenticated(true);
          sessionStorage.setItem(
            STORAGE_KEY,
            JSON.stringify({ email: creds.email, apiKey: creds.apiKey })
          );
        }}
        onLogout={() => {
          setEmail("");
          setApiKey("");
          setIsAuthenticated(false);
          sessionStorage.removeItem(STORAGE_KEY);
        }}
      />
    </main>
  );
}

Deploy (Vercel)

# In the Next.js app repo
npm install @canvas-digital/blog-sdk
vercel env add BLOG_API_URL
vercel env add NEXT_PUBLIC_BLOG_EMAIL
vercel env add NEXT_PUBLIC_BLOG_PUBLIC_KEY
vercel env add NEXT_PUBLIC_BLOG_DOMAIN
vercel --prod

Or connect the Git repo in the Vercel dashboard and set the same env vars under Project → Settings → Environment Variables.


2) Next.js Pages Router

pages/
  blog/[[...slug]].tsx
  admin-blog.tsx

pages/blog/[[...slug]].tsx

import dynamic from "next/dynamic";
import "@canvas-digital/blog-sdk/style.css";

const BlogPlatform = dynamic(
  () => import("@canvas-digital/blog-sdk").then((m) => m.BlogPlatform),
  { ssr: false }
);

export default function BlogPage() {
  return (
    <BlogPlatform
      mode="user"
      email={process.env.NEXT_PUBLIC_BLOG_EMAIL!}
      apiKey={process.env.NEXT_PUBLIC_BLOG_PUBLIC_KEY!}
      domain={process.env.NEXT_PUBLIC_BLOG_DOMAIN}
      basePath="/blog"
    />
  );
}

pages/admin-blog.tsx

Same client logic as the App Router admin example (state + isAuthenticated={false} + onLoginSuccess / onLogout).

next.config.js should still map BLOG_API_URL via env: { BLOG_API_URL: process.env.BLOG_API_URL }.


3) Vite + React

vite.config.ts

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  envPrefix: ["VITE_", "BLOG_"], // so BLOG_API_URL is exposed to import.meta.env
});

.env / .env.production

BLOG_API_URL=https://api.yourdomain.com/api/v1
[email protected]
VITE_BLOG_PUBLIC_KEY=bp_public_...
VITE_BLOG_DOMAIN=www.yoursite.com

Router (React Router v6) — catch-all for public blog

// main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "@canvas-digital/blog-sdk/style.css";
import { BlogPage } from "./BlogPage";
import { AdminBlogPage } from "./AdminBlogPage";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/blog/*" element={<BlogPage />} />
        <Route path="/admin-blog" element={<AdminBlogPage />} />
      </Routes>
    </BrowserRouter>
  </React.StrictMode>
);
// BlogPage.tsx
import { BlogPlatform } from "@canvas-digital/blog-sdk";

export function BlogPage() {
  return (
    <BlogPlatform
      mode="user"
      email={import.meta.env.VITE_BLOG_EMAIL}
      apiKey={import.meta.env.VITE_BLOG_PUBLIC_KEY}
      domain={import.meta.env.VITE_BLOG_DOMAIN}
      basePath="/blog"
    />
  );
}
// AdminBlogPage.tsx — same login-state pattern as Next admin example
import { useState } from "react";
import { BlogPlatform } from "@canvas-digital/blog-sdk";

export function AdminBlogPage() {
  const [email, setEmail] = useState("");
  const [apiKey, setApiKey] = useState("");
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  return (
    <main style={{ minHeight: "100dvh" }}>
      <BlogPlatform
        mode="admin"
        email={email}
        apiKey={apiKey}
        isAuthenticated={isAuthenticated}
        onLoginSuccess={(c) => {
          setEmail(c.email);
          setApiKey(c.apiKey);
          setIsAuthenticated(true);
        }}
        onLogout={() => {
          setEmail("");
          setApiKey("");
          setIsAuthenticated(false);
        }}
      />
    </main>
  );
}

Deploy (static / Netlify / Cloudflare Pages)

npm run build
# Upload dist/ to your host
# Set BLOG_API_URL / VITE_* vars in the host dashboard for production builds

Netlify public/_redirects (SPA fallback so /blog/my-post does not 404):

/blog/*    /index.html   200
/admin-blog /index.html  200
/*         /index.html   200

Cloudflare Pages / Vite: set “not found” behavior to serve index.html for SPA routes, or add a _routes / redirect rule equivalent to the above.


4) Create React App

CRA exposes only REACT_APP_* env vars. Pass the API URL with the prop (or rename and map it):

<BlogPlatform
  mode="user"
  apiUrl={process.env.REACT_APP_BLOG_API_URL}
  email={process.env.REACT_APP_BLOG_EMAIL}
  apiKey={process.env.REACT_APP_BLOG_PUBLIC_KEY}
  domain={process.env.REACT_APP_BLOG_DOMAIN}
  basePath="/blog"
/>
# .env
REACT_APP_BLOG_API_URL=https://api.yourdomain.com/api/v1
[email protected]
REACT_APP_BLOG_PUBLIC_KEY=bp_public_...
REACT_APP_BLOG_DOMAIN=www.yoursite.com

Use React Router path="/blog/*" the same way as Vite. For production on Nginx:

location /blog {
  try_files $uri /index.html;
}
location /admin-blog {
  try_files $uri /index.html;
}

Checklist before go-live

User blog

  • [ ] mode="user" with public email + apiKey
  • [ ] basePath matches your real URL (default /blog)
  • [ ] Catch-all route exists ([[...slug]] or /blog/*)
  • [ ] No key={pathname} on <BlogPlatform />
  • [ ] SPA host rewrite serves index.html for deep links (Vite/CRA)
  • [ ] BLOG_API_URL is HTTPS when the site is HTTPS
  • [ ] Backend has GET /blogs/{id}/related available
  • [ ] @canvas-digital/blog-sdk/style.css imported once

Admin

  • [ ] mode="admin"
  • [ ] Either pass admin email + apiKey, or isAuthenticated={false} + login handlers
  • [ ] onLoginSuccess stores { email, apiKey } in host state
  • [ ] Admin page container has height (min-height: 100dvh)
  • [ ] Do not put admin API keys in a public marketing page
  • [ ] Confirm create / update / delete shows toasts + button loaders after linking the latest SDK

Package exports

import {
  BlogPlatform,
  BlogList,
  BlogDetail,
  RelatedBlogs,
  BlogAdmin,
  AdminLoginForm,
  adminToast,
  AdminToastHost,
  useConfirmDialog,
  BlogClient,
  AdminSdkClient,
  loginAdmin,
  resolveBlogApiUrl,
} from "@canvas-digital/blog-sdk";

// or: import { BlogClient } from "@canvas-digital/blog-sdk/client";

import "@canvas-digital/blog-sdk/style.css";

Develop this package

git pull
cp .env.example .env   # playground only — not published
npm install
npm test
npm run typecheck
npm run build
npm run dev            # local playground

Link into a host app locally:

# in blog-platform-packages
npm run build
npm pack
# then in the host Next/Vite app
npm install ../path/to/canvas-digital-blog-sdk-2.3.6.tgz

Publish to npm

# Bump version in package.json first (npm rejects duplicate versions)
npm version patch   # or minor / major

npm config set //registry.npmjs.org/:_authToken=YOUR_TOKEN
npm whoami
npm publish --access public

Then in each host app:

npm install @canvas-digital/blog-sdk@latest

Troubleshooting

| Symptom | Fix | |---------|-----| | /blog/my-post → 404 | Add catch-all route + SPA rewrite; set basePath to match | | Maximum update depth exceeded | Remove key={pathname} from <BlogPlatform />; upgrade SDK | | Related blogs empty / never load | Same as above; ensure GET /blogs/{id}/related exists; hard-refresh after SDK rebuild | | Admin blank after login | Persist creds in onLoginSuccess; give wrapper min-height: 100dvh; upgrade SDK | | Admin refresh jumps to dashboard | Upgrade SDK (route persist via URL + sessionStorage) | | Delete succeeds but list unchanged | Upgrade SDK (force refetch after mutations) | | No toast on save/delete | Upgrade SDK; import style.css; confirm react-toastify is installed with the package | | Mixed content / API blocked | Use https:// for BLOG_API_URL on HTTPS sites | | Double TOC | Upgrade SDK (CSS fix); ensure style.css is imported | | API 401 | Wrong key type (public vs admin) or missing email / apiKey headers | | Env ignored | Restart Vite/Next; map BLOG_API_URL in vite.config / next.config |


License

MIT