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

@amplifyup/sdk

v0.1.77

Published

AmplifyUp Tracking SDK - CDP-agnostic event tracking library

Downloads

4,754

Readme

@amplifyup/sdk

Official JavaScript SDK for AmplifyUp — event tracking, Edge-resolved page layouts, personalization, and Composer preview on your site.

Full guide: Developer docs · API

Install

npm install @amplifyup/sdk

Quick start (production)

Production is the default. You only need a tracking ID from the AmplifyUp dashboard.

1. Provider

import { AmplifyUpProvider } from '@amplifyup/sdk/react';

export default function RootLayout({ children }) {
  return (
    <AmplifyUpProvider config={{ trackingId: 'your-tracking-id' }}>
      {children}
    </AmplifyUpProvider>
  );
}

2. Page content

import { AmplifyPageContent, ComponentContextProvider } from '@amplifyup/sdk/react';
import { Hero } from '@/components/Hero';

const registry = { Hero };

function renderComponent(componentId, props, slots, context) {
  const Component = registry[componentId];
  if (!Component) return null;
  return (
    <ComponentContextProvider
      props={props}
      slots={slots}
      layoutNodeId={context?.layoutNodeId}
      componentId={componentId}
    >
      <Component {...props} />
    </ComponentContextProvider>
  );
}

export default function Page() {
  return (
    <AmplifyPageContent
      renderComponent={renderComponent}
      fallback={<YourStaticPage />}
    />
  );
}

3. Write a component

import { Field } from '@amplifyup/sdk/react';
import type { Fields } from '@amplifyup/sdk/react';

export function Hero({ fields }: { fields: Fields<{ heading: string }> }) {
  return (
    <section>
      <h1>
        <Field field={fields.heading} />
      </h1>
    </section>
  );
}

Connect content in Composer, then Deploy so the live site can serve the page.

That's the whole loop. The rest of this section is the detail.


Working with fields

Every component receives a fields prop. It is not raw CMS data — every entry is a field envelope:

fields.heading
// → { value: 'Welcome to AmplifyUp', name: 'heading' }

fields.hero.image
// → { value: { url: '…', alt: '…' }, name: 'hero.image' }

fields.subheading   // empty on a new page
// → { value: null, name: 'subheading' }

fields.posts.value[0].title   // a row inside a list
// → { value: 'Ship faster', name: 'title', ref: { providerId, entity, id } }

name is the field's dot path. It is what lets <Field> know which field it is bound to without you telling it. value is the content, typed from your schema.

The shape is identical on the live site, in a draft, and in Composer. There is no mode where you get a bare string instead of an envelope, and no mode where an empty field is undefined instead of { value: null, name }.

Write targets

One rule explains everything about editing:

A field is editable if and only if it carries a write target. The write target is set by whoever produced the data. Nothing downstream guesses.

  • A page field (fields.heading) has no ref. Its write target is the page, and Composer already knows which page you are on.
  • A list row field (post.title) carries ref: { providerId, entity, id } — the record it belongs to. Editing it saves to that record, not to the page.
  • A row field the SDK could not trace back to a producer gets no ref and is marked readOnly: true. It renders as plain text in Composer: no outline, no click target.

You never set ref yourself. You never read it either. It is there so that the same <Field> you use for a page heading also works on a row in a list.

Two ways to read a field

// Editable — use an SDK component
<Field field={fields.heading} />

// Display only — read .value
<title>{fields.heading.value}</title>

Use the component when the field should be clickable in Composer. Use .value when it shouldn't — meta tags, aria-label, an href, a condition, anything that isn't visible text.

export function Card({ fields }: { fields: Fields<{ title: string; link: string; image: ImageValue }> }) {
  return (
    <a href={fields.link.value ?? '#'} aria-label={fields.title.value ?? undefined}>
      <Image field={fields.image} />
      <h3><Field field={fields.title} /></h3>
    </a>
  );
}

SDK field components

| Component | Field type | Renders | | --- | --- | --- | | <Field> | string, number | a <span> with the text | | <RichText> | markdown / rich text | rendered HTML in a <div> (change with as) | | <Image> | ImageValue | <img> |

All three take a field prop and read name from it. Pass any extra props (className, loading, etc.) and they go on the rendered element.

<Field field={fields.eyebrow} className="text-xs uppercase" />
<RichText field={fields.body} className="prose" />
<Image field={fields.cover} className="rounded-lg" loading="lazy" />

<Field> is scalar-only. Passing a list is a type error:

// ✗ Field<string[]> is not assignable to Field<string | number>
<Field field={fields.tags} />

Empty fields

An empty field renders nothing on the live site and a clickable placeholder in Composer. The SDK never invents default copy.

