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

@nuasite/cms

v0.14.1

Published

Astro integration for inline visual editing with a built-in local dev server.

Readme

@nuasite/cms

Astro integration that adds inline visual editing to any Astro site. Scans your components, marks editable elements with CMS IDs, and serves a live editor overlay during development. All write operations (text, images, colors, components, markdown) are handled locally via a built-in dev server — no external backend required.

Prerequisites

  • Tailwind CSS v4 — Your site must use Tailwind. The CMS color editor, text styling, and class-based editing features all operate on Tailwind utility classes. Without Tailwind, those features won't work.

Quick Start

// astro.config.mjs
import nuaCms from '@nuasite/cms'

export default defineConfig({
	integrations: [nuaCms()],
})

Run astro dev and the CMS editor loads automatically. Edits write directly to your source files, and Vite HMR picks up the changes instantly.

How It Works

The integration operates in two phases:

HTML Processing — As Astro renders each page, the integration intercepts the HTML response, parses it, and injects data-cms-id attributes on editable elements (text, images, components). It generates a per-page manifest mapping each CMS ID to its source file, line number, and code snippet.

Dev Server API — When you save an edit in the visual editor, the request goes to /_nua/cms/* endpoints running inside Vite's dev middleware. These handlers read the source file, find the snippet, apply the change, and write the file back. Vite HMR triggers a reload.

Options

nuaCms({
	// --- Editor ---
	src: undefined, // Custom editor script URL (default: built-in @app/cms bundle)
	cmsConfig: { // Passed to window.NuaCmsConfig
		apiBase: '/_nua/cms', // API endpoint base (auto-set when using local dev server)
		highlightColor: undefined,
		debug: false,
		theme: undefined,
		themePreset: undefined,
	},

	// --- Backend ---
	proxy: undefined, // Proxy /_nua requests to a remote backend (e.g. 'http://localhost:8787')
	// When set, the local dev server API is disabled
	media: undefined, // Media storage adapter (default: localMedia() when no proxy)

	// --- Marker ---
	attributeName: 'data-cms-id',
	includeTags: null, // null = all tags
	excludeTags: ['html', 'head', 'body', 'script', 'style'],
	includeEmptyText: false,
	generateManifest: true,
	manifestFile: 'cms-manifest.json',
	markComponents: true,
	componentDirs: ['src/components'],
	contentDir: 'src/content',
	seo: { trackSeo: true, markTitle: true, parseJsonLd: true },
})

Dev Server API

When no proxy is configured, the integration spins up a local API at /_nua/cms/. This handles all CMS operations without needing the Cloudflare Worker backend.

| Method | Path | Description | | ------- | ----------------------------- | ------------------------------------------------------ | | POST | /_nua/cms/update | Save text, image, color, and attribute changes | | POST | /_nua/cms/insert-component | Insert a component before/after a reference | | POST | /_nua/cms/remove-component | Remove a component from the page | | GET | /_nua/cms/markdown/content | Read markdown file content + frontmatter | | POST | /_nua/cms/markdown/update | Update markdown file (partial frontmatter merge) | | POST | /_nua/cms/markdown/create | Create a new markdown file in a collection | | GET | /_nua/cms/media/list | List uploaded media files | | POST | /_nua/cms/media/upload | Upload a file (multipart/form-data) | | DELETE | /_nua/cms/media/:id | Delete an uploaded file | | GET | /_nua/cms/deployment/status | Returns { currentDeployment: null, pendingCount: 0 } | | OPTIONS | /_nua/cms/* | CORS preflight |

Update Payload

The POST /update endpoint accepts a batch of changes:

{
  changes: [
    {
      cmsId: 'cms-0',
      newValue: 'Updated heading text',
      originalValue: 'Original heading text',
      sourcePath: 'src/pages/index.astro',
      sourceLine: 42,
      sourceSnippet: '<h1>Original heading text</h1>',
      // Optional for specific change types:
      styleChange: { oldClass: 'bg-blue-500', newClass: 'bg-red-500', type: 'bg' },
      imageChange: { newSrc: '/uploads/photo.webp', newAlt: 'A photo' },
      attributeChanges: [{ attributeName: 'href', oldValue: '/old', newValue: '/new' }],
    }
  ],
  meta: { source: 'cms-editor', url: 'http://localhost:4321/about' }
}

Changes are grouped by source file, sorted by line number (descending to avoid offset shifts), and applied in-place. The response returns { updated: number, errors?: [...] }.

Media Storage Adapters

Media uploads use a pluggable adapter pattern. Three adapters are included:

Contember (R2 + Database) — Recommended

Files are stored in Cloudflare R2 with metadata tracked in the Contember database. This is the only adapter that gives you proper asset IDs, metadata, and AI-powered image annotation. Use this for production sites.

import nuaCms, { contemberMedia } from '@nuasite/cms'

nuaCms({
	media: contemberMedia({
		apiBaseUrl: 'https://api.example.com',
		projectSlug: 'my-project',
		sessionToken: process.env.NUA_SESSION_TOKEN,
	}),
})

This adapter calls the worker's /cms/:projectSlug/media/* endpoints, which handle R2 upload, Asset record creation, and image annotation. Authentication uses the NUA_SITE_SESSION_TOKEN cookie.

Local Filesystem (default)

Stores files in public/uploads/. Served directly by Vite's static file server. Zero configuration needed. Files are committed to your repo alongside your source code.

import nuaCms, { localMedia } from '@nuasite/cms'

nuaCms({
	media: localMedia({
		dir: 'public/uploads', // default
		urlPrefix: '/uploads', // default
	}),
})

Files are named with UUIDs to avoid collisions. Listed by modification time (newest first).

S3 / R2 Direct

Direct S3-compatible object storage. Works with AWS S3, Cloudflare R2, MinIO, or any S3-compatible provider. Listing, uploading, and deleting all work, but there is no database layer — content types are not preserved on list, and there are no image dimensions or annotations. Requires @aws-sdk/client-s3 as a peer dependency.

import nuaCms, { s3Media } from '@nuasite/cms'

nuaCms({
	media: s3Media({
		bucket: 'my-bucket',
		region: 'us-east-1',
		// Optional:
		accessKeyId: process.env.AWS_ACCESS_KEY_ID,
		secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
		endpoint: 'https://account.r2.cloudflarestorage.com', // for R2
		cdnPrefix: 'https://cdn.example.com', // public URL prefix
		prefix: 'uploads', // key prefix in bucket
	}),
})

Install the optional dependency:

npm install @aws-sdk/client-s3

Custom Adapter

Implement the MediaStorageAdapter interface to use any storage backend:

import type { MediaStorageAdapter } from '@nuasite/cms'

const myAdapter: MediaStorageAdapter = {
	async list(options) {
		// Return { items: MediaItem[], hasMore: boolean, cursor?: string }
	},
	async upload(file: Buffer, filename: string, contentType: string) {
		// Return { success: boolean, url?: string, filename?: string, id?: string, error?: string }
	},
	async delete(id: string) {
		// Return { success: boolean, error?: string }
	},
}

nuaCms({ media: myAdapter })

Proxy Mode

To use the Contember worker backend for all CMS operations (not just media), set the proxy option. This disables the local dev server API and forwards all /_nua requests to the target:

nuaCms({
	proxy: 'http://localhost:8787', // Worker dev server
})

In proxy mode, the integration only handles HTML processing and manifest serving. All write operations go through the worker (which uses GitHub API for commits and R2 for media).

Content Collections

The integration auto-detects Astro content collections in src/content/. For each collection:

  • Scans all .md/.mdx files to infer a field schema from frontmatter
  • Marks collection pages with a wrapper element for body editing
  • Provides markdown CRUD endpoints for creating/updating entries
  • Parses frontmatter with yaml (no gray-matter dependency needed)

Component Operations

Components in componentDirs (default: src/components/) are scanned for props and registered as insertable/removable elements. The editor can:

  • Insert a component before or after any existing component on the page
  • Remove a component from the page

Both operations find the invocation site (the page file, not the component file itself), locate the correct JSX tag using occurrence indexing, and modify the source with proper indentation.

PostMessage API (Iframe Communication)

When the editor runs inside an iframe, it sends postMessage events to the parent window. Listen for them with:

window.addEventListener('message', (event) => {
	const msg = event.data // CmsPostMessage
	switch (msg.type) {
		case 'cms-ready': /* ... */
			break
		case 'cms-state-changed': /* ... */
			break
		case 'cms-page-navigated': /* ... */
			break
		case 'cms-element-selected': /* ... */
			break
		case 'cms-element-deselected': /* ... */
			break
	}
})

