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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@edgedev/template-engine

v0.1.12

Published

Lightweight template renderer for Edge CMS content.

Downloads

928

Readme

@edgedev/template-engine

Utility function with rendering logic. Feed it content, values, and meta data and receive HTML you can drop into any Vue (or non-Vue) surface.

Install

npm install @edgedev/template-engine

Usage

import renderTemplate from '@edgedev/template-engine'

const html = renderTemplate(contentString, valueMap, metaDefinition)

Treat valueMap and metaDefinition as override objects: they selectively replace values/meta defined within the template, and any omitted parameter (or key) falls back to the template's embedded defaults.

Full Page Rendering

Use pageRender when you need to hydrate multiple blocks, render their HTML, and collect the UnoCSS used across the entire page.

import { pageRender } from '@edgedev/template-engine'

const theme = {
  extend: {
    colors: { brand: '#2563eb' },
    fontFamily: { brand: ['Inter', 'sans-serif'] },
  },
}

const { blocks, css } = await pageRender(
  [
    {
      name: 'hero',
      content: '<section class="bg-brand text-white">{{{#text {"field":"headline"} }}}</section>',
      values: { headline: 'Launch in days, not months.' },
    },
    {
      name: 'listing',
      content: `
        <ul>
          {{{#array {"field":"items","as":"item"} }}}
            <li class="flex gap-2">
              <span class="font-semibold">{{ item.title }}</span>
              <span class="text-sm text-gray-500">{{ item.subtitle }}</span>
            </li>
          {{{/array}}}
        </ul>
      `,
      meta: {
        items: { schema: { title: 'text', subtitle: 'text' } },
      },
    },
  ],
  theme,
  '<div class="hidden md:block fixed inset-0 pointer-events-none radial-gradient-mask"></div>',
  {
    uniqueKey: 'org-123:site-456',
    clientOptions: {
      binding: env?.MY_INDEX_KV,
      accountId: process.env.CF_ACCOUNT_ID!,
      namespaceId: process.env.CF_NAMESPACE_ID!,
      apiToken: process.env.CF_API_TOKEN!,
    },
  },
)

// blocks => [{ name: 'hero', html: '<section ...>...</section>' }, ...]
// css => aggregated UnoCSS needed for all rendered HTML + extraHtml
  • Each block is hydrated first using the provided hydrate options, then rendered via renderTemplate.
  • extraHtml is optional helper markup that should also contribute to UnoCSS generation (e.g. layout shells).
  • The returned css string is ready to inline or serve as critical CSS. Rendered blocks remain accessible individually if you need to target specific components.

