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.1.7

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',
        showTableOfContents: true,
      },
      {
        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
author: Jane Doe
date: 2026-02-23
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. | | 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.

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.

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]
---

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.