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

@singi-labs/sifa-page-renderer

v0.2.34

Published

Pure HTML renderer for personal sites driven by Sifa profile data. No framework, no filesystem -- import and call.

Readme

@singi-labs/sifa-page-renderer

Pure HTML renderer for personal sites driven by Sifa profile data, styled after academicpages.github.io. No framework, no filesystem -- import and call.

What it does

Takes a Sifa profile object and returns complete standalone HTML pages. Body sections are built from the structured SDK Profile (via buildProfileSections, driven by the shared @singi-labs/sifa-sdk section model), so ordering matches the main Sifa profile page and detail is rich (proper dates, validated links, publication citations). Each page has:

  • Top masthead with horizontal navigation
  • Left sidebar with avatar, identity, and links
  • Main content area with prose styling
  • Sifa-branded footer
  • Dark mode toggle (persisted to localStorage)
  • Print-friendly layout (hides chrome)

Usage

Self-hosted static site

import { fetchProfile } from '@singi-labs/sifa-sdk/query/fetchers';
import { buildProfileSections, renderHome, renderSectionPage } from '@singi-labs/sifa-page-renderer';
import { CSS } from '@singi-labs/sifa-page-renderer/style';

// Fetch the structured profile from sifa.id
const profile = await fetchProfile({ baseUrl: 'https://sifa.id' }, 'your-handle.bsky.social');

// Build sections from the structured profile (About + Career + ... , Links excluded)
const sections = buildProfileSections(profile);

// Render pages
const indexHtml = renderHome(profile, sections, { year: 2026, updated: '2026-07-15' });
for (const section of sections) {
  if (section.slug === 'index') continue; // About is shown on the home page
  const html = renderSectionPage(profile, section, sections, { year: 2026 });
  // Write to dist/${section.slug}.html
}

See sifa-page for a complete self-hosting scaffold.

Server-rendered (Next.js, Fastify, etc.)

import { buildProfileSections, renderSinglePage } from '@singi-labs/sifa-page-renderer';
import { getCSS } from '@singi-labs/sifa-page-renderer/style';

// Override asset paths for your hosting setup. renderSinglePage serves all
// sections in one document with hash-based nav (#career, #education, ...).
const sections = buildProfileSections(profile);
const html = renderSinglePage(profile, sections, {
  paths: {
    css: '/api/style',
    assetDir: '/static/sifa',
    fontDir: '/fonts/sifa',
    favicon: '/static/sifa/favicon.svg',
  },
  og: {
    title: 'Jane Doe - Personal site',
    description: 'Jane Doe on Sifa ID',
    url: 'https://example.com/p/jane/site',
  },
});

API

buildProfileSections(profile): RenderedSection[]

Build every visible body section from a structured SDK Profile, in canonical order, each rendered to sanitized HTML: { id, slug, title, html }. The Links section is excluded (it renders in the sidebar). Always the public visitor view (owner-hidden items dropped).

renderHome(profile, sections, ctx?): string

Render the home/About page. sections is the buildProfileSections output. Returns a complete HTML document.

renderSectionPage(profile, section, sections, ctx?): string

Render a single section page (Career, Education, etc.). Returns a complete HTML document.

renderSinglePage(profile, sections, ctx?): string

Render all sections in one document with hash-based nav, for server-rendered single-route hosts (e.g. sifa-web). Returns a complete HTML document.

renderActivityStream(vms, options?): string

