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

hazo_blog

v0.3.3

Published

SEO-optimized blogging package: posts, categories, tags, MDX content, and GA4/GSC/Bing-ready SEO.

Readme

hazo_blog

SEO-first blogging for Next.js App Router apps: posts, categories, tags, MDX content, and GA4 / Google Search Console / Bing-ready output (BlogPosting + BreadcrumbList + FAQPage JSON-LD, canonical, OG, sitemap, RSS). The package owns the page layout and all SEO; the host customizes via Tailwind theme tokens and a single BlogConfig.

Install

npm install hazo_blog
# peers:
npm install hazo_core hazo_connect hazo_api hazo_files hazo_ui react react-dom next
# optional peers: hazo_images (AI images), hazo_jobs (scheduled publish), hazo_auth

Requires hazo_ui >= 3.2.0 (provides the MarkdownEditor used by the admin form).

1. Database

Apply the schema for your engine (also runnable through your migration runner):

  • db_setup_sqlite.sql — SQLite
  • db_setup_postgres.sql — PostgreSQL

Tables: hazo_blog_posts, hazo_blog_categories, hazo_blog_tags, hazo_blog_post_tags (all carry a nullable scope_id for optional multi-tenancy).

2. Define your BlogConfig

// lib/blog-config.ts
import type { BlogConfig } from "hazo_blog";

export const blogConfig: BlogConfig = {
  siteName: "Acme",
  baseUrl: "https://acme.com",
  basePath: "/blog",                        // default "/blog"
  logoUrl: "https://acme.com/logo.png",
  defaultOgImage: "https://acme.com/og.png",
  author: { name: "Acme Team", url: "https://acme.com/about" },
  getHazoConnect: () => myHazoConnectAdapter,    // SQLite or Postgres
  authorize: (req) => checkAdminPermission(req),  // wire to hazo_auth
  // resolveScope: (req) => orgIdFrom(req),       // multi-tenant only
  // getAuthor: (id) => lookupAuthor(id),         // per-post authors
  // onAnalyticsEvent: (name, params) => gtag("event", name, params),
  revalidateSeconds: 3600,
  // Admin URL overrides (omit to use defaults below):
  // adminBasePath: "/admin/blog",          // where admin UI pages are mounted
  // adminApiBasePath: "/api/admin/blog",   // where admin API routes are mounted
  // searchApiPath: "/api/blog/search",     // where search API route is mounted
};

3. Mount the sealed pages

The package owns the page implementations; you write tiny files that pass config. revalidate and dynamicParams must be static literals (a Next requirement), and default can't be destructured (reserved word):

// app/blog/page.tsx
import { createBlogIndexPage } from "hazo_blog/next";
import { blogConfig } from "@/lib/blog-config";
const page = createBlogIndexPage(blogConfig);
export default page.default;
export const generateMetadata = page.generateMetadata;
export const revalidate = 3600;
// app/blog/[slug]/page.tsx
import { createBlogPostPage } from "hazo_blog/next";
import { blogConfig } from "@/lib/blog-config";
const page = createBlogPostPage(blogConfig);
export default page.default;
export const generateMetadata = page.generateMetadata;
export const generateStaticParams = page.generateStaticParams;
export const revalidate = 3600;
export const dynamicParams = true;
// app/blog/tag/[tag]/page.tsx
import { createBlogTagPage } from "hazo_blog/next";
import { blogConfig } from "@/lib/blog-config";
const page = createBlogTagPage(blogConfig);
export default page.default;
export const generateMetadata = page.generateMetadata;
export const revalidate = 3600;

4. Mount the API routes

