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

@x-wave/blog

v2.9.0

Published

A responsive, multi-language documentation framework for React + Vite applications. Ships with TypeScript, i18n (i18next), MDX support, dark mode, and built-in navigation.

Readme

@x-wave/blog

A responsive, multi-language documentation framework for React + Vite applications. Ships with TypeScript, i18n (i18next), MDX support, dark mode, and built-in navigation.

Features

  • Multi-language support: Ship 3+ languages with a single codebase (en, es, zh included)
  • MDX content: Write docs in Markdown with React components
  • Dark mode: Built-in light/dark/system theme toggle with localStorage persistence
  • Advanced mode: Optional Simple/Advanced content variants for the same page
  • Mobile responsive: Automatic sidebar → mobile menu on small screens
  • Headless: No styling opinions—includes SCSS variables for full customization
  • HMR-friendly: Vite development with Hot Module Replacement for instant feedback

Installation

npm

npm install @x-wave/blog

pnpm

pnpm add @x-wave/blog

yarn

yarn add @x-wave/blog

Quick setup

1. Create your app structure

src/
├── App.tsx                      # Your app component
├── main.tsx                     # Entry point
├── navigation.ts                # Site navigation definition
├── utils.ts                     # Content loaders
├── logo.svg                     # Optional: your logo
└── docs/
    ├── en/
    │   ├── welcome.mdx
    │   ├── glossary.mdx
    │   └── faq.mdx
    ├── es/
    │   ├── welcome.mdx
    │   ├── glossary.mdx
    │   └── faq.mdx
    └── zh/
        ├── welcome.mdx
        ├── glossary.mdx
        └── faq.mdx

2. Set up i18n and styles

Import the i18n setup and framework styles in your app entry point:

// src/main.tsx
import '@x-wave/blog/locales'   // Initialises i18next with en, es, zh
import '@x-wave/blog/styles'    // Compiled CSS with variables and component styles (required)
import { createRoot } from 'react-dom/client'
import App from './App'

createRoot(document.getElementById('root')!).render(<App />)

The styles import is required for the UI components and layout to render correctly. This imports a single compiled CSS file (index.css) that contains all framework styles, CSS variables, and component styles.

Add custom translations:

// src/main.tsx
import '@x-wave/blog/locales'
import i18next from 'i18next'

// Add French translations
i18next.addResourceBundle('fr', 'translation', {
  language: 'Français',
  'ui.simple': 'Simple',
  'ui.advanced': 'Avancé',
  // ... other keys
})

3. Define your navigation

// src/navigation.ts
import type { NavigationEntry } from '@x-wave/blog/types'

export const NAVIGATION_DATA: NavigationEntry[] = [
  {
    title: 'docs.welcome',
    slug: 'welcome',
  },
  {
    title: 'Help',
    defaultOpen: true,
    items: [
      {
        title: 'docs.glossary',
        slug: 'glossary',
      },
      {
        title: 'docs.faq',
        slug: 'faq',
      },
    ],
  },
]

4. Create content loaders

// src/utils.ts
import { createBlogUtils } from '@x-wave/blog'

// Vite glob import – resolved relative to this file
const mdxFiles = import.meta.glob('./docs/**/*.mdx', {
  query: '?raw',
  import: 'default',
  eager: false,
})

// Export all blog utilities in a single object
export const blog = createBlogUtils(mdxFiles)

5. Wrap your app with BlogProvider

// src/App.tsx
import { BlogProvider, DocumentationRoutes } from '@x-wave/blog'
import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-dom'
import { blog } from './utils'
import { NAVIGATION_DATA } from './navigation'

const SUPPORTED_LANGUAGES = ['en', 'es', 'zh'] as const

export default function App() {
  return (
    <BlogProvider
      config={{
        title: 'My Documentation',
        supportedLanguages: SUPPORTED_LANGUAGES,
        navigationData: NAVIGATION_DATA,
        header: {
          navLinks: [
            {
              label: 'Visit Site',
              url: 'https://example.com',
              target: '_blank',
            },
          ],
        },
      }}
      blog={blog}
    >
      <Router>
        <Routes>
          <Route path="/:language/*" element={<DocumentationRoutes />} />
          <Route path="/" element={<Navigate to="/en/welcome" replace />} />
        </Routes>
      </Router>
    </BlogProvider>
  )
}

