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

@credlyst/blog

v0.1.2

Published

Drop-in React blog components that render content published from Credlyst. Works on Lovable's TanStack Start and legacy Vite stacks.

Readme

@credlyst/blog

React components that render a full blog from content published in Credlyst. Built for Lovable sites; works in any React app.

Content is authored, published, scheduled and retired in Credlyst. This package only reads. There is no write path, no API key, and nothing to configure beyond a site key.


How it works, in one paragraph

Publishing in Credlyst writes JSON snapshots to a public CDN — an index of teasers, one file per post, and a taxonomy. These components fetch those files and render them, caching in memory and localStorage so the CDN is touched about once per content change rather than once per page view. There is no server, no database and no runtime dependency beyond React.

Credlyst publish ──> JSON snapshots on a CDN ──> @credlyst/blog ──> your pages

Install

bun add @credlyst/blog

npm install @credlyst/blog and pnpm add @credlyst/blog work too. React 18 or newer is a peer dependency.

Configure

Copy the config block from the Credlyst channel setup page. It looks like this:

// credlyst.config.ts
import type { CredlystConfig } from '@credlyst/blog'

export const credlystConfig: CredlystConfig = {
  siteKey: '00000000-0000-0000-0000-000000000000',
  baseUrl: 'https://xyz.supabase.co/storage/v1/object/public/lovable-snapshots/sites/00000000-0000-0000-0000-000000000000',
  pageSize: 10,
  categoriesEnabled: true,
}

Add basePath if the blog does not live at /blog:

basePath: '/insights',

None of this is secret. The snapshots are published blog posts on a public CDN. Do not put these values in environment variables — it makes the config harder to find without making anything safer.


Which stack am I on?

Check whether the project has a src/routes/ directory with createFileRoute.

| | TanStack Start (projects created on or after 13 May 2026) | Legacy Vite SPA | |---|---|---| | Fetch in | route loader | the component | | Crawler sees | fully rendered HTML | Lovable's prerender | | Use | loader + initialData | components alone |

Use the route loader when the project has one. It is the difference between a crawler receiving the article text and receiving an empty shell.


TanStack Start

Fetch in the loader and pass the result as initialData. The component then renders synchronously with no client fetch and no loading flash.

Index route — src/routes/blog/index.tsx

import { createFileRoute } from '@tanstack/react-router'
import { BlogIndex, fetchCredlystIndex } from '@credlyst/blog'
import { credlystConfig } from '../../credlyst.config'

export const Route = createFileRoute('/blog/')({
  loader: () => fetchCredlystIndex(credlystConfig, 1),
  component: BlogIndexPage,
})

function BlogIndexPage() {
  const data = Route.useLoaderData()
  return <BlogIndex config={credlystConfig} initialData={data} />
}

Post route — src/routes/blog/$slug.tsx

import { createFileRoute } from '@tanstack/react-router'
import { BlogPost, fetchCredlystPost } from '@credlyst/blog'
import { credlystConfig } from '../../credlyst.config'

export const Route = createFileRoute('/blog/$slug')({
  loader: ({ params }) => fetchCredlystPost(credlystConfig, params.slug),
  component: BlogPostPage,
})

function BlogPostPage() {
  const { slug } = Route.useParams()
  const data = Route.useLoaderData()
  return <BlogPost config={credlystConfig} slug={slug} initialData={data} />
}

Paginated route — src/routes/blog/page/$page.tsx

import { createFileRoute } from '@tanstack/react-router'
import { BlogIndex, fetchCredlystIndex } from '@credlyst/blog'
import { credlystConfig } from '../../../credlyst.config'

export const Route = createFileRoute('/blog/page/$page')({
  loader: ({ params }) => fetchCredlystIndex(credlystConfig, Number(params.page)),
  component: () => {
    const { page } = Route.useParams()
    return (
      <BlogIndex config={credlystConfig} page={Number(page)} initialData={Route.useLoaderData()} />
    )
  },
})

Legacy Vite SPA

No loaders. Render the components and let them fetch.

import { Routes, Route } from 'react-router-dom'
import { BlogIndex, BlogPost } from '@credlyst/blog'
import { credlystConfig } from './credlyst.config'
import { useParams } from 'react-router-dom'

function PostPage() {
  const { slug } = useParams()
  return <BlogPost config={credlystConfig} slug={slug} />
}

export function AppRoutes() {
  return (
    <Routes>
      <Route path="/blog" element={<BlogIndex config={credlystConfig} />} />
      <Route path="/blog/:slug" element={<PostPage />} />
    </Routes>
  )
}

Lovable prerenders pages for verified crawlers, so indexing still works. Humans get the SPA shell first and the content a moment later.


