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

@nexuscontent/core

v0.2.10

Published

An open source, framework neutral content access layer for JavaScript and TypeScript applications.

Readme

NexusContent

An open source, framework neutral content access layer for JavaScript and TypeScript applications.

NexusContent provides a consistent API for retrieving normalized content from external sources — Git managed content, WordPress, Strapi, and future providers. It sits between content sources and applications so your frontend never depends directly on a CMS.

Consumers own the application. Content providers own the content. Deployment infrastructure owns delivery.

Why NexusContent

Developers want Git, static builds, and predictable deployments. Content editors want forms, media management, and no code access. NexusContent keeps these concerns separate — your application consumes normalized content without depending on which CMS, framework, or hosting platform is in use.

Content Sources → NexusContent → Consumer Build → Deployment

The same application should be able to switch from WordPress to Git content without a frontend rewrite.

Install

npm install @nexuscontent/core

Quick Start

import { NexusContent } from "@nexuscontent/core";

const nexus = new NexusContent(config);

const page = await nexus.getPage("about");
const projects = await nexus.getCollection("projects");
const project = await nexus.getItem("projects", "project-one");

Works identically in Astro, Next.js, plain Node scripts, or any JavaScript runtime.

Providers

| Provider | Status | Source | |----------|--------|--------| | Git | Implemented | JSON content directories | | WordPress | Implemented | WordPress REST API | | Strapi | Planned | Strapi REST API |

Git Provider

Content lives in a separate repository. Point NexusContent at it:

import { defineNexusConfig, NexusContent } from "@nexuscontent/core";

const nexus = new NexusContent(
  defineNexusConfig({
    providers: {
      content: {
        type: "git",
        options: { contentPath: "../client-content" }
      }
    },
    schema: {
      models: {
        home: {
          kind: "singleton",
          source: { provider: "content", key: "home" },
          fields: {
            hero: {
              type: "object",
              fields: {
                heading: { type: "string", required: true },
                intro: { type: "string" }
              }
            }
          }
        },
        about: {
          kind: "singleton",
          source: { provider: "content", key: "about" }
        }
      }
    }
  })
);

Content structure:

client-content/
├── pages/
│   ├── home.json
│   └── about.json
├── collections/
│   └── team/
├── navigation/
│   └── main.json
└── settings/
    └── site.json

WordPress Provider

import {
  defineNexusConfig,
  NexusContent,
  WordPressMediaProvider,
  WordPressProvider
} from "@nexuscontent/core";

const wordpress = new WordPressProvider({
  baseUrl: "https://wordpress.example.com/wp-json/wp/v2"
});

const nexus = new NexusContent(
  defineNexusConfig({
    providers: {
      wordpress: {
        type: "wordpress",
        options: { baseUrl: "https://wordpress.example.com/wp-json/wp/v2" }
      }
    },
    schema: {
      models: {
        home: {
          kind: "singleton",
          source: { provider: "wordpress", key: "home" }
        },
        posts: {
          kind: "collection",
          source: { provider: "wordpress", key: "posts" }
        }
      }
    }
  })
);

nexus.register("wordpress", wordpress);

const home = await nexus.getPage("home");
const posts = await nexus.getCollection("posts");

Model Schema

The schema.models contract declares where each logical model's content comes from and what its data should look like. Model kind selects the retrieval operation: singleton models route through getPage (Git pages/<key>.json), collection through getCollection, navigation through getNavigation, and settings through getSettings.

Field types are string, number, boolean, datetime, object, reference, media, richText, component, and blocks. Fields support required, list, options (enum strings), nested object.fields, reference.collection references, and media overrides. component fields reference declared schema.components; blocks fields validate a discriminated _type list against allowedComponents. Data is validated at retrieval time; a mismatch throws a SchemaError. Undeclared data fields pass through. defineNexusConfig() preserves literal model names and field declarations, so retrieval methods accept only compatible model kinds and infer each result's data shape without explicit generic parameters.

