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

@duffcloudservices/cms

v0.13.8

Published

Vue 3 composables and Vite plugins for DCS CMS integration

Downloads

800

Readme

@duffcloudservices/cms

Vue 3 composables and Vite plugins for DCS (Duff Cloud Services) CMS integration.

Installation

# Using pnpm
pnpm add @duffcloudservices/cms

# Using npm
npm install @duffcloudservices/cms

# Using yarn
yarn add @duffcloudservices/cms

Peer Dependencies

This package requires:

  • vue ^3.4.0
  • @unhead/vue ^1.9.0

Quick Start

1. Configure Vite Plugins

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { dcsContentPlugin, dcsSeoPlugin } from '@duffcloudservices/cms/plugins'

export default defineConfig({
  plugins: [
    vue(),
    dcsContentPlugin(),
    dcsSeoPlugin()
  ]
})

For VitePress:

// .vitepress/config.ts
import { defineConfig } from 'vitepress'
import { dcsContentPlugin, dcsSeoPlugin } from '@duffcloudservices/cms/plugins'

export default defineConfig({
  vite: {
    plugins: [
      dcsContentPlugin(),
      dcsSeoPlugin()
    ]
  }
})

2. Set Environment Variables

# .env
# The API base URL. REQUIRED for the release-notes / site-version reads and the
# preview ribbon: as of the cms release after 0.13.4 there is no built-in production default -
# an unset value logs one warning and the reads go relative to the site's own
# origin (C-1062). The canonical site-deploy workflow sets it from
# `.dcs/site.yaml` `api_base_url`; local and preview builds set it here.
VITE_API_BASE_URL=https://api.duffcloudservices.com
VITE_TEXT_OVERRIDE_MODE=commit  # 'commit' (default) or 'runtime'

# Deprecated: the site is now resolved server-side from the request Host
# (or the dedicated Container App's DCS_SITE_SLUG). VITE_SITE_SLUG is no
# longer placed in request URLs; it is optional and used only as a local
# cache-key hint. Safe to omit.
# VITE_SITE_SLUG=your-site-slug

3. Use Composables

<script setup lang="ts">
import { useTextContent, useSEO } from '@duffcloudservices/cms'

// Text content with defaults
const { t } = useTextContent({
  pageSlug: 'home',
  defaults: {
    'hero.title': 'Welcome to Our Site',
    'hero.subtitle': 'Build amazing things with us',
    'cta.primary': 'Get Started'
  }
})

// SEO configuration
const { applyHead } = useSEO('home')
applyHead()
</script>

<template>
  <section class="hero">
    <h1>{{ t('hero.title') }}</h1>
    <p>{{ t('hero.subtitle') }}</p>
    <button>{{ t('cta.primary') }}</button>
  </section>
</template>

Composables

useTextContent

Provides text content management with build-time injection and optional runtime overrides.

import { useTextContent } from '@duffcloudservices/cms'

const {
  t,                    // (key: string, fallback?: string) => string
  texts,                // ComputedRef<Record<string, string>>
  overrides,            // Ref<Record<string, string>>
  isLoading,            // Ref<boolean>
  error,                // Ref<string | null>
  refresh,              // () => Promise<void>
  hasOverride,          // (key: string) => boolean
  hasBuildTimeContent,  // boolean
  mode                  // 'commit' | 'runtime'
} = useTextContent({
  pageSlug: 'home',
  defaults: {
    'hero.title': 'Default Title'
  },
  fetchOnMount: true,  // default: true (only matters in runtime mode)
  cacheTtl: 60000      // default: 60000ms
})

Content Resolution Order:

  1. Runtime API overrides (premium tier only)
  2. Build-time content from .dcs/content.yaml
  3. Hardcoded defaults passed to the composable

useSEO

Provides SEO configuration with meta tags, Open Graph, Twitter Cards, and JSON-LD.

import { useSEO } from '@duffcloudservices/cms'