Note: BrowserRouter requires server configuration to fallback to index.html for all routes.

Using a base path

To mount documentation under a subpath (e.g., /blog or /docs), set the basePath config option:

export default function App() {
  return (
    <BlogProvider
      config={{
        title: 'My Documentation',
        basePath: '/blog',  // All routes will be prefixed with /blog
        supportedLanguages: SUPPORTED_LANGUAGES,
        navigationData: NAVIGATION_DATA,
      }}
      blog={blog}
    >
      <Router>
        <Routes>
          {/* Routes now mounted under /blog */}
          <Route path="/blog/:language/*" element={<DocumentationRoutes />} />
          <Route path="/blog" element={<Navigate to="/blog/en/welcome" replace />} />
          
          {/* Your other app routes */}
          <Route path="/" element={<HomePage />} />
        </Routes>
      </Router>
    </BlogProvider>
  )
}

This ensures internal navigation (search, tags, language switching) uses the correct base path.

Writing content

File naming

Place your MDX files in language-specific directories:

src/docs/
├── en/welcome.mdx
├── es/welcome.mdx
└── zh/welcome.mdx

File names must match the slug field in your navigation definition.

Frontmatter

Optional YAML at the top of your MDX file:

---
title: Getting Started
description: Learn the basics in 5 minutes
author: Jane Doe
date: 2026-02-23
keywords:
  - getting started
  - tutorial
  - basics
ogImage: getting-started.png
hasAdvanced: true
tags:
  - tutorial
  - beginner
---

# Welcome!

Regular content here.

| Field | Type | Description | |---|---|---| | title | string | Document title (informational, not displayed by framework) | | description | string | Optional SEO description for the article. Used in the page <meta name="description"> tag. Falls back to site-level description if not provided. | | author | string | Author name. Displayed below the page title with a user icon. | | date | string | Publication or update date. Displayed below the page title with "Last edited" label and calendar icon (i18n supported). | | keywords | string[] \| string | Optional array of keywords or comma-separated string for SEO. Inserted into the page <meta name="keywords"> tag. | | ogImage | string | Optional dedicated Open Graph image filename for the article. Used in the page <meta property="og:image"> tag for social media sharing. If not provided, falls back to site-level og image if available. | | hasAdvanced | boolean | Enables Simple/Advanced mode toggle. Requires a -advanced.mdx variant. | | tags | string[] | Array of tag strings for categorizing content. Tags are automatically indexed by the framework when you pass mdxFiles to BlogProvider. Tags are clickable and show search results. |

Advanced mode variants

Create welcome-advanced.mdx alongside welcome.mdx:

src/docs/
├── en/
│   ├── welcome.mdx
│   └── welcome-advanced.mdx

Set hasAdvanced: true in the simple version's frontmatter, and the framework automatically shows a toggle.

Supported Markdown

The blog system supports standard Markdown and extended syntax via GitHub Flavored Markdown (GFM). Here's what you can use:

Headings

# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6

Text Styling

**bold text**
*italic text*
***bold and italic***
~~strikethrough~~

Lists

Unordered lists:

- Item 1
- Item 2
  - Nested item

Ordered lists:

1. First item
2. Second item
   1. Nested item

Links

