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

@jaydixit/astro-utils

v1.0.6

Published

Utility components and plugins for Astro projects

Readme

@jaydixit/astro-utils

A collection of utility components and plugins for Astro projects.

Installation

npm install @jaydixit/astro-utils

Features

Components

  • TableOfContents - Shared TOC with classic/socratic variants (brace indicator, auto-scroll, overlap detection)
  • ImageGallery - PhotoSwipe-powered image gallery with lightbox
  • AcademicReference - Academic citation system with tooltips
  • AnchorHeading - Linkable headings with clipboard functionality
  • Backlinks - Content relationship system
  • LinkPeek - Link preview system
  • SocialShare - Social sharing with clipboard
  • StructuredData - SEO schema generation
  • Image - Shared image pipeline (Astro assets + Unpic)

Remark Plugins

  • Wiki-style links
  • Smart quotes
  • Auto image galleries
  • Reading time
  • PDF link formatting
  • Subtitle formatting with ~ prefix
  • And more...

Blog Filtering Utilities

  • Advanced visibility control system
  • Main blog vs specialized page filtering
  • Tag-based content organization
  • Draft post management

Experimental Mode Controller

  • Flag detection (?experimental=true) with optional custom param/value
  • Variant management with keyboard cycling (⌥X by default)
  • DOM dataset sync (data-experimental, data-experimental-variant) for CSS hooks
  • Simple subscription API for wiring UI treatments or telemetry

Usage