const {
  config,         // ComputedRef<ResolvedPageSeo>
  applyHead,      // () => void   <- NO ARGUMENTS. See the head-authority contract.
  getSchema,      // () => object[]
  getCanonical,   // () => string
  hasBuildTimeSeo // boolean
} = useSEO('home', '/') // pageSlug, optional pagePath

// Re-assert the baked head for this route.
applyHead()

The head-authority contract (C-356)

.dcs/seo.yaml is the only writer of <title>, description, keywords, robots, canonical, og:*, twitter:* and the JSON-LD graph. applyHead() RE-ASSERTS that head at runtime; it never authors one, so it takes no arguments. Need a different value? Change it in .dcs/seo.yaml (or the portal SEO editor, which writes it).

A hardcoded value that currently matches the baked one is still a violation — the contract grades authority, not values. Full text, the measured evidence and the rejected alternatives: .docs/plans/dynamic-site-resolution/README.md § "The head-authority contract (C-356)".

Enforced here, not per-site: the () => void type (fails type-check), auditHeadContract() in src/seo/headContract.ts (fails the audit), and a runtime refusal that drops the override and keeps seo.yaml authoritative. Audit a real site with:

pnpm --filter @duffcloudservices/cms build
node cli/head-contract-audit.mjs --all --json out.json

Exemptions carry live claims (C-420). An entry in a site's .dcs/head-contract.json whose reason NAMES a host must also declare what that host does, and the audit fetches it (redirects read, never followed):

"site/src/views/HomeView.vue": {
  "reason": "…why the seo.yaml factory cannot own this head…",
  "hosts": { "example.com": { "status": 200, "robots": "noindex",
                              "canonicalHost": "other.example" } }
}

A named host with no claim, a claim with no observation, and a claim the live host contradicts all FAIL — --no-probe silences none of them. The measured case: an exemption asserted a host was a noindex cross-domain handoff while that host served index, follow and a self-canonical from a different SWA, and the audit was green because of the exemption.

useReleaseNotes

Fetches release notes from the DCS Portal API.

import { useReleaseNotes } from '@duffcloudservices/cms'

const {
  releaseNote,  // Ref<ReleaseNote | null>
  isLoading,    // Ref<boolean>
  error,        // Ref<string | null>
  refresh       // () => Promise<void>
} = useReleaseNotes('1.2.0')  // or 'latest'

useSiteVersion

Gets the current site version for footer badges.

import { useSiteVersion } from '@duffcloudservices/cms'

const {
  version,          // Ref<string | null>
  isLoading,        // Ref<boolean>
  releaseNotesUrl   // ComputedRef<string>
} = useSiteVersion()

Conversion capture (automatic — no site code)

Measures the money-moment. Importing this package installs it. One delegated, capture-phase listener on document classifies every link and form submit on the site and emits a site_interaction event.

There is nothing to mount. That is the point: this shipped as an opt-in (useConversionTracking) and was adopted by zero of eleven fleet sites, so on 2026-07-26 two independent reviews found the same hole — Bryan's Handyman's five tel: CTAs, the only conversion action the site has, emitting nothing at all. A capability a site must remember to switch on is a capability that is off.

Event schema

| Property | Value | |---|---| | interaction_type | booking · phone · email · form_submit · social · external · internal · button | | is_conversion | "true" for the first four. One predicate for owner reports. | | label | Visible/aria label of the clicked control, truncated to 120 chars | | href | Destination, query + fragment stripped. Contact schemes are redacted to tel:#<digest> | | href_scheme | tel: · sms: · mailto: · https: · … | | href_host | Destination host ('' for contact schemes and buttons) | | href_hash | Digest of the contact target — lets a report rank CTAs without storing a number | | page_path, host | Where the interaction happened | | capture_version | 2 |

No raw phone number or email address is ever emitted. The digest is a normalising convenience, not anonymisation — a site publishes two or three numbers, so treat it as "the contact string never reaches the analytics store", nothing stronger.