Render an activity stream to an HTML fragment (a <section>), from an array of StreamCardVM objects -- the same normalized view-model the sifa-web activity cards consume (produced by the SDK's toStreamCardVM). Both surfaces render the same VM, so page.sifa.id and sifa-web stay in lockstep without sharing a DOM.

import { renderActivityStream } from '@singi-labs/sifa-page-renderer';
import type { StreamCardVM } from '@singi-labs/sifa-sdk';

const html = renderActivityStream(vms, {
  // Optional. Build an image URL from a blob ref. Media may arrive already
  // resolved ({ url }) or as a raw blob ref ({ did, cid }); the VM stays
  // host-agnostic, so the host decides the CDN. Default: a Bluesky-style URL
  // `${cdnBase}/img/feed_fullsize/plain/{did}/{cid}@jpeg`.
  blobUrl: (did, cid) => `https://images.example/${did}/${cid}`,
  cdnBase: 'https://cdn.bsky.app',       // base for the default blobUrl builder
  permalink: (vm) => webUrlFor(vm),      // turn the at:// uri into a web link
  groupByDay: true,                      // Today / Yesterday / date headers (UTC)
  now: new Date(),                       // reference point for grouping + times
  emptyText: 'No activity yet.',
});

Options (all optional):

  • blobUrl(did, cid): string | null | undefined -- builds an image URL for blob-ref media. Return null to skip. Default: ${cdnBase}/img/feed_fullsize/plain/{did}/{cid}@jpeg.
  • cdnBase: string -- base for the default blobUrl builder. Default https://cdn.bsky.app.
  • permalink(vm): string | null | undefined -- the VM's uri is an at:// URI, not a web URL; return an http(s) URL to link the card title. Default: unlinked (per-item permalinks are deferred).
  • groupByDay: boolean -- group items under Today / Yesterday / date headers (UTC, deterministic for server rendering). Default true.
  • now: Date -- reference "now" for relative times and day grouping. Default new Date().
  • emptyText: string -- shown when the stream is empty. Default "No activity yet.".

All user-controlled strings are HTML-escaped and every URL is scheme-validated (http/https only), the same way the profile renderer handles profile data. The card body switches on body.kind (text | media | link | track | generic); unrecognized future kinds degrade to the text fallback. Style the output with the .activity-stream / .stream-* rules in getCSS().

renderActivityPage(profile, sections, vms, ctx?, streamOptions?): string

Render a standalone activity ("Now") page: the same masthead + sidebar + footer layout as renderSectionPage, with renderActivityStream(vms, streamOptions) as its main content. vms is an array of StreamCardVM (as consumed by renderActivityStream); streamOptions is forwarded verbatim. Returns a complete HTML document with <title>Now - {name}</title> and the "Now" nav item marked active. The page links to now.html (its nav slug is now), so a static build writes it as dist/now.html.

import { renderActivityPage } from '@singi-labs/sifa-page-renderer';

const nowHtml = renderActivityPage(profile, sections, vms, { year: 2026 }, {
  permalink: (vm) => webUrlFor(vm),
});
// Write to dist/now.html

renderActivityPage forces the "Now" nav entry on for its own page. To surface that link on the home page and every section page too, set the ctx.activityStream flag on those calls (see below).

ctx.activityStream -- the "Now" nav flag

RenderContext accepts an optional activityStream flag that injects the "Now" nav entry (masthead + mobile bottom nav) into renderHome, renderSectionPage, and renderSinglePage, linking to now.html. Pass true for the default "Now" label, or an ActivityNavConfig object to customize it. When omitted, the nav is byte-identical to a build without an activity page.

ActivityNavConfig accepts:

  • label -- nav label + page title for the activity page. Default: "Now".
  • href -- href for the "Now" nav entry (masthead + mobile bottom nav). Default: "now.html". Set this so a single-page host (e.g. sifa-web's page.sifa.id/{handle} route, driven by renderSinglePage) can point "Now" at a real per-handle URL such as "/gui.do/now" instead of the static file. May be an absolute http(s) URL or a same-origin relative path; the value is validated and escaped, and executable schemes like javascript: are rejected (falling back to "now.html"). Active-state highlighting is keyed on the entry's slug (now), so a custom href still highlights correctly on the activity page.
const ctx = { year: 2026, activityStream: true };
const indexHtml = renderHome(profile, sections, ctx);
const careerHtml = renderSectionPage(profile, career, sections, ctx);
const nowHtml = renderActivityPage(profile, sections, vms, ctx);
// All three now share a nav with an active-on-now.html "Now" link.

// Point "Now" at a per-handle URL (e.g. for a server-rendered single-page host):
const hosted = { activityStream: { href: '/gui.do/now' } };
const singleHtml = renderSinglePage(profile, sections, hosted);

ctx.profileHomeHref -- point section links at the single-page home

On a single-page host, the standalone activity page rendered by renderActivityPage lives at a nested route like page.sifa.id/{handle}/now. Its section nav (About/Career/…) would otherwise emit relative career.html links, which resolve to page.sifa.id/{handle}/career.html and 404. Set ctx.profileHomeHref to the single-page profile home so those links point back to it instead:

  • the About/index section links to profileHomeHref itself (e.g. /gui.do);
  • every other section with slug S links to profileHomeHref + # + S (e.g. /gui.do#career).

The rewrite applies to both the masthead and the mobile bottom nav. The "Now" activity entry keeps its own activityStream.href and active-state, and the masthead brand badge is untouched. Section active-state stays slug-based.

Like activityStream.href, the value may be an absolute http(s) URL or a same-origin relative path; it is validated and escaped, and executable schemes like javascript: are rejected (falling back to the default section links). When omitted, the nav is byte-identical to today.

const ctx = {
  activityStream: { href: '/gui.do/now' },
  profileHomeHref: '/gui.do',
};
const nowHtml = renderActivityPage(profile, sections, vms, ctx);
// Section links now point at /gui.do and /gui.do#career; "Now" stays /gui.do/now.

ctx.did -- at-tags AT URI meta tags

Set ctx.did to the DID of the account the page renders and every page gains the at-tags meta tags, which map the HTML back to the AT record it is built from:

<meta name="at:canonical" content="at://did:plc:xxx/id.sifa.profile.self/self">
<meta name="at:author" content="at://did:plc:xxx">

at:canonical points at id.sifa.profile.self because deleting that record is what would make the page cease to exist, which is the spec's test for canonical. Per-section records are not emitted as at:alternate: a filled profile spans 13+ collections, and the proposal gives no guidance yet for pages that aggregate many records.

The value is ignored unless it parses as a DID (did:<method>:<id>), so passing a handle by mistake emits nothing rather than a bogus AT URI. When omitted, the <head> is byte-identical to today.

const html = renderHome(profile, sections, { did: 'did:plc:zcanytz...' });

parseSections(md: string): ParsedSection[]

Parse a markdown string into ##-keyed sections. Retained for consumers that still parse the .md export; the renderer itself no longer uses it.

sectionSlug(title: string): string

Convert a section title to a URL-safe slug.

isSidebarOnly(title: string): boolean

Returns true for sections that render in the sidebar (currently "Links").

getCSS(opts?): string

Generate the stylesheet. Pass { fontDir: '/custom/path' } to override font paths.

CSS: string

Default stylesheet (equivalent to getCSS()).

Static assets

The package includes fonts and SVG logos under static/. Import them via the package exports:

@singi-labs/sifa-page-renderer/static/fonts/quattro-regular.woff2
@singi-labs/sifa-page-renderer/static/assets/sifa-logo.svg

Or copy them to your build output:

cp -r node_modules/@singi-labs/sifa-page-renderer/static/* dist/

Data requirements

The renderer expects a single data input:

  • Profile -- an SDK Profile object (from fetchProfile). Identity fields (handle, displayName, headline, about, avatar, website, location*, externalAccounts) drive the sidebar/footer; the section arrays (positions, education, publications, ...) drive the body sections via buildProfileSections.

License

MIT. See LICENSE.

Fonts: Quattro (SIL Open Font License), Space Grotesk (SIL Open Font License). See static/fonts/LICENSE.txt.