// A new page, `subheading` not yet filled in:

<Field field={fields.subheading} />
// live:      (nothing)
// Composer:  clickable empty placeholder

If you want a default, it's yours to write, and it reads from .value:

// ✓ your default, in your code
<p>{fields.subheading.value ?? 'Built for teams that ship.'}</p>

// ✓ hide the whole component when the key field is empty
export function Banner({ fields }: { fields: Fields<{ message: string }> }) {
  if (!fields.message.value) return null;
  return <div className="banner"><Field field={fields.message} /></div>;
}

Why no fallback prop: a fallback turns "this field was never populated" into "this field says Welcome". Nobody notices until a customer does. Empty should look empty.

Wrapper elements

<Field> renders an inline <span>, never a heading or paragraph, so you supply the semantic tag around it:

// ✓
<h1><Field field={fields.heading} /></h1>
<p className="lead"><Field field={fields.intro} /></p>

// ✗ — Field has no `as` prop; put the element around it
<Field as="h1" field={fields.heading} />

<RichText> is the exception: it owns a block element, and as picks which one.

// ✓ — default is <div>
<RichText field={fields.body} className="prose" />

// ✓ — pick a different block element
<RichText field={fields.body} as="article" className="prose" />

// ✗ — don't nest a block element inside a paragraph
<p><RichText field={fields.body} /></p>

If the wrapper should disappear when the field is empty, check .value:

{fields.intro.value ? (
  <p className="lead"><Field field={fields.intro} /></p>
) : null}

Computed values

Occasionally what you display isn't a field — it's derived from one. Then you pass value and must also pass name so Composer knows which field a click edits:

// Show a formatted price but edit the raw number
<Field
  value={formatPrice(fields.price.value)}
  name={fields.price.name}
/>

name is only accepted alongside value. This is the one place you write it.

// ✗ — redundant, and a type error
<Field field={fields.price} name="price" />

// ✗ — value without name has nothing to bind to
<Field value={formatPrice(fields.price.value)} />

Rendering differently in Composer

Most components render the same everywhere. Sometimes the live markup can't host an editable field — the text is a CSS background, an SVG <title>, a <meta> tag, an attribute, or it's behind interaction (a closed accordion, an auto-playing carousel). In Composer you still need something the author can click.

useInComposer() tells you which mode you're in:

import { Field, Image, useInComposer } from '@amplifyup/sdk/react';

export function HeroBackground({ fields }: { fields: Fields<{ heading: string; image: ImageValue }> }) {
  const inComposer = useInComposer();

  return (
    <section
      className="hero"
      style={{ backgroundImage: `url(${fields.image.value?.url ?? ''})` }}
    >
      {/* Live: image is a CSS background. Composer: also render it so it's clickable. */}
      {inComposer ? <Image field={fields.image} className="hidden" /> : null}
      <h1><Field field={fields.heading} /></h1>
    </section>
  );
}

Other common cases:

// Attribute on the live site, editable in Composer
const inComposer = useInComposer();
return (
  <>
    <button aria-label={fields.label.value ?? undefined}>
      <Icon />
      {inComposer ? <Field field={fields.label} /> : null}
    </button>
  </>
);
// Component that hides itself when empty on live, but stays visible for authors
export function Announcement({ fields }: { fields: Fields<{ message: string }> }) {
  const inComposer = useInComposer();
  if (!fields.message.value && !inComposer) return null;
  return <div className="announcement"><Field field={fields.message} /></div>;
}
// Interaction that fights the canvas — turn it off in Composer
export function Carousel({ fields }) {
  const inComposer = useInComposer();
  return (
    <>
      {(fields.slides.value ?? []).map((slide) => (
        <Slide key={slide.id} slide={slide} autoplay={!inComposer} />
      ))}
    </>
  );
}

Rules of thumb:

  • The field component is the source of truth in Composer. If a field is visible to authors, it must be rendered through <Field> / <RichText> / <Image> in Composer mode, even if the live site reads .value.
  • Don't fork the whole component. Branch the one element that differs, not the entire return. Two divergent trees drift and the preview stops matching production.
  • Empty gating uses inComposer. if (!x.value) return null is correct for live and wrong for authors — they can't click what isn't rendered. Add && !inComposer.
  • Don't use it to hide editable text on live. If the text is visible on the live site, render it with the field component in both modes. useInComposer is for content the live markup genuinely can't expose as a field.
// ✗ — heading is visible on live; no reason to fork
{inComposer ? <Field field={fields.heading} /> : fields.heading.value}

// ✓
<Field field={fields.heading} />

Lists