Where the events go. Two independent transports, and it will use whichever exist:

  1. GA4 — picked up automatically from window.gtag, which the deploy workflow injects whenever .dcs/site.yaml has a google_analytics_id. A site with GA4 configured measures conversions with no code change whatsoever.
  2. App Insights — attaches through a global, so no package depends on a telemetry SDK:
// After your App Insights instance is live. One line, no import.
window.__dcsConversionAttach?.((e) => appInsights.trackEvent(e))

@duffcloudservices/telemetry already does this inside initialize(). Clicks captured before a transport exists are buffered (bounded FIFO) and flushed on attach — deferring or lazy-loading the App Insights SDK must never silently zero a customer's booking numbers.

Per-site configuration — call the installer yourself at app entry; the first call wins:

import { installConversionCapture } from '@duffcloudservices/cms'

installConversionCapture({
  bookingHosts: ['stridethera.com', 'momence.com'], // on top of the fleet defaults
  bookingPaths: ['/book'],                          // self-hosted booking routes
})

It deliberately does nothing when there is no document (SSR/prerender), inside an iframe (the visual editor and preview surfaces — an editor's clicks are not leads), under Do Not Track, or when the page sets window.__dcsConversionOptOut = true / <html data-dcs-analytics="off">.

Double counting is guarded, not hoped for. Only one delegated listener may bind per document — a second start() refuses with a console warning rather than doubling every conversion — and a click on a form's submit control is dropped so the submit event can be the single source of truth (a click that fails validation is not a lead). This is the same discipline as trackPageView's auto-route refusal: C-288 measured 52% duplicate page views on a live site from exactly this class of mistake, and the portal reported them to the owner.

Do not add "sideEffects": false to this package. It would let a consumer's bundler drop the auto-install and silently restore the blind spot.

useConversionTracking / createConversionTracker remain exported for non-cms consumers and for tests.

Vite Plugins

dcsContentPlugin

Injects .dcs/content.yaml at build time.

import { dcsContentPlugin } from '@duffcloudservices/cms/plugins'

dcsContentPlugin({
  contentPath: '.dcs/content.yaml',  // default
  debug: false                        // default
})

dcsSeoPlugin

Injects .dcs/seo.yaml at build time and, opt-in, emits per-route static SEO.

import { dcsSeoPlugin } from '@duffcloudservices/cms/plugins'

dcsSeoPlugin({
  seoPath: '.dcs/seo.yaml',  // default
  debug: false,               // default

  // Vue-SPA per-route static SEO (VitePress sites leave this off):
  emitStaticHtml: true,           // emit dist/<route>/index.html with baked
                                  // <head> meta + JSON-LD, plus sitemap/robots/llms
  noindex: ['account', 'projects'], // auth-gated routes → robots noindex,nofollow
})

noindex is a predicate over pages.yaml, not a standalone directive. Every consumer asks it route-first (noindexSet.has(route.path) || …has(route.slug)), so an entry naming a page that is not in pages.yaml emits no document at all — the directive silently does nothing and the URL stays index, follow. Since C-416 that is a hard build failure (NoindexOrphanError, thrown from config() before a byte is emitted):

[dcs-seo] noindex entries matching NO page in the route manifest: "/homepage-clone-1", "account".
… Fix it by adding the page to pages.yaml …, or by deleting the stale entry.
Matchable keys are the manifest's paths and slugs: "/", "/services", "home", "services"

Notes: matching is raw, so '/services/' does not match a '/services' route (the emitter would not match it either, so blessing it would certify a no-op); matching runs against the full manifest, so an entry that is also in exclude is fine. There is no severity knob — the fix is always "add the page" or "delete the row".

Body prerender (crawler-visible body content)

When emitStaticHtml is on, the plugin also prerenders each indexable route's body so non-JS AI crawlers see the real page prose — not just <div id="app"></div>. After the per-route <head> is emitted, it drives the just-built SPA in headless Chromium (via the site's playwright), captures each route's rendered #app DOM, and splices it into the mount container. On the client, app.mount('#app') replaces that DOM (no hydration), so users are unaffected while crawlers get the body.

  • Default ON whenever emitStaticHtml is on — a cms bump enables it with no per-site edit. playwright is already a fleet devDependency; if it is absent the pass is a graceful no-op (head + JSON-LD still emitted).
  • noindex / auth-gated routes are never body-prerendered (they get a head-only file).
  • A route that crashes at render time fails the build loud rather than shipping a broken/empty body. Because it renders the PRODUCTION bundle, dev-only fallback defaults (e.g. sample reviews) stay off — no fabricated content is baked.
  • Per-site escape hatch (no code change, no cms republish): set prerenderBody: false at the root of .dcs/seo.yaml to disable body prerender for one site, or pass dcsSeoPlugin({ prerenderBody: false }). Preview builds skip it automatically.

