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

@devchitchat/index97

v2.2.1

Published

A Bun-native web framework that trusts the platform. File-based routing, zero build steps, HTML that works, JavaScript that ships.

Downloads

761

Readme

index97

A Bun-native web framework. File-based routing, server-side templates, zero config.


Prerequisites

Install Bun:

curl -fsSL https://bun.sh/install | bash

Phase 1 — Up and running in 5 minutes

1. Create a project

mkdir my-site && cd my-site
bun init -y
bun add @devchitchat/index97

2. Create the entry point

// server.js
import { createServer } from '@devchitchat/index97'
createServer({ pagesDir: './pages' })

3. Create your first page and layout

mkdir pages pages/public
<!-- pages/_layout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{slot:title || My Site}}</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
  <main>
    {{content}}
  </main>
</body>
</html>
<!-- pages/index.html -->
<template data-slot="title">Home — My Site</template>

<h1>Hello, world.</h1>
<include src="_greeting.phtml">
<!-- pages/_greeting.phtml -->
<p>Welcome to index97. Files are routes. No config needed.</p>
/* pages/public/style.css */
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
nav a { margin-right: 1rem; }

4. Run it

bun server.js

Open http://localhost:3000. Edit any file — the browser updates instantly.


Phase 2 — Level up

Dynamic routes

Wrap a folder name in brackets to make it a parameter.

pages/
  blog/
    [slug].js       ← handles /blog/hello-world
    [slug].phtml    ← template for the above
// pages/blog/[slug].js
import db from './_db.js'

export async function GET(req) {
  const post = db.query('SELECT * FROM posts WHERE slug = ?').get(req.params.slug)
  if (!post) return new Response('', { status: 404 })
  return { post }
}
<!-- pages/blog/[slug].phtml -->
<h1>{{post.title}}</h1>
<p>{{post.body}}</p>

Templates

| Syntax | What it does | |--------|-------------| | {{name}} | Render value, HTML-escaped | | {{{name}}} | Render value, raw HTML | | {{#if name}}...{{/if}} | Conditional | | {{#each items}}...{{/each}} | Loop — {{this}} is each item | | <include src="partial.phtml"> | Server-side partial | | <include src="partial.phtml" label="@item.label"> | Pass data to partial |

Layout slots

Pages can inject into named slots in the layout:

<!-- in any page -->
<template data-slot="title">About — My Site</template>
<template data-slot="head">
  <link rel="stylesheet" href="/about.css">
</template>

<h1>About</h1>
<!-- in _layout.html -->
<title>{{slot:title || My Site}}</title>
{{slot:head}}
{{content}}

Server-side layout data

Export a data function from _layout.js to make values available across every page — useful for navigation, session state, feature flags:

// pages/_layout.js
export function data(req) {
  const session = getSession(req)
  return { session }
}
<!-- in _layout.html -->
{{#if session}}<a href="/signout">Sign out</a>{{/if}}

Forms with PUT / PATCH / DELETE

Forms only support GET and POST natively. index97 rewrites the others automatically:

<form method="DELETE" action="/posts/42">
  <button>Delete</button>
</form>

Export the matching method from your handler:

export async function DELETE(req) {
  db.run('DELETE FROM posts WHERE id = ?', [req.params.id])
  return Response.redirect('/posts', 303)
}

Static site generation

Export staticPaths() from any dynamic handler to tell the build which URLs to render:

// pages/blog/[slug].js
export function staticPaths() {
  return db.query('SELECT slug FROM posts').all().map(p => ({ slug: p.slug }))
}
bunx index97 build   # renders all routes to dist/
bunx index97 serve   # serves dist/ as a static site

CLI

| Command | What it does | |---------|-------------| | bunx index97 | Start dev server with HMR | | bunx index97 start | Start production server | | bunx index97 build | Generate static site to dist/ | | bunx index97 serve | Serve a pre-built dist/ |

Project layout

my-site/
  server.js           ← entry point
  pages/
    _layout.html      ← wraps every page
    _layout.js        ← server-side data for the layout
    index.html        ← /
    about.html        ← /about
    blog/
      index.html      ← /blog
      [slug].js       ← /blog/:slug  (handler)
      [slug].phtml    ← template for the handler
    public/
      style.css       ← served as static files
      logo.png

Files starting with _ are private — they are not routes.


Security

Content Security Policy

index97 sets the following CSP header on every response by default:

default-src 'self'; style-src 'self'; script-src 'self'

This means inline styles and inline scripts are blocked. Use external stylesheets and script files served from pages/public/ instead.

<!-- blocked -->
<div style="color: red">hello</div>
<style>body { margin: 0 }</style>

<!-- allowed -->
<link rel="stylesheet" href="/style.css">

To override the CSP for the entire server, pass a csp option to createServer:

import { createServer } from '@devchitchat/index97'
createServer({
  pagesDir: './pages',
  csp: "default-src 'self'; style-src 'self' 'unsafe-inline'"
})

To override the CSP for a single route, set Content-Security-Policy on the response returned by the handler — index97 will leave it untouched:

Bun.serve({
  routes: {
    '/embed': {
      GET: () => new Response('ok', {
        headers: { 'Content-Security-Policy': "frame-ancestors 'self' https://example.com" }
      })
    }
  }
})

The same pattern works for Permissions-Policy.