Experimental Mode: Quick Start

  1. Install & build the shared package

    pnpm --filter @jaydixit/astro-utils build
  2. Register the controller in your global layout

    <script type="module" is:inline>
      import { createExperimentalModeController } from '@jaydixit/astro-utils/utils/experimentalMode';
    
      const controller = createExperimentalModeController({
        variants: [
          { key: 'current', label: 'Production' },
          { key: 'nav-hover', label: 'Navigation hover refresh' },
        ],
        defaultKey: 'current',
        experimentalDefaultKey: 'nav-hover',
      });
    
      controller.subscribe(({ enabled, activeKey }) => {
        window.dispatchEvent(new CustomEvent('experimental-mode-change', {
          detail: { enabled, activeKey },
        }));
      });
    </script>
  3. Style via data attributes The controller sets data-experimental and data-experimental-variant on <html>, so you can scope hover states or layouts with pure CSS:

    html[data-experimental='true'][data-experimental-variant='nav-hover'] .site-nav a:hover {
      background: var(--nav-hover-bg, rgba(56, 189, 248, 0.16));
      color: var(--nav-hover-color, #0284c7);
    }
  4. React to runtime changes (optional) Components can listen for the broadcast event or inspect window.__experimentalMode:

    window.addEventListener('experimental-mode-change', (event: CustomEvent) => {
      const { enabled, activeKey } = event.detail;
      if (enabled && activeKey === 'nav-hover') {
        document.documentElement.style.setProperty('--nav-hover-bg', 'rgba(59, 130, 246, 0.18)');
      } else {
        document.documentElement.style.removeProperty('--nav-hover-bg');
      }
    });

Hotkeys & customization

  • Flag defaults: ?experimental=true; override with paramName / paramValue.
  • Keyboard cycling: ⌥X (Option+X / Alt+X). Supply cycleHotkey to change or disable.
  • Programmatic control: window.__experimentalMode.setActiveKey('variant-key') and cycleVariant() help during QA sessions.

Shared Image Component & Resolver

  • The shared <Image /> resolves inputs flexibly:
    • ~/assets/images/... → resolved to ImageMetadata via Vite glob and optimized to /_astro/...
    • Root-relative /images/... and other strings → rendered as plain <img>
    • Remote URLs on allowed domains → optimized via Unpic
  • Resolver contract: returns ImageMetadata or the original string (never null) so callers don’t silently render nothing.

Dev cache note: After editing this package, rebuild and restart consuming app dev servers so Vite picks up the new dist:

pnpm -C packages/astro-utils build
pnpm -C apps/<app> dev -- --force

Image Smoke Test Route (apps)

Each app includes /image-smoke-test with:

  • Optimized image via ~/assets/images/... (should produce /_astro/... src)
  • Public image via /... (plain <img>, no optimization)
  • Remote image on an allowed domain (Unpic)

Use this page to quickly verify image resolution and detect regressions or stale dev caches.

Complete Astro Configuration

// astro.config.ts or astro.config.mjs
import {
  readingTimeRemarkPlugin,
  responsiveTablesRehypePlugin,
  lazyImagesRehypePlugin,
  remarkWikiLink,
  remarkEmDashCodeBlocks,
  remarkSmartQuotesHeadings,
  remarkRemoveDuplicateFirstHeading,
  remarkSmartQuotesUserBlocks,
  remarkSmartQuotesQuoteBlocks,
  remarkSubtitlePrefix,
  remarkParenthesizedLinks,
  remarkPdfLinks,
  remarkStudyLinks
} from '@jaydixit/astro-utils';
import remarkSmartypants from 'remark-smartypants';

export default defineConfig({
  integrations: [
    mdx({
      remarkPlugins: [
        // Reading time must come first
        readingTimeRemarkPlugin,
        
        // Smart quotes processing (order matters!)
        remarkSmartQuotesUserBlocks,
        remarkSmartQuotesQuoteBlocks,
        [remarkSmartypants, {
          dashes: 'oldschool',
          backticks: false,
          ellipses: false,
          quotes: true
        }],
        remarkEmDashCodeBlocks,
        remarkSmartQuotesHeadings,
        
        // Content transformations
        remarkSubtitlePrefix,
        remarkRemoveDuplicateFirstHeading,
        remarkParenthesizedLinks,
        remarkPdfLinks,
        remarkStudyLinks,
        
        // Wiki links must come last
        remarkWikiLink
      ],
      rehypePlugins: [
        responsiveTablesRehypePlugin,
        lazyImagesRehypePlugin
      ],
    })
  ]
});

TableOfContents Component

A shared table of contents component with two variants for different use cases.

---
import TableOfContents from '@jaydixit/astro-utils/components/TableOfContents.astro';

// Get headings from your content
const { headings } = await post.render();
---

<!-- Classic variant: sticky sidebar with border indicator -->
<TableOfContents
  headings={headings}
  maxDepth={2}
  variant="classic"
  class="lg:w-64"
/>

<!-- Socratic variant: fixed sidebar with brace indicator -->
<TableOfContents
  headings={headings}
  maxDepth={3}
  variant="socratic"
/>

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | headings | Heading[] | required | Array of { depth, slug, text } from Astro's render() | | maxDepth | number | 2 | Maximum heading depth to show (2 = H1+H2) | | variant | 'classic' \| 'socratic' | 'classic' | Visual style variant | | label | string | 'Table of Contents' | Accessible label | | id | string | 'toc-sidebar' | Element ID | | class | string | '' | Additional CSS classes |

Variant features:

  • classic: Sticky positioning, smooth scroll on click, left border active indicator
  • socratic: Fixed positioning, auto-scroll TOC to active item, curly brace indicator, hides when overlapping footer

Both variants include:

  • Active link highlighting based on scroll position
  • Dark mode support
  • Responsive hiding on smaller screens
  • Emoji stripping from heading text

ImageGallery Component

---
// In your .astro file
import ImageGallery from '@jaydixit/astro-utils/components/ImageGallery.astro';
---

<!-- Basic usage with external images -->
<ImageGallery
  images={[
    { src: "/image1.jpg", alt: "Image 1" },
    { src: "/image2.jpg", alt: "Image 2" }
  ]}
  galleryId="my-gallery"
  columns={3}
/>
---
// In your .mdx file
import ImageGallery from '@jaydixit/astro-utils/components/ImageGallery.astro';
import img1 from './assets/image1.jpg';
import img2 from './assets/image2.jpg';
---

<!-- Using imported images in MDX -->
<ImageGallery
  images={[
    { src: img1, alt: "Description 1" },
    { src: img2, alt: "Description 2" }
  ]}
  galleryId="gallery-2634"
  columns={2}
/>

Typography Utilities

---
// Smart quotes for dynamic content
import { smartQuotes } from '@jaydixit/astro-utils';

const title = '"Building better websites"';
const processedTitle = smartQuotes(title); // "Building better websites"
---

<h1>{processedTitle}</h1>
<p>{smartQuotes("It's a beautiful day — let's code!")}</p>

StructuredData Component

---
// In your Layout.astro
import StructuredData from '@jaydixit/astro-utils/components/StructuredData.astro';

const structuredData = {
  "@type": "WebSite",
  name: "My Website",
  url: "https://example.com"
};
---

<head>
  <StructuredData data={structuredData} />
</head>

LinkPeek Component

---
// Enable link previews in your content
import LinkPeek from '@jaydixit/astro-utils/components/LinkPeek.astro';
---

<!-- In blog post layout -->
<Content components={{ LinkPeek, ImageGallery }} />

CSS Styles

/* In your global CSS file */
@import '@jaydixit/astro-utils/styles/blocks.css';

Working with Content Collections

---
// Proper typing for content collections
import type { CollectionEntry } from 'astro:content';
type BlogPost = CollectionEntry<'blog'>;

const { post }: { post: BlogPost } = Astro.props;
---

<article>
  <h1>{smartQuotes(post.data.title)}</h1>
  <time datetime={post.data.publishDate.toISOString()}>
    {getFormattedDate(post.data.publishDate)}
  </time>
</article>

Blog Filtering and Visibility Control

Control where your blog posts appear with advanced filtering utilities:

// Import filtering utilities
import { 
  filterMainBlogPosts,
  filterSpecializedBlogPosts,
  isPostHidden,
  isPostSpecialized,
  normalizeTags
} from '@jaydixit/astro-utils';

// Get posts from content collection
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');

// Filter for main blog index (excludes draft and visibility-restricted posts)
const mainBlogPosts = filterMainBlogPosts(allPosts);

// Filter for specialized pages (e.g., /examples/)
const examplePosts = filterSpecializedBlogPosts(allPosts, 'example');

// Check individual post visibility
const isHidden = isPostHidden(post); // true if draft or visibility="hidden"
const isSpecialized = isPostSpecialized(post); // true if has visibility value

Content Schema Setup

Configure your content collection schema to support visibility control:

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    excerpt: z.string(),
    publishDate: z.date(),
    // Support both single string and array formats for tags
    tags: z.union([z.string(), z.array(z.string())]).optional(),
    // Visibility control
    visibility: z.string().optional(),
    draft: z.boolean().optional(),
    // ... other fields
  }),
});