SEO emission honesty rails (C-334)

Three build-time asserts against one failure class: a platform capability that exists, is silently undone downstream, and has nothing watching. All three are ON by default at severity error whenever emitStaticHtml is on, so a cms bump arms them fleet-wide with no per-site edit.

| Rail | Asserts | Kill-tested against | |------|---------|---------------------| | P1 head honesty | For every emitted route, the baked <title>/description equals the rendered ones after the app mounts | Live Iron Oak, 2026-07-27: baked Handyman, Carpentry & Home Repairs in SE Michigan \| Iron Oak vs rendered Our Services — 18/18 routes divergent | | P2 emitted-URL honesty | Every URL the factory publishes (JSON-LD logo/image, og:image, icon links, sitemap <loc>s, llms.txt links) resolves to what it promises | Live Iron Oak, 2026-07-27: Organization.logo → 200 text/html, byte-identical to the homepage | | P12 charset budget | <meta charset> lands inside the spec's first-1024-byte encoding-sniffing window — asserted straight out of dcsContentPlugin (C-414), with the emitStaticHtml hoist as a backstop | Live Iron Oak and Bryan's, 2026-07-27: charset at byte 910, 90 bytes of headroom past the declaration, no header-level charset backstop |

Why they behave the way they do:

  • P1 rides the prerender browser — the page is already being loaded, so the marginal cost is one page.evaluate() per route. If a site has opted out of body prerender the render still happens in observe-only mode (nothing is written to dist/), because a gate that quietly does not exist on some sites is worse than no gate. A clean site passes structurally: applyHead() produces byte-identical tags to the build-time emit. Since C-356 an override is impossible — the parameter is gone from the signature and a runtime argument is dropped — which is what makes "structurally clean" the default rather than an achievement. (Historically applyHead({ title }) used the override verbatim, skipping global.titleTemplate, so an override that "looked right" still diverged; that was the root cause of 93 divergences on KEPT and 18 more across three sites.) P1 remains the rail that judges the values; the source-side auditHeadContract() judges who is allowed to write them.
  • P2 splits by how certain the answer is. Same-origin assets are proven against dist/ with no network at all: an emitted https://site/logos/x.svg with no dist/logos/x.svg behind it means the host's SPA fallback will answer HTML, so it is a hard failure. Cross-origin assets are probed under a 24h cache, a 5s per-request timeout and a 20s total budget, where a definitive wrong answer (HTML where an image was promised, or a 4xx/5xx) fails the build and a non-answer (DNS, timeout, reset, budget exhausted) is only a warning. A gate that reds on a network hiccup gets disabled within a week and then protects nothing.
  • P12 fixes as well as asserts. The motion-token <style> was injected head-prepend, above the site's own <meta charset>; C-414 fixed that at the source — it now injects head (append), which keeps it above the bundled site CSS (so a per-site :root override still wins) while leaving the encoding declaration first in <head>. The emitStaticHtml path still hoists the declaration in the emitted shell (idempotent, moves nothing else, now a no-op on a stock shell) and then asserts the result — that hoist is the backstop for hand-rolled shells, and it never ran on VitePress sites, which is why the source fix was the one that reached the whole fleet. Also fix the header where the site's config lives — SWA currently answers Content-Type: text/html with no charset parameter, so there is no header-level backstop.