Home-page strip

import { BlogTeaserGrid } from '@credlyst/blog'
import { credlystConfig } from './credlyst.config'

<BlogTeaserGrid
  config={credlystConfig}
  limit={3}
  heading={<h2 className="mb-6 text-2xl font-semibold">Latest writing</h2>}
/>

It renders nothing when there are no posts and when the fetch fails. That is deliberate — an empty band with apologetic copy on a home page reads as broken.


Client-side navigation

By default links are plain <a> tags, which reload the page. Pass your router's link component to get client-side navigation — which is also what makes the in-memory cache worth having:

import { Link } from '@tanstack/react-router'

<BlogIndex
  config={credlystConfig}
  linkComponent={({ href, className, children }) => (
    <Link to={href} className={className}>{children}</Link>
  )}
/>

Restyling

Every element takes its classes from one named slot. Override any subset; an override replaces the default for that slot rather than merging with it.

<BlogIndex
  config={credlystConfig}
  theme={{
    cardTitle: 'text-2xl font-bold tracking-tight text-brand-900',
    card: 'rounded-2xl border border-brand-100 p-6 shadow-sm',
    postBody: 'prose prose-lg prose-brand max-w-none',
  }}
/>

To restyle the whole blog at once, wrap it:

import { CredlystThemeProvider } from '@credlyst/blog'

<CredlystThemeProvider value={{ cardTitle: 'text-2xl font-bold' }}>
  {children}
</CredlystThemeProvider>

Slot names: root, list, card, cardMedia, cardBody, cardTitle, cardExcerpt, cardMeta, tagList, tag, grid, pagination, paginationLink, paginationCurrent, filterBar, filterButton, filterButtonActive, searchInput, post, postHeader, postTitle, postMeta, postHero, postBody, emptyState, errorState, goneState, skeleton.

The defaults are neutral greys on purpose, so they do not fight whatever the site already looks like. The article body uses prose classes — install @tailwindcss/typography for it to have any effect, or override postBody with your own.

Wording

<BlogPost
  config={credlystConfig}
  slug={slug}
  copy={{
    gone: 'This case study has been withdrawn',
    goneDetail: 'Get in touch if you were looking for it.',
    error: 'We could not load this right now.',
  }}
/>

Behaviour worth knowing before you debug something

A retracted post is not an error. When a post is unpublished in Credlyst, its file is replaced with a marker and BlogPost renders a "no longer available" state. Old links keep working and say something useful. Use isGone() if you handle the data yourself.

A disconnected channel empties the index, not the posts. BlogIndex renders the empty state; every post URL still resolves. Reconnecting in Credlyst restores the index on the next publish.

Search and filtering work over the loaded page. The snapshots are static files with no query endpoint, so BlogIndex filters what it has. Pass searchScope="all" to fetch every page once and search the lot — good for a few dozen posts, wasteful for hundreds.

Pagination hides while a filter is active, because the page numbers describe the unfiltered set.

A failed refresh keeps the old content on screen. A blog that has worked for a week does not go blank because one request timed out; the stale copy stays and the error is not shown.

The headline is not rendered twice. Credlyst article bodies usually open with an <h1> that repeats the title, so it is dropped. Pass dedupeLeadingHeading={false} if your bodies genuinely start with a different heading.

Article HTML is inserted directly. It was sanitised at the origin, at publish time, by Credlyst's single renderer using an allowlist, and written to a bucket only Credlyst can write to. This package deliberately ships no sanitiser of its own — a second, different policy would mean the published article could differ from the one the author previewed.


SEO

Title, meta description, Open Graph, Twitter card, article JSON-LD and a canonical link. Build the descriptor once; feed it to whichever mechanism your stack has.

The canonical comes from the snapshot's canonical_url, which Credlyst resolved from the workspace's setting — the Credlyst-hosted page, or this site. Do not compute your own. The whole point of that setting is that the API, the snapshots and these tags agree.

TanStack Start — head()

import { createFileRoute } from '@tanstack/react-router'
import { BlogPost, buildPostHead, fetchCredlystPost, isGone, toTanStackHead } from '@credlyst/blog'
import { credlystConfig } from '../../credlyst.config'

const seo = { siteUrl: 'https://acme.com', siteName: 'Acme', twitterHandle: '@acme' }

export const Route = createFileRoute('/blog/$slug')({
  loader: ({ params }) => fetchCredlystPost(credlystConfig, params.slug),
  head: ({ loaderData }) =>
    isGone(loaderData)
      ? { meta: [{ title: 'No longer available' }] }
      : toTanStackHead(buildPostHead(loaderData.post, credlystConfig, seo)),
  component: () => (
    <BlogPost
      config={credlystConfig}
      slug={Route.useParams().slug}
      initialData={Route.useLoaderData()}
    />
  ),
})