All message types are exported as TypeScript interfaces:

import type {
	CmsPostMessage,
	CmsReadyMessage,
	CmsStateChangedMessage,
} from '@nuasite/cms'

cms-ready

Sent once when the manifest loads for the first time. Contains the full page context:

| Field | Type | Description | | ---------------------------- | --------------------------------------- | ------------------------------------------------ | | data.pathname | string | Current page URL pathname | | data.pageTitle | string? | Page title from SEO data or pages array | | data.seo | PageSeoData? | Full SEO metadata (title, description, OG, etc.) | | data.pages | PageEntry[]? | All site pages with pathname and title | | data.collectionDefinitions | Record<string, CollectionDefinition>? | Content collections with inferred schemas | | data.componentDefinitions | Record<string, ComponentDefinition>? | Registered component definitions | | data.availableColors | AvailableColors? | Tailwind color palette | | data.availableTextStyles | AvailableTextStyles? | Tailwind text style options | | data.metadata | ManifestMetadata? | Manifest version, build ID, content hash |

cms-state-changed

Sent whenever editor state changes (editing mode, dirty counts, deployment, undo/redo):

| Field | Type | Description | | --------------------------------- | ------------------------------ | --------------------------------- | | state.isEditing | boolean | Whether edit mode is active | | state.hasChanges | boolean | Whether any unsaved changes exist | | state.dirtyCount.text | number | Pending text changes | | state.dirtyCount.image | number | Pending image changes | | state.dirtyCount.color | number | Pending color changes | | state.dirtyCount.bgImage | number | Pending background image changes | | state.dirtyCount.attribute | number | Pending attribute changes | | state.dirtyCount.seo | number | Pending SEO changes | | state.dirtyCount.total | number | Total pending changes | | state.deployment.status | DeploymentStatusType \| null | Current deployment status | | state.deployment.lastDeployedAt | string \| null | ISO timestamp of last deployment | | state.canUndo | boolean | Whether undo is available | | state.canRedo | boolean | Whether redo is available |

