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

@sahan-sac/blog-kit

v0.3.1

Published

Headless AI blog toolkit on @sahan-sac/ai-core: full-post generation with a JSON schema and repair pass, SEO suggestions, draft and cover helpers, AI image generation through an ImageSink port, plus pure blog utilities (slugs, Markdown and HTML conversion

Readme

@sahan-sac/blog-kit

Headless AI blog toolkit built on @sahan-sac/ai-core. It covers:

  • full-post generation, with a JSON response schema, a repair pass and a 42 s text budget;
  • SEO suggestions;
  • the draft, cover and full-post helpers;
  • AI images through an ImageSink port;
  • pure blog utilities.

It is headless and framework-agnostic: no Prisma, no Next.js APIs, no React, and no storage service. The host app keeps:

  • the routes, with auth, availability, origin checks and rate limits first;
  • the post schema and queries;
  • rendering with its HTML sanitizer;
  • revisions storage, the editor UI and where images are kept.

Subpaths

| Import | What | | --- | --- | | @sahan-sac/blog-kit/deps | realBlogDeps(env, site?) (blog provider chain + shared health + site profile), BlogAiDeps, AiHelperFailure | | @sahan-sac/blog-kit/generate | generateBlogPost, generateSeoSuggestion, blogGenerationSchema, BLOG_RESPONSE_SCHEMA, TEXT_BUDGET_MS, JSON repair helpers | | @sahan-sac/blog-kit/helpers | draftPost, suggestCover, generateFullPost | | @sahan-sac/blog-kit/prompts | Full-post, repair and SEO prompt builders (caller input fenced as data) | | @sahan-sac/blog-kit/helper-prompts | Draft, cover and full-post helper prompts | | @sahan-sac/blog-kit/images | generateBlogImage(prompt, options, { config, sink }), ImageSink | | @sahan-sac/blog-kit/ai-image-tokens | Inline image placeholders in generated Markdown (applyImageToken, removeImageToken) and in the editor's HTML (applyImageTokenToHtml) | | @sahan-sac/blog-kit/markdown | markdownToHtml, htmlToMarkdown (charts and heading ids included) | | @sahan-sac/blog-kit/chart | Chart code blocks to accessible SVG figures | | @sahan-sac/blog-kit/slug | slugify, isValidSlug, ensureUniqueSlug | | @sahan-sac/blog-kit/readtime | computeReadMinutes | | @sahan-sac/blog-kit/revisions | Post snapshot schema and comparison, REVISIONS_KEPT | | @sahan-sac/blog-kit/draft | Browser draft autosave keys (draftStorageKey(id, prefix = "admin")) and freshness check | | @sahan-sac/blog-kit/concurrency | parseSubmittedUpdatedAt, conflict message for optimistic concurrency |

generate and helpers import server-only, and deps and images reach the provider code. The utilities (markdown, chart, slug, readtime, revisions, draft, concurrency, ai-image-tokens) are browser-safe.

Environment

Uses @sahan-sac/ai-core's variables; see its README. Offer the blog AI only when blogAiEnabled(env) is true: ENABLE_BLOG_AI=true, which is off by default, and a provider that can answer for the blog (its key set, in AI_PROVIDER_ORDER_BLOG or AI_PROVIDER_ORDER when one is set, and AI_ALLOW_PAID=true for a paid one). Every generator runs the blog chain, so a failing provider hands over to the next. Offer images only when blogAiImagesEnabled(env) is true, which also needs an image provider. Both functions are in @sahan-sac/ai-core/availability.

Wiring

import { imageConfigFromEnv } from "@sahan-sac/ai-core/image";
import { realBlogDeps } from "@sahan-sac/blog-kit/deps";
import { generateBlogPost } from "@sahan-sac/blog-kit/generate";
import { generateBlogImage } from "@sahan-sac/blog-kit/images";

// Every prompt describes the site and may only link to these paths. All optional:
// the defaults are "this site", ["/"] and no call-to-action path.
// `guidance` is the owner's standing guidance, added after the fixed rules of every prompt.
const site = { description: "a bakery's site", internalLinks: ["/", "/menu", "/contact"], callToAction: ["/contact"], guidance: "Warm, plain English. Never quote prices." };

// `input.instructions` (2,000 chars) and `input.resources` (pasted reference text, 12,000) are optional and
// fenced as untrusted data: resources are facts, never instructions, and no link is fetched.
// `signal` stops the model calls (and skips the repair) when the client goes away.
const result = await generateBlogPost(input, realBlogDeps(env, site), { onStatus: (status) => send("provider_status", status), signal: request.signal });
if (!result.ok) return send("error", { error: result.error });

const config = imageConfigFromEnv(env);
if (config) {
  const image = await generateBlogImage(
    result.post.featuredImage.prompt,
    { alt: result.post.featuredImage.alt, folder: "blog/ai", aspectRatio: "16:9", signal },
    { config, sink: mediaLibrarySink(actor) }
  );
}

ImageSink is where generated images go. A media-library sink uploads to your storage, records the asset and audits it:

import type { ImageSink } from "@sahan-sac/blog-kit/images";

export function mediaLibrarySink(actor: Actor): ImageSink {
  return {
    async storeGenerated(image, meta) {
      const saved = await registerGeneratedImage({ ...image, ...meta }, actor); // upload + DB row + audit, in your app
      return saved.ok ? { ok: true, mediaId: saved.asset.id, url: saved.asset.url } : { ok: false, error: saved.error };
    },
  };
}

When the sink fails, generateBlogImage returns { ok: false, stage: "store", image } with the generated image, so the editor can still show it and offer a download or retry. Image calls are billed. A failed generation returns stage: "generate" and never calls the sink.

Tests pass fake providers ({ providers: [{ name, generate }] }) and a fake generate for images, so nothing touches the network.