Visibility Options

  • No visibility field - Post appears everywhere (default)
  • visibility: "hidden" - Hidden from all listings (still accessible via direct URL)
  • visibility: "example" - Hidden from main blog, shown on /examples/ page
  • draft: true - Excluded from all production listings

Example: Creating Specialized Pages

---
// pages/examples/index.astro
import { getCollection } from 'astro:content';
import { filterSpecializedBlogPosts } from '@jaydixit/astro-utils';

// Get all posts filtered for "example" visibility or tag
const allPosts = await getCollection('blog');
const examplePosts = filterSpecializedBlogPosts(allPosts, 'example');
---

<h1>Examples</h1>
{examplePosts.map(post => (
  <article>
    <h2>{post.data.title}</h2>
    <p>{post.data.excerpt}</p>
  </article>
))}

Tag Normalization

The utility handles both single string and array tag formats:

import { normalizeTags } from '@jaydixit/astro-utils';

// Works with both formats
normalizeTags("tutorial");              // Returns: ["tutorial"]
normalizeTags(["tutorial", "advanced"]); // Returns: ["tutorial", "advanced"]
normalizeTags(undefined);                // Returns: []

Workspace Configuration

// In your app's package.json
{
  "dependencies": {
    "@jaydixit/astro-utils": "workspace:*"
  }
}

Common Patterns

Blog Post with All Features

---
// pages/blog/[slug].astro
import { smartQuotes } from '@jaydixit/astro-utils';
import ImageGallery from '@jaydixit/astro-utils/components/ImageGallery.astro';
import LinkPeek from '@jaydixit/astro-utils/components/LinkPeek.astro';
import type { CollectionEntry } from 'astro:content';

type BlogPost = CollectionEntry<'blog'>;
const { post }: { post: BlogPost } = Astro.props;
const { Content } = await post.render();
---

<article>
  <header>
    <h1>{smartQuotes(post.data.title)}</h1>
    {post.data.subtitle && (
      <p class="subtitle">{smartQuotes(post.data.subtitle)}</p>
    )}
  </header>
  
  <Content components={{ LinkPeek, ImageGallery }} />
</article>

Hero Component with Smart Quotes

---
import { smartQuotes } from '@jaydixit/astro-utils';

interface Props {
  title?: string;
  subtitle?: string;
  tagline?: string;
}

const { title, subtitle, tagline } = Astro.props;
---

<section class="hero">
  {title && <h1>{smartQuotes(title)}</h1>}
  {subtitle && <h2>{smartQuotes(subtitle)}</h2>}
  {tagline && <p>{smartQuotes(tagline)}</p>}
</section>

Plugin Order Matters!

The order of remark plugins is critical for proper processing:

  1. readingTimeRemarkPlugin - Must come first
  2. Smart quotes plugins - Process different quote contexts
  3. remarkSmartypants - General smart typography
  4. Content transformations - Headings, subtitles, etc.
  5. remarkStudyLinks - Convert [[study:...]] to AcademicReference
  6. remarkWikiLink - Must come last to avoid conflicts