cms-page-navigated

Sent when the manifest reloads after navigating to a different page:

| Field | Type | Description | | --------------- | --------- | ----------------- | | page.pathname | string | New page pathname | | page.title | string? | Page title |

cms-element-selected

Sent when the user hovers or clicks a CMS element. Contains full element metadata from the manifest including sourcePath, sourceLine, sourceSnippet, sourceHash, stableId, contentPath, image/color/attribute data, and component instance info.

cms-element-deselected

Sent when no element is hovered. No additional data.

Inbound Messages (Parent → Iframe)

The parent window can send commands to the editor iframe using postMessage:

const iframe = document.querySelector('iframe')

// Deselect the currently selected component
iframe.contentWindow.postMessage({ type: 'cms-deselect-element' }, '*')

All inbound message types are exported as TypeScript interfaces:

import type { CmsDeselectElementMessage, CmsInboundMessage } from '@nuasite/cms'

cms-deselect-element

Deselects the currently selected component and closes the block editor. No additional data required.

Exports

// Default export
import nuaCms from '@nuasite/cms'

// Media adapters
import { contemberMedia, localMedia, s3Media } from '@nuasite/cms'

// Types
import type { MediaItem, MediaStorageAdapter } from '@nuasite/cms'
import type {
	CmsManifest,
	ComponentDefinition,
	ManifestEntry,
} from '@nuasite/cms'

// Utilities
import { getProjectRoot, scanCollections, setProjectRoot } from '@nuasite/cms'
import { findCollectionSource, parseMarkdownContent } from '@nuasite/cms'