Every object array is a list of records. Each row is a fields object plus a plain id. Edit post.title in the grid and you are editing the post. Which records are in the list, and their order, is edited in the props panel — not on the canvas.

Scalar lists (tags, multi-select) stay plain: fields.tags is Field<string[]>.

import { Field, Image, RichText } from '@amplifyup/sdk/react';

export function LatestPosts({ fields }: { fields: Fields<{ heading: string; posts: Post[] }> }) {
  const posts = fields.posts.value ?? [];
  return (
    <section>
      <h2><Field field={fields.heading} /></h2>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Image field={post.mainImage} />
            <a href={`/blog/${post.slug}`}>
              <Field field={post.title} />
            </a>
          </li>
        ))}
      </ul>
    </section>
  );
}
// ✗ — Field expects a scalar, not the list or the row
<Field field={fields.posts} />
<Field field={post} />

// ✓ — edit the record through its fields
<Field field={post.title} />
<Image field={post.mainImage} />

Search or load-more rows from queryContent are the same shape. Render them with the same components — one renderer for both:

const posts = hits ?? fields.posts.value ?? [];
return posts.map((post) => (
  <article key={post.id}>
    <h2><Field field={post.title} /></h2>
  </article>
));

When a row is editable

A row is editable when the SDK knows which record it came from. That happens automatically in two cases:

| Rows came from | Editable in Composer? | | --- | --- | | A list prop on your component (curated or query connection) | Yes | | queryContent using the published spec from {field}Pagination.spec | Yes | | queryContent with a spec you assembled by hand, missing providerId or entity | No — renders as plain text | | Rows you fetched yourself from your CMS and passed in as props | No — renders as plain text |

Read-only rows still render their text on the live site and in Composer. They just aren't clickable, and in development the SDK logs once per field:

[AmplifyUp SDK] title has no write target; rendered read-only.

If you see that and expected an editable field, the fix is upstream — use the published spec instead of a hand-built one, or bind the list to a connection in Composer. Do not try to add ref yourself.

// ✓ — spec comes from the list prop Composer published; rows stay editable
const hits = await queryContent({
  trackingId,
  route: '/insights',
  spec: searchSpec(postsPagination, 'title', term),
});

// ✗ — hand-built spec with no connection behind it; rows render read-only
const hits = await queryContent({
  trackingId,
  route: '/insights',
  spec: { providerId: '', entity: '', filter: [], sort: [], limit: 10, offset: 0 },
});

Slots

A slot is a region where authors drop other components. Render it with <Slot>:

import { Slot } from '@amplifyup/sdk/react';

export function TwoColumn() {
  return (
    <div className="grid grid-cols-2">
      <div><Slot name="left" /></div>
      <div><Slot name="right" /></div>
    </div>
  );
}

<Slot> takes only name and className. It reads the slot content from ComponentContextProvider, so you never thread a slots prop through your component.

// ✗ — Slot has no `slots` prop
<Slot name="left" slots={slots} />

// ✓
<Slot name="left" />

Slot names must match the slots declared on the component in AmplifyUp. Slots hold components, not fields — nothing in a slot is read through fields.


Do / Don't

| Do | Don't | | --- | --- | | <Field field={fields.heading} /> | <Field field={fields.heading} name="heading" /> | | {fields.heading.value} for non-visible uses | {fields.heading} as a JSX child (it's an object) | | {fields.x.value ?? 'default'} in your code | expect the SDK to supply default copy | | if (!fields.x.value) return null to hide a component | wrap Field in conditional logic that hides the field in Composer | | <h1><Field … /></h1> | look for an as / wrapper prop | | <Field field={post.title} /> in a list | <Field field={fields.posts} /> or <Field field={post} /> | | value + name together for computed output | value alone, or name alone | | <Image field={fields.cover} /> | <img src={fields.cover.value.url} /> when it should be editable | | queryContent rows through <Field field={post.title} /> | treat query rows as raw CMS objects | | spec: searchSpec(postsPagination, …) from the published list prop | hand-build a spec and expect rows to stay editable | | <Slot name="left" /> | <Slot name="left" slots={slots} /> | | let the producer set ref | set, copy, or patch ref yourself | | useInComposer() to expose a field the live markup can't (CSS background, attribute, hidden panel) | fork the whole component's return on inComposer | | if (!x.value && !inComposer) return null | if (!x.value) return null — authors can't click what isn't rendered |


Console messages

In development the SDK tells you exactly what is wrong. Production is silent.