Escape hatches — deliberately impossible to use silently:

# .dcs/seo.yaml (portal-owned, in git, reviewable)
headHonesty:
  mode: warn                 # error (default) | warn | off
  allow: ['/legacy-route']   # every exemption is printed on every build
  checkDescription: true
urlHonesty:
  mode: error
  allow: ['https://cdn.example.com/known-missing.svg']
  network: true              # false ⇒ same-origin still enforced
charsetBudget: error
# Per-build override — prints in the build log, so it is never invisible
DCS_SEO_HEAD_HONESTY=warn DCS_SEO_URL_HONESTY=warn pnpm build
DCS_SEO_URL_HONESTY_NETWORK=off pnpm build   # offline CI

Verification (needs a browser; not part of pnpm test):

pnpm --filter @duffcloudservices/cms run verify:honesty-rails
# and, to see what a LIVE site's build will red on before bumping cms:
pnpm --filter @duffcloudservices/cms run probe:live-urls https://example.com

The served-document pass (C-417)

All three rails above assert against the candidate build — the dist/ on the machine that ran the build. Nothing re-asserted them against the document a browser receives, and that gap is how C-341's charset hoisted 913 -> 48 claim survived: true of a local build, false in production, where the served document has <meta charset> at byte 910 (measured 2026-07-31, KEPT re-review F3). The KEPT re-review estimated a post-deploy re-run would have caught it in 8 minutes.

Run the same rail primitives over the bytes the origin serves — after every deploy, and always before citing a rail's number as evidence:

# P2 + P12 on the served document (default)
pnpm --filter @duffcloudservices/cms run verify:served-honesty https://example.com --routes all

# ...and hold the CANDIDATE BUILD's recorded numbers against what shipped.
# `dist-honesty-report.json` is what DCS_SEO_HONESTY_REPORT wrote during the build.
DCS_SEO_HONESTY_REPORT=dist-honesty-report.json pnpm build
pnpm --filter @duffcloudservices/cms run verify:served-honesty https://example.com \
  --routes all --rails p1,p2,p12 --rendered --compare dist-honesty-report.json

Exit codes: 0 pass · 1 a rail FAILED or the recorded evidence does not describe the served document · 3 a requested rail DID NOT RUN. A rail that could not run is never folded into a pass — the C-338 execution assertion, applied to the served side.

Configuration Files

.dcs/content.yaml

version: 1
lastUpdated: "2025-01-01T00:00:00Z"
updatedBy: "portal"

global:
  nav.home: Home
  nav.about: About
  footer.copyright: © 2025 My Company

pages:
  home:
    hero.title: Welcome to Our Site
    hero.subtitle: Build amazing things
    cta.primary: Get Started
  about:
    hero.title: About Us
    hero.subtitle: Learn more about our mission

.dcs/seo.yaml

version: 1
lastUpdated: "2025-01-01T00:00:00Z"

global:
  siteName: My Site
  siteUrl: https://example.com
  locale: en_US
  defaultTitle: My Site
  defaultDescription: Build amazing things with us
  titleTemplate: "%s | My Site"
  
  social:
    twitter: mycompany
    linkedin: my-company
  
  images:
    logo: https://example.com/logo.png
    ogDefault: https://example.com/og-image.jpg

pages:
  home:
    title: Welcome
    description: Build amazing things with our platform
    noTitleTemplate: true
    openGraph:
      type: website
    twitter:
      card: summary_large_image
  
  about:
    title: About Us
    description: Learn more about our company and mission