The renderer supports:

  • Root-level and nested #array / #subarray blocks with aliases
  • Conditional #if / #else blocks
  • Simple blocks (#text, #image, #textarea, #richtext)
  • Schema-aware formatting for number, integer, money, and richtext types using the provided meta

See src/index.ts for the full list of helpers that mimic the original Vue component.

Comprehensive Example

const content = `
<section class="playbook">
  <header>
    <h1>{{{#text {"field":"title"} }}}</h1>
    <p>{{{#textarea {"field":"summary"} }}}</p>
    <img :src="{{{#image {"field":"heroImage"} }}}" alt="Hero" />
    {{{#richtext {"field":"body"} }}}
  </header>

  <div class="cta">
    <a href="{{{ {"field":"heroHref","value":"https://edge.co"} }}}">{{{ {"field":"heroCta","type":"text"} }}}</a>
  </div>

  <ul class="stats">
    {{{#array {"field":"stats","as":"stat"} }}}
      <li>
        <span class="label">{{ stat.label }}</span>
        <span class="value">{{ stat.value }}</span>
      </li>
    {{{/array}}}
  </ul>

  <section class="team">
    {{{#array {"field":"team","as":"member","schema":{"budget":"money"}} }}}
      <article>
        <h3>{{ member.name }}</h3>
        <p>{{ member.role }}</p>
        {{{#if {"cond":"item.isLead == true"} }}}
          <span class="badge">Lead</span>
        {{{#else}}}
          <span class="badge muted">Contributor</span>
        {{{/if}}}
        <p class="bio">{{ member.bio }}</p>
        <div>Annual budget: {{ member.budget }}</div>

        <ul>
          {{{#subarray:project {"field":"item.projects","limit":2} }}}
            <li>
              <strong>{{ project.name }}</strong>
              <span>{{ project.status }}</span>
            </li>
          {{{/subarray}}}
        </ul>
      </article>
    {{{/array}}}
  </section>
</section>
`

const values = {
  title: 'Q4 Launch Playbook',
  summary: 'Tactics and timelines for the winter product drop.',
  heroImage: 'https://cdn.edge.co/playbook-hero.png',
  heroHref: 'https://edge.co/campaigns/playbook',
  heroCta: 'View Campaign Timeline',
  body: '<p><strong>Note:</strong> This block accepts raw HTML.</p>',
  stats: [
    { label: 'Net-new leads', value: 3400 },
    { label: 'Pipeline ($)', value: 1750000 },
  ],
  team: [
    {
      name: 'Mia Chen',
      role: 'Program Manager',
      isLead: true,
      budget: 125000,
      bio: 'Owns launch roadmap & GTM alignment.',
      projects: [
        { name: 'Hubble', status: 'Active' },
        { name: 'Lumen', status: 'Planning' },
      ],
    },
    {
      name: 'Jonas Patel',
      role: 'Growth Engineer',
      isLead: false,
      budget: 60000,
      bio: '<em>Automates activation experiments.</em>',
      projects: [
        { name: 'Arcade', status: 'QA' },
      ],
    },
  ],
}

const meta = {
  stats: {
    // object-based schema (field -> type)
    schema: {
      value: 'number',
    },
  },
  team: {
    // array-based schema (common on older Edge configs)
    schema: [
      { field: 'budget', type: 'money' },
      { field: 'bio', type: 'richtext' },
    ],
  },
}

const html = renderTemplate(content, values, meta)

Key behaviors demonstrated:

  • {{{#text}}}, {{{#textarea}}}, {{{#image}}}, and {{{#richtext}}} show the simple block helpers.
  • {{ {"field":"...","type":"text"} }} illustrates double-brace placeholders with type-aware escaping.
  • #array handles the root stats and team lists, while #subarray reaches into each member’s nested projects.
  • The schema on stats (object form) and team (array form) ensures value, budget, and bio pick up number/money/richtext formatting.
  • The conditional #if/#else block switches the badge copy based on item.isLead.
  • limit on #subarray trims the nested list to the first two entries.

Cloudflare KV Index Client

The package also exposes a helper for working with Cloudflare KV index namespaces. Initialize it once with your credentials (and optional Worker KV binding) and reuse the client to run prefix queries.

import { createKvIndexClient } from '@edgedev/template-engine'

const kvClient = createKvIndexClient({
  // Optional: the Workers KV binding when running inside a Worker / Pages Function.
  binding: env?.MY_INDEX_KV,

  // Required: account + namespace identifiers for the REST fallback.
  accountId: process.env.CF_ACCOUNT_ID!,
  namespaceId: process.env.CF_NAMESPACE_ID!,
  apiToken: process.env.CF_API_TOKEN!,

  // Optional: custom fetch (defaults to globalThis.fetch).
  fetch: globalThis.fetch,
})

// Example 1: expect at most one canonical match.
const [post] = await kvClient.queryIndex({
  baseKey: 'post', // the client automatically prefixes keys with "idx:"
  searchKey: 'slug',
  uniqueKey: 'user-34329473094',
  searchValue: 'blog:dasfadsfasd',
})

// Example 2: gather multiple raw index hits (no canonical pointer needed).
const relatedPosts = await kvClient.queryIndex({
  baseKey: 'post',
  searchKey: 'tag',
  uniqueKey: 'user-34329473094',
  searchValue: ['launch', 'beta'], // arrays fan out to multiple prefix searches
})

// `post` is the canonical object (when exactly one match is found).
// `relatedPosts` is an array of parsed index meta objects (no manual JSON.parse needed).

// Fetch an exact key if you already know the canonical identifier.
const canonical = await kvClient.getKey('post:canonical:user-34329473094')
// -> returns an object (or null) and emits console.warn indicating whether the binding or API was used.

Hydrating Collections from KV

Templates can declare #array blocks that describe KV-backed collections. Use hydrateValues to resolve those collections before rendering.

import {
  hydrateValues,
  renderTemplate,
  type HydrateValuesOptions,
} from '@edgedev/template-engine'

const options: HydrateValuesOptions = {
  content: contentString,
  values: initialValueOverrides,
  meta: metaDefinitionOverride,
  uniqueKey: 'workspace-1234', // the namespace segment used when indexing
  clientOptions: {
    // Optional binding (Workers runtime)
    binding: env?.MY_INDEX_KV,

    // REST fallback credentials (e.g. during SSR/build)
    accountId: process.env.CF_ACCOUNT_ID!,
    namespaceId: process.env.CF_NAMESPACE_ID!,
    apiToken: process.env.CF_API_TOKEN!,

    fetch: globalThis.fetch,
  },
}

const hydratedValues = await hydrateValues(options)
const html = renderTemplate(contentString, hydratedValues, metaDefinitionOverride)

Key details:

  • hydrateValues reads any collection, queryItems, queryOptions, order, and limit settings from the template markup or meta overrides.
  • Each query item fans out to kvClient.queryIndex calls, combining responses and deduplicating by canonical.
  • Filters (queryOptions) and ordering rules execute in JS after the index lookups, and the final array is written back to the corresponding field in the returned values.
  • If the collection cannot be fetched, the resolver falls back to any inline value you provided or leaves an empty array.

UnoCSS SSR Helpers

When your rendered HTML relies on UnoCSS utilities, the package exposes helpers to generate the exact CSS during SSR or at build time.

import {
  unoCssFromHtml,
  normalizeTheme,
  buildCssVarsBlock,
} from '@edgedev/template-engine'

const theme = {
  extend: {
    colors: {
      brand: '#2563eb',
    },
    fontFamily: {
      brand: ['Inter', 'sans-serif'],
    },
  },
  variants: {
    dark: {
      extend: {
        colors: {
          brand: '#3b82f6',
        },
      },
    },
  },
}

const { css, hash } = await unoCssFromHtml(renderedHtml, theme)

// Inject the CSS into your HTML shell, or cache it under the returned hash.
const cssVarsBlock = buildCssVarsBlock(theme) // optional: inline preflight variables yourself
const unoTheme = normalizeTheme(theme) // optional: feed into other Uno-powered tooling

Details:

  • unoCssFromHtml(html, theme) returns { css, hash }, producing minified output that already includes preflights and the custom font-family rules bundled here.
  • normalizeTheme strips the extend wrapper so your theme matches Uno’s expected shape if you need to share it elsewhere.
  • buildCssVarsBlock mirrors the default preflight while letting you embed the CSS variables manually if desired.
  • The generator is cached per theme hash, and transformer dependencies are loaded lazily; ensure your runtime supports dynamic import() (Node 18+, modern bundlers, Workers, etc.).