Working with Images

Setting Up the ~ Alias

The ~ alias provides a clean way to import from your src directory. Configure it in your tsconfig.json:

// tsconfig.json in each app
{
  "extends": "astro/tsconfigs/base",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": ["src/*"],
      "astro-utils/*": ["../../packages/astro-utils/src/*"]
    }
  }
}

Image Organization

Organize your images in a consistent structure:

apps/[app-name]/src/assets/
├── images/
│   ├── posts/           # Blog post images
│   │   ├── [post-slug]/ # Individual post folders
│   │   └── shared/      # Shared across posts
│   ├── authors/         # Author headshots
│   └── backgrounds/     # Background images
└── icons/               # SVG icons

Import Patterns in .astro Files

---
// Basic image imports in .astro files
import { Image } from 'astro:assets';
import heroImage from '~/assets/images/posts/my-post/hero.jpg';
import authorImage from '~/assets/images/authors/jay-dixit-512.png';

// Multiple related images
import visionIcon from '~/assets/images/plant-grow.png';
import humanApproachIcon from '~/assets/images/new-creativity-brain.png';
import neuralNetBg from '~/assets/images/backgrounds/neural-net.png';
---

<!-- Using Astro's Image component -->
<Image src={heroImage} alt="Hero image description" />

<!-- With additional attributes -->
<Image 
  src={authorImage} 
  alt="Jay Dixit"
  width={512}
  height={512}
  class="rounded-full"
/>

Import Patterns in .mdx Files

---
title: My Blog Post
author: Jay Dixit
# Reference images in frontmatter using ~ alias
authorImage: ~/assets/images/authors/jay-dixit-512.png
image: ~/assets/images/posts/my-post/hero.jpg
imageAlt: Descriptive alt text for hero image
---

{/* Import images after frontmatter */}
import { Image } from 'astro:assets';
import hero from '~/assets/images/posts/my-post/hero.jpg';

{/* For ImageGallery component */}
import ImageGallery from '@jaydixit/astro-utils/components/ImageGallery.astro';
import img1 from '~/assets/images/posts/my-post/image-1.jpg';
import img2 from '~/assets/images/posts/my-post/image-2.png';
import img3 from '~/assets/images/posts/my-post/image-3.webp';

# My Blog Post

<Image src={hero} alt="Hero image" />

Regular content here...

<ImageGallery
  images={[
    { src: img1, alt: "First image description" },
    { src: img2, alt: "Second image description" },
    { src: img3, alt: "Third image description" }
  ]}
  galleryId="unique-gallery-id"
/>

Complex Gallery Example (from gallery-test-rigorous.mdx)

---
title: Gallery Test
# Image in frontmatter
image: ~/assets/images/posts/gallery-test/hero.png
imageAlt: Hero image description
---

import { Image } from 'astro:assets';
import ImageGallery from '@jaydixit/astro-utils/components/ImageGallery.astro';

{/* Import multiple images for galleries */}
import massimoImgA from '~/assets/images/posts/massimo-dutti/outfit-1.jpg';
import massimoImgB from '~/assets/images/posts/massimo-dutti/outfit-2.jpg';
import boggiImage from '~/assets/images/posts/boggi-storefront.webp';
import massimoNews01 from '~/assets/images/posts/massimo-dutti/editorial.jpg';
import qrCode from '~/assets/images/posts/gallery-test/QR1-0943.jpg';

{/* Mixed format imports (JPG, PNG, WebP) */}
import portrait from '~/assets/images/posts/gallery-test/portrait.jpg';
import screenshot from '~/assets/images/posts/gallery-test/screenshot.png';
import optimized from '~/assets/images/posts/gallery-test/optimized.webp';

# Multi-Column Gallery (5 images)

<ImageGallery
  images={[
    { src: massimoImgA, alt: "Massimo Dutti outfit 1" },
    { src: massimoImgB, alt: "Massimo Dutti outfit 2" },
    { src: boggiImage, alt: "Boggi Milano storefront" },
    { src: massimoNews01, alt: "Massimo Dutti editorial" },
    { src: qrCode, alt: "QR code test" }
  ]}
  galleryId="gallery-389"
  columns={3}
/>

# Mixed Content: Gallery + Individual Images

{/* 2-image gallery */}
<ImageGallery
  images={[
    { src: portrait, alt: "Portrait photo" },
    { src: screenshot, alt: "Screenshot" }
  ]}
  galleryId="gallery-2634"
  columns={2}