The tags are rendered server-side, so they are in the first response — which is what makes them worth having.

Legacy Vite SPA — useCredlystHead

import { BlogPost, buildPostHead, isGone, useCredlystHead, useCredlystPost } from '@credlyst/blog'
import { credlystConfig } from './credlyst.config'
import { useParams } from 'react-router-dom'

const seo = { siteUrl: 'https://acme.com', siteName: 'Acme' }

export function PostPage() {
  const { slug } = useParams()
  const { data } = useCredlystPost(credlystConfig, slug)

  useCredlystHead(
    data && !isGone(data) ? buildPostHead(data.post, credlystConfig, seo) : null,
  )

  return <BlogPost config={credlystConfig} slug={slug} initialData={data} />
}

It writes into document.head during layout — before paint, and before Lovable's crawler prerender snapshots the DOM. It removes the previous page's tags on navigation, so an SPA does not accumulate two canonicals. It touches nothing it did not write, and while loading it leaves whatever is there in place: wrong tags beat no tags, because one is a mis-titled search result and the other is an untitled one.

Index pages

head: ({ loaderData }) => toTanStackHead(buildIndexHead(loaderData, credlystConfig, {
  ...seo,
  title: 'Insights',
  description: 'Writing on publishing infrastructure.',
})),

Page 2 onwards gets — page 2 in the title, so a paginated blog does not read as duplicate content, plus rel="prev"/rel="next".

Sitemap

Credlyst writes sitemap-blog.xml alongside the snapshots. Get its URL with sitemapUrl(config) and either link it from your robots.txt or list it in a sitemap index:

Sitemap: https://xyz.supabase.co/storage/v1/object/public/lovable-snapshots/sites/YOUR_SITE_KEY/sitemap-blog.xml

It lists post URLs on your site, which requires the channel's site_url to be set in Credlyst (or the workspace canonical source set to external). Without either, Credlyst has no way to know where your posts live and the sitemap comes back empty.


Caching

Two layers, plus the browser's.

| Layer | Survives | Purpose | |---|---|---| | Memory | client-side navigation | instant repeat renders | | localStorage | reload, new tab | no loading state on a page you have seen | | Browser HTTP cache | per its own rules | revalidation, If-None-Match, 304s |

Cached content renders immediately even when stale, and a refresh runs in the background. Default freshness is 5 minutes; change it with ttlMs.

Revalidation is left to the browser deliberately. Supabase Storage does not send Access-Control-Expose-Headers, so a cross-origin response's ETag is unreadable from JavaScript — response.headers.get('etag') returns null on a customer's domain. The browser can read it and does, sending If-None-Match and getting real 304s. If you are changing src/cache.ts: never pass cache: 'no-store', and never branch on a read ETag.

clearCredlystCache() drops everything, if you need it.


API

Components

| | | |---|---| | BlogIndex | paginated list with filter and search | | BlogPost | one article | | BlogTeaserGrid | latest-N strip | | BlogCard | one teaser, for your own layouts |

Server-safe functions

Call from a route loader. No React, no browser APIs.

fetchCredlystIndex(config, page?) · fetchCredlystPost(config, slug) · fetchCredlystTaxonomy(config)

Hooks

useCredlystIndex(config, { page, initialData }) · useCredlystPost(config, slug, { initialData }) · useCredlystTaxonomy(config, { initialData })

Each returns { data, status, error, isValidating, refresh }.

Helpers

isGone(result) · postHref(config, teaser) · pageHref(config, page) · sitemapUrl(config) · clearCredlystCache() · formatDate(iso, locale)


Troubleshooting

Nothing renders and the network tab shows a 400. Storage answers 400, not 404, for a file that is not there. Check baseUrl against the Credlyst setup page, and confirm something has been published to the channel.

The index is empty but posts exist. The channel was disconnected in Credlyst, or nothing has been published to this channel — publishing to LinkedIn does not populate a Lovable blog.

Pagination skips posts. pageSize here must match the channel's page size in Credlyst. It is in the config block; do not change one without the other.

Content is stale. Default freshness is 5 minutes and the CDN adds up to a minute. Drop ttlMs to see changes sooner, at the cost of more requests.

Styles look unstyled. Tailwind only emits classes it can find. If the components live outside your content globs, add the package:

content: ['./src/**/*.{ts,tsx}', './node_modules/@credlyst/blog/dist/**/*.js']

The article body has no typography. Install @tailwindcss/typography, or override the postBody slot.


Licence

MIT