fields: {
  title: { type: "string", required: true },
  status: { type: "string", options: ["draft", "published"] },
  tags: { type: "string", list: true },
  cover: { type: "media" },
  author: { type: "reference", collection: "people" }
}

Page sections

Pages returned by getPage() carry the provider's ordered sections in two forms:

  • page.sections — a named map keyed by section type, for declarative template composition: <Hero {...page.sections.hero} />. Repeated section types are suffixed in order (hero, hero_2, hero_3).
  • page.sectionsList — the authoritative ordered { type, data } array, for renderers that iterate sections (<PostSections sections={page.sectionsList} />).

Both arrive identically whether the content comes from Git or a CMS provider. Git pages author the map directly as sections:{...} in pages/<key>.json (JSON key order is the section order); WordPress flexible/Gutenberg content is normalized to the same ordered list and projected to the map. A page model therefore never needs to declare the sections it uses — leave its fields off, or declare additional non-section fields:

// content/pages/about.json
{
  "key": "about",
  "sections": {
    "hero": { "heading": "We make content replaceable" },
    "rich_text": { "body": "<p>Origin story.</p>" }
  }
}

Media

Media references stay neutral. Providers normalize source media into MediaAsset where src is the URL source (migrated from the legacy url field in 0.2.2):

media: {
  default: "remote",
  providers: {
    local: { type: "local", options: { root: "../client-content/media", publicPath: "/media" } },
    remote: { type: "remote" }
  }
}

local maps root-relative paths to publicPath web URLs with traversal protection. remote validates absolute http(s) URLs without fetching. A WordPress media provider resolves ids through the WordPress media endpoint and is registered manually:

nexus.registerMedia("wordpress", new WordPressMediaProvider({ baseUrl }));
const asset = await nexus.media.resolve({ id: "9" }); // or { src: "..." }

For whole content structures (page sections maps, collection item data), nexus.media.resolveFields(value) recursively resolves every object that carries a src string into a plain { src, alt }, leaving everything else untouched — so pages resolve all section media before rendering:

const home = (await nexus.media.resolveFields(page.sections)) as HomeData;

When the provider yields no asset, the authored { src, alt } reference is kept; id-only references are not generically detectable and pass through.

WordPress Options

new WordPressProvider({
  baseUrl: "https://wordpress.example.com/wp-json/wp/v2", // required
  headers: { Authorization: `Bearer ${token}` },          // optional auth
  collections: { books: { endpoint: "books" } },          // custom post types
  perPage: 100,        // 1-100, default 100
  maxPages: 100,       // safety limit
  timeoutMs: 10000     // request timeout
});

With apiStrategy: "auto" or "companion", getSettings() prefers the companion plugin's public contract-v1 /settings route and falls back to native wp/v2/settings when that route is unavailable. Companion plugin 0.1.8 provides an ACF Pro/Secure Custom Fields option page for site settings; ACF Free and plugin-free installs use WordPress core values.

The same plugin release adds page/post SEO authoring. Gutenberg uses a document sidebar and ACF modes use a field group; both emit optional normalized seo on page responses. The provider maps companion image url values to MediaAsset.src.

WordPress Components and Synchronisation

Section vocabulary lives in one monorepo canonical file, integrations/wordpress/nexuscontent/sections.json; the PHP plugin registry and the generated sections.generated.ts both derive from it (npm run check:sections enforces freshness in CI).

At build time, validate your declared consumer components against the install:

const wordpress = await new WordPressProvider({
  baseUrl: "...",
  componentTypeMap: { servicesList: "features" } // rename bridge
});

// Throws wordpress/unknown-component for unresolvable names.
wordpress.validateComponents(schema.components);

// Serializable contract to push to a site (see below):
const contract = wordpress.projectComponentContract(schema); // { components, sectionTypes }

During auto/companion API strategy, the provider reconciles its effective registry against the site's live /schema — install-only sections extend the registry, and registry-only or conflicting sections surface as structured diagnostics (thrown in strict companion mode).