[External link](https://example.com)
[Internal link](./getting-started)

Internal links using relative paths (starting with ./) will automatically be configured with the correct language prefix and base path.

Code

Inline code:

Use the `createBlogUtils()` function to setup your blog.

Code blocks:

```
const blog = createBlogUtils(mdxFiles)
```

Code blocks accept generic fenced code without language specifiers.

Blockquotes

> This is a blockquote.
> 
> It can span multiple lines.

Tables

| Column 1 | Column 2 | Column 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   | Cell 3   |
| Cell 4   | Cell 5   | Cell 6   |

Images

![Alt text](./image.png)

Images support automatic dark mode variants. If a dark variant exists at image-dark.png, it will automatically be used when the dark theme is active.


Charts

Embed responsive charts directly in your Markdown using fenced code blocks with the chart language identifier. The body must be valid JSON.

Supported chart types: bar, line, pie.

Bar chart:

```chart
{
  "type": "bar",
  "title": "Monthly Revenue",
  "xKey": "month",
  "data": [
    { "month": "Jan", "revenue": 4000 },
    { "month": "Feb", "revenue": 3000 },
    { "month": "Mar", "revenue": 5000 }
  ],
  "series": [{ "key": "revenue", "name": "Revenue" }]
}
```

Line chart:

```chart
{
  "type": "line",
  "title": "Daily Users",
  "xKey": "day",
  "data": [
    { "day": "Mon", "users": 120, "sessions": 200 },
    { "day": "Tue", "users": 150, "sessions": 230 },
    { "day": "Wed", "users": 180, "sessions": 260 }
  ],
  "series": [
    { "key": "users", "name": "Users", "color": "#6366f1" },
    { "key": "sessions", "name": "Sessions", "color": "#10b981" }
  ]
}
```

Pie chart:

```chart
{
  "type": "pie",
  "title": "Traffic Sources",
  "data": [
    { "name": "Organic", "value": 400 },
    { "name": "Referral", "value": 300 },
    { "name": "Direct", "value": 200 }
  ]
}
```

Chart JSON fields:

| Field | Type | Required | Description | |---|---|---|---| | type | "bar" \| "line" \| "pie" | Yes | Chart type | | data | object[] | Yes | Array of data points | | title | string | No | Chart title displayed above | | xKey | string | No | Key for X-axis categories (bar/line). Default: "name" | | xLabel | string | No | Label for the X-axis | | yLabel | string | No | Label for the Y-axis | | series | { key, name?, color? }[] | No | Data series definitions (bar/line). Auto-detected from numeric keys if omitted | | nameKey | string | No | Key for slice labels (pie). Default: "name" | | valueKey | string | No | Key for slice values (pie). Default: "value" | | height | number | No | Chart height in pixels. Default: 350 |

Charts are fully responsive and adapt to dark/light themes automatically.


API reference

Components

All components are exported from @x-wave/blog:

import { 
  BlogProvider, 
  DocumentationRoutes,
  DocumentationLayout, 
  ContentPage,
  HomePage,
  Header,
  Sidebar,
  TableOfContents,
  AdvancedModeToggle,
  Metadata
} from '@x-wave/blog'

| Component | Purpose | |---|---| | BlogProvider | Root context wrapper (required) | | DocumentationRoutes | Pre-configured Routes for documentation pages (recommended) | | DocumentationLayout | Page layout: header + sidebar + content | | ContentPage | Loads and renders MDX pages | | HomePage | Lists latest articles with metadata (configurable via defaultRoute) | | Header | Top navigation bar | | Sidebar | Left navigation panel | | TableOfContents | "On this page" anchor panel | | AdvancedModeToggle | Simple/Advanced tab switch | | Metadata | Displays author and publication date with icons (used in HomePage and ContentPage) |

Hooks

import { useTheme } from '@x-wave/blog'

const { theme, setTheme, effectiveTheme } = useTheme()

Manages light/dark/system theme preference.

Utilities

import { createBlogUtils } from '@x-wave/blog'

const blog = createBlogUtils(mdxFiles)

Creates all blog utilities from a Vite glob import. This is the recommended approach as it bundles everything together.

Returns an object with:

  • mdxFiles: The glob import (used internally for automatic tag indexing)
  • loadContent(language, slug, advanced?): Loads MDX content for a specific language and slug
  • loadEnglishContent(slug, advanced?): Loads English content for heading ID generation

Pass the entire blog object to BlogProvider:

<BlogProvider config={config} blog={blog}>

Alternative: Advanced usage

import { createContentLoaders } from '@x-wave/blog'

const { loadMDXContent, loadEnglishContent, buildTagIndex } = createContentLoaders(mdxFiles)

For advanced use cases where you need more control over content loading and tag indexing.

Types

All TypeScript types are exported from @x-wave/blog/types:

import type { NavigationEntry, BlogConfig, HeaderLink } from '@x-wave/blog/types'

Customization

CSS variables

The framework uses CSS custom properties (--xw-* prefix) for theming. These are automatically injected when the framework initializes, but you can override them:

/* In your app.css */
.xw-blog-root {
  --xw-primary: #007bff;
  --xw-background: #fafafa;
  --xw-foreground: #1a1a1a;
  /* ... other variables */
}

Or in JavaScript:

const blogRoot = document.querySelector('.xw-blog-root')
blogRoot.style.setProperty('--xw-primary', '#007bff')
blogRoot.style.setProperty('--xw-background', '#fafafa')

Available CSS variables include:

  • --xw-primary, --xw-secondary
  • --xw-background, --xw-foreground
  • --xw-card, --xw-card-foreground
  • --xw-muted, --xw-muted-foreground
  • --xw-accent, --xw-accent-foreground
  • --xw-border, --xw-ring
  • And more—defined in packages/styles/main.scss

All CSS variables are scoped to .xw-blog-root for isolation and to prevent conflicts with your application styles.

Link color overrides (light/dark)

Link color is controlled through CSS variables in the consuming app.

  • --xw-link-light: used in light theme
  • --xw-link-dark: used in dark theme

Set them on .xw-blog-root (or a parent scope) to override defaults:

/* In your app.css */
.xw-blog-root {
  --xw-link-light: oklch(0.62 0.16 252);
  --xw-link-dark: oklch(0.78 0.12 252);
}

If either variable is not set, the framework falls back to its built-in value from packages/styles/main.scss. This override is intentionally CSS-based and is not passed through BlogProvider.

Config options

BlogConfig properties:

interface BlogConfig {
  title: string                                    // Site title
  description?: string                            // Optional default description for SEO (used as fallback if article has no description)
  logo?: React.ComponentType<{ className?: string }>  // Optional logo
  supportedLanguages: readonly string[]           // e.g. ['en', 'es', 'zh']
  navigationData: NavigationEntry[]               // Menu structure
  basePath?: string                               // Base path for all routes (default: '')
  contentMaxWidth?: string                        // Max width for content area (default: '80rem')
  defaultRoute?: string                           // Default route: 'latest' (home page) or article slug (default: 'latest')
  header?: {                                       // Optional: omit to hide built-in header
    navLinks?: HeaderLink[]                       // Top-level nav links
    dropdownItems?: HeaderDropdownItem[]          // Support dropdown menu
  }
}

Key properties:

  • header: Optional. If omitted entirely, the built-in header component will not be rendered. Use this when you want to implement a custom header.
  • description: Optional. Default SEO description used in the page <meta name="description"> tag. Articles can override this with their own description in frontmatter.
  • contentMaxWidth: Optional. Set a custom maximum width for the main content area. Example: '100rem' or '1400px'. Default: '80rem'.
  • defaultRoute: Optional. Controls which page displays at the root path (/):
    • Set to 'latest' (default) to show the HomePage listing the latest articles
    • Set to any article slug (e.g., 'welcome', 'getting-started') to redirect the root path to that article

Home page feature

The framework includes an optional home/latest posts page that lists articles sorted by publish date. Configure it with the defaultRoute option:

// Show home page at root path (lists latest articles)
config: {
  defaultRoute: 'latest'  // or omit (defaults to 'latest')
}

// Or redirect root path to a specific article
config: {
  defaultRoute: 'welcome'  // Root path goes to /welcome article
}

The home page displays:

  • Title: "Latest Posts" (i18n translated)
  • Header metadata: Author and date from the most recent article
  • Article cards: Recent articles with title, description, author, and date
  • Sorting: Articles sorted by date (newest first), limited to 50 articles

Article metadata comes from MDX frontmatter:

---
title: Getting Started
author: Jane Doe
date: 2026-02-23
description: Learn the basics in 5 minutes
keywords: [getting started, tutorial, basics]
---

Static article indexer (blog-indexer)

The framework ships a CLI tool that pre-generates static JSON index files from your MDX content. The home page and month-based archive views read these files at runtime instead of discovering articles on each request.

Install (the CLI is included in @x-wave/blog):

npx blog-indexer --docs src/docs --output public/blog-index

Or add it as a project script:

// package.json
{
  "scripts": {
    "index": "blog-indexer --docs src/docs --output public/blog-index"
  }
}

Options:

| Flag | Description | |---|---| | --docs | Path to the docs directory (must contain language sub-directories, e.g. en/, es/) | | --output | Path to the output directory where JSON index files will be written |

Output structure (one set per language):

public/blog-index/
├── en/
│   ├── latest.json            # Latest 20 articles (home page)
│   ├── months-index.json      # Sorted list of YYYY-MM strings
│   └── months/
│       └── 2026-02.json       # Articles for that month
├── es/
│   └── ...
└── zh/
    └── ...

Run the indexer whenever you add, remove, or update articles, and commit the output so it is available at deploy time.

CI: keeping the index in sync

Add a GitHub Actions workflow to verify the committed index is up to date on every push. The workflow re-runs the indexer and fails if the output differs from what is committed:

# .github/workflows/check-blog-index.yml
name: Check blog index is up to date

on: push

jobs:
  check-index:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - name: Run blog indexer
        run: npx blog-indexer --docs src/docs --output public/blog-index

      - name: Verify index is up to date
        run: |
          if ! git diff --exit-code public/blog-index/; then
            echo "::error::Blog index is out of date. Run the indexer and commit the result."
            exit 1
          fi

git diff --exit-code compares file content hashes internally, so no separate hash file is needed — if any generated file differs from the committed version the job fails with a clear error message.

Custom headers

If you need a custom header instead of the built-in one, simply omit the header config and use the provided hooks:

import { useTheme, useSearchModal } from '@x-wave/blog'
import { Moon, Sun, MagnifyingGlass } from '@phosphor-icons/react'

function CustomHeader() {
  const { theme, setTheme } = useTheme()
  const { openSearchModal } = useSearchModal()

  return (
    <header>
      <h1>My Custom Header</h1>
      
      {/* Theme toggle button */}
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        {theme === 'dark' ? <Sun /> : <Moon />}
      </button>
      
      {/* Search button */}
      <button onClick={openSearchModal}>
        <MagnifyingGlass />
      </button>
    </header>
  )
}

function App() {
  return (
    <BlogProvider
      config={{
        title: 'My Docs',
        supportedLanguages: ['en'],
        navigationData: NAVIGATION_DATA,
        // No header config = no built-in header
      }}
      blog={blog}
    >
      <CustomHeader />
      <Router>
        <Routes>
          <Route path="/:language/*" element={<DocumentationRoutes />} />
        </Routes>
      </Router>
    </BlogProvider>
  )
}

Reusable components

Metadata component

The Metadata component displays article metadata (author and publication date) with icons and i18n support. It's used throughout the framework (HomePage and ContentPage) but is also exported for your own use:

import { Metadata } from '@x-wave/blog'

function MyComponent() {
  return (
    <Metadata 
      author="Jane Doe"
      date="2026-02-23"
    />
  )
}

The component:

  • Displays author with a user icon
  • Displays date with a calendar icon and "Last edited" label
  • Automatically formats the date using the user's locale
  • Returns null if both author and date are missing
  • Uses i18n for the "Last edited" label and date formatting

Available hooks:

  • useTheme(): Returns { theme, setTheme, effectiveTheme } for managing light/dark/system theme
  • useSearchModal(): Returns { openSearchModal, closeSearchModal } for controlling the search modal

These are the exact functions used by the built-in header, so you get the same functionality with full customization.

Key properties (continued):

| Property | Type | Required | Description | |---|---|---|---| | title | string | Yes | Site title displayed in header and sidebar | | supportedLanguages | string[] | Yes | Array of language codes (e.g., ['en', 'es', 'zh']) | | navigationData | NavigationEntry[] | Yes | Sidebar navigation structure | | basePath | string | No | Base path prefix for all routes. Use when mounting docs under a subpath like /blog or /docs. Default: '' (root) | | logo | React.ComponentType | No | SVG component for site logo | | header | object | No | Header configuration with nav links and dropdown items |

Browser support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
  • Mobile browsers (iOS Safari 14.5+, Chrome Android 90+)

License

See LICENSE file in this repository.


For framework maintainers

Contributing or maintaining this framework? See DEVELOPMENT.md for setup, architecture, and build system details.