/>

{/* Individual image (not in gallery) */}
<Image src={optimized} alt="Standalone optimized image" />

Importing from Shared Folders

{/* Import from shared folder for reusable images */}
import hero from '~/assets/images/posts/shared/cherry-blossom.png';
import defaultAuthor from '~/assets/images/authors/default-avatar.png';

{/* Cross-post image sharing */}
import sharedDiagram from '~/assets/images/posts/shared/venn-diagram.png';

Image Format Support

Astro supports these image formats:

  • Static formats: .jpg, .jpeg, .png, .webp, .gif, .svg, .avif
  • Animated: .gif (preserved), .webp (animated)
  • Vector: .svg (passed through unchanged)
---
// Different format examples
import jpgImage from '~/assets/images/photo.jpg';
import pngImage from '~/assets/images/diagram.png';
import webpImage from '~/assets/images/optimized.webp';
import svgIcon from '~/assets/images/icon.svg';
---

<!-- Each format handled appropriately -->
<Image src={jpgImage} alt="JPEG photo" />
<Image src={pngImage} alt="PNG with transparency" />
<Image src={webpImage} alt="WebP optimized" />
<img src={svgIcon.src} alt="SVG icon" /> <!-- SVG as regular img -->

Logo Component Example

---
// components/Logo.astro
import { Image } from 'astro:assets';
import logoImage from '~/assets/images/logo.png';
---

<div class="logo">
  <Image 
    src={logoImage} 
    alt="Company Logo"
    width={200}
    height={60}
    loading="eager"
  />
</div>

Common Patterns and Best Practices

  1. Consistent Naming: Use descriptive, kebab-case filenames

    ✅ jay-dixit-headshot.jpg
    ✅ socratic-ai-logo.png
    ❌ IMG_1234.jpg
    ❌ untitled.png
  2. Folder Organization: Group related images

    posts/worlds-best-cities/cascais-portugal.jpg
    posts/worlds-best-cities/tokyo-skyline.jpg
    posts/shared/default-hero.png
  3. Alt Text: Always provide meaningful alt text

    <Image src={heroImage} alt="Sunset over Cascais beach in Portugal" />
  4. Image Optimization: Let Astro handle it

    <!-- Astro automatically optimizes this -->
    <Image src={largePhoto} alt="Description" />
       
    <!-- Specify dimensions for better performance -->
    <Image src={largePhoto} alt="Description" width={800} height={600} />
  5. Frontmatter vs Imports:

    • Use frontmatter for metadata (hero images, og:image)
    • Use imports for images rendered in content

Tips and Best Practices

  • Always import from 'astro-utils' not relative paths
  • Use workspace:* for monorepo dependencies
  • Apply smart quotes to all user-facing text
  • Test plugin combinations when adding new ones
  • Use proper TypeScript types for content collections
  • Organize images in consistent folder structures
  • Always use the ~ alias for src-relative imports
  • Provide descriptive alt text for all images
  • Use the smoke test route to validate image behavior after changes

License

MIT

SEO Helpers

Build consistent meta, OpenGraph, and Twitter card data across apps.

import { createSEOHelpers, adaptOpenGraphImages } from '@jaydixit/astro-utils';

const helpers = createSEOHelpers({
  siteName: 'My Site',
  baseUrl: 'https://example.com',
  locale: 'en_US',
  twitterHandle: '@example',
  titleTemplate: '%s | My Site',
  defaultImage: '/img/default.png',
  defaultImageAlt: 'Default image'
});

const meta = helpers.buildMeta({
  title: 'Page Title',
  description: 'Desc',
  canonicalPath: '/page',
});

let og = helpers.buildOpenGraph({
  title: meta.title,
  description: meta.description,
  image: '/img/og.png',
  canonicalUrl: meta.canonical,
  type: 'article',
});
og = await adaptOpenGraphImages(og, new URL('https://example.com'));

const twitter = helpers.buildTwitter({ title: meta.title, description: meta.description, image: '/img/og.png' });

Related Posts Scoring

Centralize relevance scoring based on tag/category overlap.

import { scoreRelated } from '@jaydixit/astro-utils';

const related = scoreRelated(allPosts, currentPost, {
  tagWeight: 2,
  categoryWeight: 1,
  minScore: 2,
  limit: 4,
});

Notes:

  • Posts should have { slug, category?: { slug }, tags?: Array<{ slug }> }.
  • Returns up to limit items, with fallback fill if fewer meet minScore.