Push the project contract to a WordPress site to see expected-vs-installed drift on the plugin Dashboard:

curl -X POST "https://wordpress.example.com/wp-json/nexuscontent/v1/project-contract" \
  -H "X-WP-Nonce: <rest-nonce>" \
  -H "Content-Type: application/json" \
  -d '{"components":["hero","servicesList"],"sectionTypes":["hero","features"]}'

The route requires manage_options, stores only sanitized string arrays in nexuscontent_settings, and never reconfigures editor settings automatically. It lives outside the content wire contract, so no contractVersion negotiation applies.

The nexus-contract npm bin (shipped with @nexuscontent/core) drives both flows from a consumer project using an Application Password, and provides a config-backed contract workflow so the loop is repeatable and CI-checkable:

# Scaffold the workflow: writes nexus.contract.json (config) and sections.custom.json:
npx @nexuscontent/core nexus-contract init --schema src/schema/schema.ts --write wp-content/mu-plugins/nexuscontent-sections.php

# Re-derive and re-render after schema changes (reads the config; flags override):
npx @nexuscontent/core nexus-contract regenerate

# Classify without writing; fails if the contract references undefined sections:
npx @nexuscontent/core nexus-contract validate

# Scaffold a consumer-owned mu-plugin registering custom sections as ACF layouts:
npx @nexuscontent/core nexus-contract generate --schema src/schema/schema.ts --custom sections.custom.json --write wp-content/mu-plugins/nexuscontent-sections.php

# Post the consumer contract to the admin-only project-contract route:
npx @nexuscontent/core nexus-contract push --schema src/schema/schema.ts --api-root "$WORDPRESS_API_URL" --username admin-user --app-password "xxxx xxxx xxxx xxxx xxxx xxxx"

generate derives the { components, sectionTypes } contract from your own field schema through projectComponentContract, classifies each section type as installed, custom (emitted), or missing (fails), warns on unused custom declarations, and emits deterministic PHP registering the custom sections via nexuscontent_section_definitions so the companion plugin auto-creates their ACF flexible layouts and optional ACF blocks. Classification uses the site's live /schema when --api-root is given and otherwise falls back to the bundled offline vocabulary (scripts/sections.json). regenerate re-runs generate using the paths recorded by init (explicit flags override), and validate prints the same drift without writing. Generated mu-plugin code is owned by the consumer and must not be committed to this repository.

See docs/wordpress-companion.md for the project-facing definition of the companion plugin.

Preview

The companion plugin serves draft/scheduled content for single-use preview. An editor with edit_posts mints a short-lived, post-scoped token:

curl -X POST "https://wordpress.example.com/wp-json/nexuscontent/v1/preview-token" \
  -H "X-WP-Nonce: <rest-nonce>" \
  -d '{"postId":42}'
# => { "token": "<64-hex>", "expiresAt": "..." }