TypeScript Support

All composables and plugins are fully typed. Import types as needed:

import type {
  TextContentConfig,
  TextContentReturn,
  SeoConfiguration,
  PageSeoConfig,
  ReleaseNote
} from '@duffcloudservices/cms'

Migration from Manual Setup

If you're migrating from manually copied composables:

// Before:
import { useTextContent } from '@/lib/use-text-content'

// After:
import { useTextContent } from '@duffcloudservices/cms'

The API is the same, so no other code changes are needed.

DCS Visual Editor Bridge

For DCS-managed sites, src/editor/editorBridge.ts is the shared discovery/runtime layer that turns site DOM markers into portal editing entry points.

Managed background images

For customer-site hero, CTA, footer, card, staff, and gallery images that need editor image replacement, prefer the exported ManagedImage component over CSS-only background-image or hardcoded image arrays.

<script setup lang="ts">
import ManagedImage from '@duffcloudservices/cms/managed-image'
</script>

<template>
  <ManagedImage
    page-slug="home"
    image-key="hero.image.url"
    alt-key="hero.image.alt"
    fallback-src="/images/hero.jpg"
    fallback-alt="Clinic hero"
    context="hero"
    class="h-full w-full object-cover"
  />
</template>

Add the URL and alt keys to .dcs/content.yaml using CDN URLs for committed content:

pages:
  home:
    hero.image.url: https://files.duffcloudservices.com/content/site-slug/assets/example.jpg
    hero.image.alt: Clinic hero

ManagedImage resolves build-time/runtime content with useTextContent, renders a real <picture>/<img> target, applies responsive CDN variants through useResponsiveImage, and emits data-dcs-image-key, data-dcs-image-url, and data-dcs-image-alt on the actual <img>. This lets the visual editor open image management for the asset and lets snapshot capture wait on an actual image before full-page.png.

For repeated galleries or cards, use indexed keys such as gallery.item-0.image.url and gallery.item-0.image.alt, then pass those keys through the rendered item. Do not make editable images CSS-only backgrounds; if a design needs background behavior, render a real managed image layer behind the content.

Click-to-call (NAP phone)

DcsCallButton renders a tel: call affordance driven by the portal-managed business.phone NAP key (the same global content key used for LocalBusiness identity). Use it for the above-the-fold mobile call path service sites need.

<script setup lang="ts">
import DcsCallButton from '@duffcloudservices/cms/call-button'
</script>

<template>
  <!-- compact, above-the-fold mobile header affordance -->
  <DcsCallButton variant="icon" />

  <!-- icon + label + number for a nav/menu row or hero -->
  <DcsCallButton variant="inline" />
</template>

business.phone lives in .dcs/content.yaml under global (create-dcs-site scaffolds it empty):

global:
  business.phone: "(248) 385-2926"
  business.name: Iron Oak Contractors

The number is read through useTextContent, so it is SSR-safe (no window/document at setup) and portal-editable. The component renders nothing until business.phone is set — it never fabricates a placeholder number into prod SSR. The tel: href strips formatting to digits (preserving a leading + for E.164), the visible number carries data-dcs-text="business.phone" for inline editing, and styling is themeable via --dcs-call-* custom properties (icon inherits currentColor and stays unobtrusive on desktop).

Current first-party surfaces:

  • Managed forms — discovers [data-form-key], reports hasManagedForms/managedFormIds, and emits dcs:managed-form-click
  • Reviews — discovers [data-dcs-reviews], reports hasReviews/reviewKeys, emits dcs:reviews-click, and accepts structured dcs:update-reviews data for live preview refresh

The bridge owns discovery and affordances; the portal owns the actual editing workflow. Keep every entry point converged on one sheet per component family instead of creating parallel editors.

See ../FIRST-PARTY-COMPONENTS.md for the shared contract across runtime markers, bridge events, portal workflows, publishing, rollout, and validation.

License

MIT