Route-handler factories are exported from the main hazo_blog entry (they are React-free, so they don't pull the React page components into your API bundles):

// app/api/blog/search/route.ts
import { createBlogSearchRoute } from "hazo_blog";
import { blogConfig } from "@/lib/blog-config";
export const GET = createBlogSearchRoute(blogConfig);

// app/api/blog/feed/route.ts  → RSS
export const GET = createBlogFeedRoute(blogConfig);

// app/api/admin/blog/route.ts (cookie/session, guarded by authorize)
const admin = createBlogAdminRoutes(blogConfig);
export const GET = admin.collection.GET;
export const POST = admin.collection.POST;

// app/api/admin/blog/[id]/route.ts
export const PUT = admin.item.PUT;
export const DELETE = admin.item.DELETE;

// app/api/blog/manage/route.ts (programmatic, hazo_api keys; scopes blog:read / blog:write)
const manage = createBlogManageRoutes(blogConfig);
export const { GET, POST, PATCH } = manage;

5. Sitemap & robots

sitemap.xml / robots.txt are site-level singletons — merge the blog's contributions into your root files:

// app/sitemap.ts
import { createBlogService, getBlogSitemapEntries, resolveConfig } from "hazo_blog";
import { blogConfig } from "@/lib/blog-config";

export default async function sitemap() {
  const service = createBlogService(blogConfig);
  const { posts } = await service.listPosts({ perPage: 1000 });
  return [/* ...your routes */, ...getBlogSitemapEntries(resolveConfig(blogConfig), posts)];
}

6. Admin pages

Option A — Sealed admin pages (recommended)

Drop in the three sealed admin page factories. They render a full admin UI (post list with status/publish-date badges + edit links, create form, edit form) and respect your authorize(req) gate via the API routes.

// app/admin/blog/page.tsx  — post list
import { createBlogAdminListPage } from "hazo_blog/next";
import { blogConfig } from "@/lib/blog-config";
const page = createBlogAdminListPage(blogConfig);
export default page.default;
export const dynamic = "force-dynamic"; // must be a static literal

// app/admin/blog/new/page.tsx  — create form
import { createBlogAdminNewPage } from "hazo_blog/next";
const page = createBlogAdminNewPage(blogConfig);
export default page.default;
export const dynamic = "force-dynamic";

// app/admin/blog/[slug]/edit/page.tsx  — edit form
import { createBlogAdminEditPage } from "hazo_blog/next";
const page = createBlogAdminEditPage(blogConfig);
export default page.default;
export const dynamic = "force-dynamic";

If your admin API or UI routes live at non-default paths, override in BlogConfig:

adminBasePath: "/admin/blog",        // default — where factory pages link to
adminApiBasePath: "/api/admin/blog", // default — where PostForm POSTs to

Option B — Custom admin form

Use PostForm directly for a fully custom admin UI:

"use client";
import { PostForm } from "hazo_blog/client";
// pass categories + an endpoint; wire onImageUpload to hazo_files for paste-upload
<PostForm categories={categories} endpoint="/api/admin/blog" onSaved={...} />
// For edit: <PostForm post={existingPost} categories={categories} ... />

Required Next.js config

// next.config.js
const nextConfig = {
  // Transpile next-mdx-remote so Turbopack dedupes a single React copy
  // (otherwise MDX prerender throws "React Element from an older version").
  transpilePackages: ["hazo_blog", "hazo_ui", "next-mdx-remote"],
  // Only if you use hazo_connect's SQLite (better-sqlite3) adapter:
  serverExternalPackages: ["better-sqlite3"],
};
  • hazo_connect SQLite uses sql.js (WASM) by default — copy node_modules/sql.js/dist/sql-wasm.wasm to your app's public/sql-wasm.wasm.
  • Tailwind v4: add @source "../node_modules/hazo_blog/dist"; to your CSS so the package's classes are compiled. The admin editor's styles load automatically via hazo_ui's MarkdownEditor.

Build resilience

BlogContent (≥ 0.3.0) is an async server component that catches MDX compilation errors with try/catch, so a single malformed post renders a graceful fallback instead of failing the whole next build. It also pre-sanitizes content with sanitizeMdx before passing it to the compiler. The sanitizer fixes the common ways raw WordPress HTML trips the MDX/acorn parser: converts HTML comments → MDX comments (and escapes stray <!), strips <script>/<style> blocks whose bodies break parsing, self-closes HTML void elements (<img>, <br>, …), and drops string-valued style="..." attributes (invalid JSX; the object form style={{...}} is left untouched). Fenced and inline code spans are left byte-for-byte untouched, and the transform is idempotent. The sanitizer is also available as a standalone export from hazo_blog/lib for custom rendering pipelines.

Exports

| Entry | Contents | |---|---| | hazo_blog | server: createBlogService, createBlogRepository, SEO builders, route-handler factories (React-free), types | | hazo_blog/client | client components: PostCard, PostHero, AuthorBio, FaqSection, TableOfContents, RelatedPosts, BlogSearch, PostForm, MDX components, trackBlogEvent | | hazo_blog/next | sealed page factories: public (createBlogIndexPage, createBlogPostPage, createBlogTagPage) + admin (createBlogAdminListPage, createBlogAdminNewPage, createBlogAdminEditPage) + BlogContent + MdxErrorBoundary | | hazo_blog/lib | pure text utilities: sanitizeMdx, mdxToPlainText, buildExcerpt, slugify, calculateReadingTime, extractToc | | hazo_blog/seo | buildBlogPostingJsonLd, buildBreadcrumbJsonLd, buildFaqJsonLd, getBlogSitemapEntries, getBlogRobotsRules, getBlogRssXml | | hazo_blog/config | BlogConfig types + resolveConfig |

Search

BlogSearch (rendered inside createBlogIndexPage) supports:

  • Dropdown: appears after 2+ characters with a 250 ms debounce
  • Enter key: navigates to ${basePath}?q={term} — the index page renders a filtered grid with a result count and Clear link
  • Escape: dismisses the dropdown

If your search API lives at a non-default path, set searchApiPath in BlogConfig:

searchApiPath: "/api/blog/search", // default

See SETUP_CHECKLIST.md for a step-by-step integration list. A runnable demo lives in test-app/.

License

MIT