| Message | What it means | Fix | | --- | --- | --- | | Field expects a scalar field. Did you mean <Field field={post.title} />? | You passed a list or a whole row to Field / RichText / Image | Pass one scalar field off the row | | title has no write target; rendered read-only. | A row field the SDK can't trace to a record | Bind the list to a connection, or query with the published spec | | Field value={…} requires name="…" for computed values | You passed value without name | Add name={fields.x.name} | | Field needs field={fields.yourProp} (or value + name for computed values) | No binding at all | Pass field= | | Slot "left" used outside ComponentContextProvider | The component wasn't wrapped when rendered | Wrap it in renderComponent |


Types

import type { Field, Fields, ImageValue } from '@amplifyup/sdk/react';

type Field<T> = {
  value: T;
  name: string;
  /** Write target — the record this field saves to. Page fields omit it. */
  ref?: { providerId: string; entity: string; id: string };
  /** Row field with no write target — display only in Composer. */
  readOnly?: true;
};

type Fields<T> = { [K in keyof T]: /* Field<T[K]>, recursing into objects and lists */ };

Declare the schema shape once on the component and let inference do the rest:

type HeroFields = Fields<{
  eyebrow: string;
  heading: string;
  body: string;          // markdown → use <RichText>
  image: ImageValue;     // → use <Image>
  posts: Post[];         // list — each row is Fields<Post> & { id: string }
  tags: string[];        // scalar list — Field<string[]>
}>;

export function Hero({ fields }: { fields: HeroFields }) { … }

fields.heading.value is string. fields.image.value is ImageValue | null. fields.posts.value[0] is Fields<Post> & { id: string }. fields.posts.value[0].title is Field<string> with ref pointing at that post.


Content sources

| Mode | Source | When | |------|--------|------| | Production visitors | Edge /v1/resolve | Default | | Next.js SSG / ISR | Edge /v1/routes then /v1/resolve | generateAmplifyStaticParams + fetchPageConfigServer | | Composer preview | Orchestrator (Control Plane) layout provider | ?preview=true / Composer iframe | | Runtime queries | Edge /v1/query | queryContent — search, load more, pickers |

Field envelopes are attached once, when the SDK reads the Edge response. Edge's JSON is raw; you never see it that way if you go through AmplifyPageContent or fetchPageConfigServer. If you call fetchEdgeResolve directly you get raw props and must map them yourself — prefer the documented path.

Pattern pages (From page)

Components using Composer From page require pageContext on the provider (or init) so the first paint and first page view include fields. Late setPageContext is fine for subsequent events, not for rendering those components.

resource.key is the URL param value. If a mapped entity field (for example post.slug) does not match that param, the SDK warns and uses the URL param.

<AmplifyUpProvider
  config={{ trackingId: 'your-tracking-id' }}
  pageContext={{
    resourceType: 'post',
    key: slug,
    fields: { title: post.title, slug: post.slug, category: post.category },
  }}
>
  {children}
</AmplifyUpProvider>

Pass pageContext at provider/init for From-page components. Do not wait until after first paint. Fixed (static) page fields still attach to events when pageContext is omitted.

The SDK maps Edge's resolved tree into layoutTree and renders it. Edge already applies personalization and CMS projection — the site does not call CMS or Decision APIs for layout. After paint, the overlay calls POST /v1/select (or selectVariants()) then /v1/resolve?variants= so decisions stay on the edge (clientPersonalization).

Runtime queries

Page resolve covers data known at Deploy. For data a visitor asks for — search, load more, a picker — use queryContent. The browser asks AmplifyUp, AmplifyUp asks your content source, so no CMS credentials or query syntax live in your site.

Mark a query connection paginated in Composer and the list prop gains pagination meta, including the published query spec:

'use client';
import { queryContent, nextPageSpec, searchSpec } from '@amplifyup/sdk/react';

// next page
const more = await queryContent({
  trackingId,
  route: '/insights',
  spec: nextPageSpec(postsPagination),
});

// search — keep `?q=` as input only; do not read searchParams on the server page
const hits = await queryContent({
  trackingId,
  route: '/insights',
  spec: searchSpec(postsPagination, 'title', term),
});

Only entities published with that route can be queried, and visitors only see published content. Also available from @amplifyup/sdk/server. See the runtime queries guide.

Always start from postsPagination.spec. nextPageSpec and searchSpec do that for you. The spec identifies the connection, and that is what makes the returned rows editable in Composer — see when a row is editable. A spec you assemble by hand has no connection behind it, so its rows come back read-only.

Track events

import { init, page, track, identify } from '@amplifyup/sdk';

await init({ trackingId: 'your-tracking-id' });

page('/home');
track('Product Viewed', { productId: '123' });
identify('user-123', { email: '[email protected]' });

Debugging

AmplifyUp.getStatus()

React context: useAmplifyUp() → { pageConfig, loading, error, source, meta }.

License

MIT