The public GET /nexuscontent/v1/preview/{token}/{id} route returns the normalized content envelope for a valid, matching, unexpired token — the token is the auth, so no session is required. Invalid, expired, or mismatched tokens return 401 and are revoked on use. The Gutenberg "Open frontend preview" button (gated by the plugin's preview_frontend_url setting) mints a token and opens a consumer preview route with ?token=...&id=.... The astro-wordpress example's preview.astro route is a static, consumer-owned preview that fetches the tokenized route and renders through the shared section components, emitting noindex so previews never leak into production.

Webhooks

The companion plugin can notify the consuming frontend of WordPress content changes. It is opt-in and configured on the plugin's Settings page (webhook_url, plus an optional shared secret stored in a separate option that is never echoed or logged). On page/post create, update, trash, or restore, the plugin POSTs a compact JSON payload carrying only change metadata (no full content):

{
  "event": "updated",
  "id": 42,
  "type": "page",
  "slug": "about",
  "status": "publish",
  "title": "About",
  "modifiedAt": "2026-08-31T12:00:00Z",
  "source": "wordpress"
}

When a shared secret is set, the request carries an X-NexusContent-Signature: sha256=<HMAC-SHA256 of the raw JSON body> header so the consumer can verify the request genuinely came from WordPress. Dispatch is outbound-only, best-effort, non-blocking, and never triggers rebuilds or other site mutations — the consumer verifies the signature and decides whether to act.

Editor Modes

Pages can use one of three editor modes. The companion plugin stores the mode per-page in WordPress, so different pages on the same site can use different modes:

| Mode | Requires | Description | |------|----------|-------------| | gutenberg | WordPress core | Native block editor (default) | | acf_fixed | ACF Free 6.2+ | Fixed Hero, Intro, CTA fields | | acf_flexible | ACF Pro 6.2+ | Flexible layouts for all 12 sections |

The provider resolves the mode per-page: per-page field → defaultEditorMode → editorMode fallback (defaults to "gutenberg"). With the companion plugin the mode is resolved server-side.

new WordPressProvider({
  baseUrl: "...",
  editorMode: "gutenberg",       // fallback, default "gutenberg"
  defaultEditorMode: "acf_fixed" // site-wide intermediate default
});

Localisation (Optional)

const nexus = new NexusContent(
  defineNexusConfig({
    locales: {
      default: "en",
      supported: ["en", "fr"],
      fallback: { fr: "en" }
    },
    // providers and a schema.models contract as usual
  })
);

const page = await nexus.getPage("about", { locale: "fr" });

Astro Usage

---
import { NexusContent } from "@nexuscontent/core";
import BaseLayout from "../layouts/BaseLayout.astro";

const nexus = new NexusContent(config);
const page = await nexus.getPage("about");
if (!page) throw new Error("About content was not found.");
---

<BaseLayout title={page.title}>
  <h1>{page.title}</h1>
  <Fragment set:html={page.data.content} />
</BaseLayout>

Content Types

interface PageContent<TData> {
  id: string;
  key: string;
  slug?: string;
  title?: string;
  seo?: SeoData;
  sections?: Record<string, unknown>;  // named map keyed by section type
  sectionsList?: ContentSection[];      // authoritative ordered sections
  data: TData;
  meta: { source: string; sourceId?: string; updatedAt?: string; locale?: string };
}

interface CollectionItem<TData> {
  id: string;
  key: string;
  slug?: string;
  title?: string;
  data: TData;
  meta: ContentMeta;
}

SEO

import { resolveSeo } from "@nexuscontent/core";

const seo = resolveSeo(
  { title: page.title, excerpt: page.data.excerpt, featuredImage: page.data.featuredImage },
  { siteTitle: "My Site", defaultImage: { src: "https://example.com/social.jpg" } }
);

Inline JSON-LD is serialized with the Core serializeJsonLd(value) helper, which escapes <, >, &, U+2028, and U+2029 so authored strings cannot break out of a <script type="application/ld+json"> tag. Rendering remains consumer-owned: the Astro examples keep their own NexusSeo component. For content-driven site identity (siteName, defaultImage) authored in a settings model, use the service method nexus.resolvePageSeo(input, options?). Canonical URLs are joined by Core's makeCanonicalUrl(baseUrl, pathname); the base URL itself stays deployment-owned per project (loaded from PUBLIC_SITE_URL in the examples) — Core never infers deployment URLs.

Key Concepts

  • Framework neutral — Core never imports Astro, Next.js, React, or any frontend framework
  • Normalized content — Every provider returns the same PageContent and CollectionItem shapes
  • Model contracts — schema.models declares sources and validates field data per logical model
  • Content provenance — Every result includes meta.source and meta.sourceId
  • Media — Provider-neutral MediaAsset and MediaReference with local, remote, and WordPress resolution
  • Validation — Runtime schema validation with Zod; project-level schemas where practical
  • Static first — Works at build time without a persistent server

Documentation

License

MIT