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

@qoh/core-angular

v1.0.0

Published

**Angular client library for [headless.li](https://www.headless.li) — the Semantic Layer for AI Agents.**

Readme

@qoh/core-angular

Angular client library for headless.li — the Semantic Layer for AI Agents.

Build CMS-driven websites with zero hallucinations at a fraction of the (token) cost.


Stop fighting GraphQL. Start shipping.

headless.li is a middleware platform that connects GraphQL-based headless CMS systems to your Angular components — without manually writing any GraphQL queries. Works with:

Drupal · WordPress · AEM · Sitecore · Contentstack · Hygraph · Strapi · Payload · Sanity · Directus (and any other GraphQL-based CMS)

Register your components once against CMS type names, call service.query() with a query name and slug, and add <queenofhearts-renderer /> to your template. The library resolves the right component for each content block automatically based on __typename.

Installation

npm install @qoh/core-angular

Requires Angular 19+, RxJS 7+, and Zod 4+ as peer dependencies.

Quick start

1. Provide your API token

In app.config.ts, add provideHttpClient() and your headless.li token:

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    { provide: 'apiToken', useValue: 'your-headlessli-api-token' },
  ],
};

You can get a free token at headless.li.

2. Register your components

Map each CMS content type (__typename) to the Angular component that should render it. Call this once at startup — e.g. in a dedicated register-components.ts imported by your main.ts:

import { registerComponent } from '@qoh/core-angular';
import { HeroComponent } from './components/hero.component';
import { TextBlockComponent } from './components/text-block.component';

registerComponent(HeroComponent, 'HeroRecord');
registerComponent(TextBlockComponent, 'TextBlockRecord');

3. Fetch page data

Inject QueenofheartsService and call query(). The service automatically sends the registered component list to the API so only the fields your components actually use are returned.

import { Component, OnInit } from '@angular/core';
import { QueenofheartsService, Filter } from '@qoh/core-angular';

@Component({ ... })
export class PageComponent implements OnInit {
  page: any;

  constructor(private qoh: QueenofheartsService) {}

  async ngOnInit() {
    this.page = await this.qoh.query('page', {
      variables: {
        filter: [{ name: 'slug', operator: Filter.eq, value: 'home' }],
      },
    });
  }
}

4. Render the content

Add <queenofhearts-renderer /> to your template and import QueenofheartsRenderComponent. It iterates the block array and renders the correct registered component for each item based on __typename.

import { QueenofheartsRenderComponent } from '@qoh/core-angular';

@Component({
  standalone: true,
  imports: [QueenofheartsRenderComponent],
  template: `<queenofhearts-renderer [data]="page?.sections" />`,
})
export class PageComponent { ... }

Component registration

Eager components

registerComponent(MyComponent, 'MyRecord');

registerComponent accepts an optional Zod schema as the third argument for selective field fetching (see Zod schemas).

Lazy components

For code-splitting, register a dynamic import loader instead of the component directly:

import { registerLazyComponent } from '@qoh/core-angular';

registerLazyComponent(
  () => import('./components/heavy-chart.component').then(m => m.HeavyChartComponent),
  'ChartRecord',
);

The loader is called the first time that component type is encountered and the result cached in the registry.


Zod schemas — selective field fetching

When you register a component with a Zod schema, the library derives a ComponentFieldMap from the schema and sends it to the headless.li API. The API then restricts each component's query to only the fields that schema describes, reducing payload size.

import { z } from 'zod';
import { registerComponent } from '@qoh/core-angular';

const HeroSchema = z.object({
  __typename: z.string(),
  headline: z.string(),
  subline: z.string().optional(),
  image: z.object({
    url: z.string(),
    alt: z.string().nullable(),
  }),
});

registerComponent(HeroComponent, 'HeroRecord', HeroSchema);

For union/polymorphic fields (an array of blocks of different types), use .loose() on the object — this signals to the library that child component resolution should be delegated:

const PageSchema = z.object({
  __typename: z.string(),
  sections: z.array(z.object({ __typename: z.string() }).loose()),
});

Without a schema the library requests all fields ({ __all: true }).


Querying content

service.query(queryName, options?)

Fetches CMS records by query name. The query name corresponds to a root-level query in your CMS GraphQL schema (e.g. page, blogPost, product).

const result = await this.qoh.query('page', {
  variables: {
    filter: [{ name: 'slug', operator: Filter.eq, value: 'about' }],
    locale: 'en',
  },
  depth: 3,
});

Options:

| Option | Type | Description | |---|---|---| | variables.filter | { name, operator, value }[] | Filter conditions | | variables.locale | string | Locale code | | variables | Record<string, unknown> | Any additional CMS-specific variables | | ignoreProperties | string[] | CMS fields to exclude from the response | | depth | number | Relation nesting depth | | fieldArgs | Record<string, unknown> | Extra per-field arguments |

Filter operators (via the Filter enum):

import { Filter } from '@qoh/core-angular';

Filter.eq      // field equals value
Filter.neq     // field does not equal value
Filter.in      // field is one of (value is comma-separated)
Filter.notIn   // field is not one of

service.queryGraphql(graphqlQuery)

Sends a raw GraphQL query string to the headless.li proxy. Use this for one-off queries that don't map to registered components.

const result = await this.qoh.queryGraphql(`
  query {
    allProducts {
      id
      title
    }
  }
`);

Passing shared data to all components

Use the [childData] input to inject shared data (locale, router state, feature flags, etc.) into every component rendered by <queenofhearts-renderer />. Each component receives it via its childData input.

<queenofhearts-renderer [data]="page.sections" [childData]="{ locale: 'en' }" />

CMS backends

For CMS-specific response normalization, provide a backend adapter:

import { DatoCMSBackend } from '@qoh/core-angular';

// in app.config.ts providers:
{ provide: 'backend', useValue: new DatoCMSBackend() }

Built-in adapters: DatoCMSBackend, StrapiCMSBackend.


Browser devtools

When the <body> element has the class qoh-inject-ids, the service enters debug mode. It injects a __qohId attribute into every CMS block object and listens for custom window events emitted by the headless.li browser devtools extension to support component highlighting and data inspection.


API reference

| Export | Kind | Description | |---|---|---| | QueenofheartsService | Angular service | Injected automatically. Call .query() or .queryGraphql() to fetch content. | | QueenofheartsRenderComponent | Angular component | Renders a CMS data array or object by dispatching to registered components. Selector: queenofhearts-renderer. | | registerComponent | function | Registers an eager Angular component against a CMS type name. | | registerLazyComponent | function | Registers a lazy-loaded component with a dynamic import loader. | | Filter | enum | Filter operators: eq, neq, in, notIn. | | zodToComponentFields | function | Converts a Zod schema to a ComponentFieldMap. | | DatoCMSBackend | class | Response normalizer for DatoCMS. | | StrapiCMSBackend | class | Response normalizer for Strapi. |