ChemPal Documentation - v1.6.0
    Preparing search index...

    Extracts, normalizes, and queries schema.org JSON-LD found on a page.

    Construct it from a DOM (or DOM-like) document with SchemaOrgData.fromDocument, or from already-parsed objects with SchemaOrgData.fromNodes, then use the query helpers (get, first, all) or the type-keyed toNested view.

    const data = SchemaOrgData.fromDocument(document);
    data.types(); // ['Organization', 'WebPage', 'BreadcrumbList', 'Product']
    data.first('Product')?.sku; // 'CHEM027'
    export class SchemaOrgData {
    /**
    * Top-level schema.org nodes, in document order, with `@type` and `@context`
    * left intact. `@graph` wrappers are already flattened into this list.
    *
    * @example
    * ```ts
    * const data = SchemaOrgData.fromNodes(blocks);
    * data.nodes.length; // number of top-level entities
    * data.nodes[0]['@type']; // 'Organization'
    * ```
    *
    * @source
    */
    readonly nodes: readonly SchemaNode[];

    /**
    * Wrap an already-collected list of nodes. Prefer the static factories unless
    * you have pre-filtered nodes of your own. Each node is normalized on the way
    * in: schema.org enumeration URLs in string values are reduced to their bare
    * member name (see {@link stripSchemaEnumPrefix}), leaving `@context` intact.
    *
    * @param nodes - Top-level schema.org nodes to hold.
    *
    * @example
    * ```ts
    * const data = new SchemaOrgData([
    * { '@context': 'https://schema.org', '@type': 'Product' },
    * ]);
    * ```
    *
    * @source
    */
    constructor(nodes: SchemaNode[]) {
    this.nodes = nodes.map((node) => normalizeValue(node) as SchemaNode);
    }

    /**
    * Parse every `<script type="application/ld+json">` block under `root`,
    * flatten `@graph` wrappers and arrays, and keep only nodes whose `@context`
    * is schema.org.
    *
    * @param root - A document or element subtree to search. Defaults to the
    * global `document` in a browser; pass a jsdom/linkedom document in Node.
    * @returns A {@link SchemaOrgData} over the discovered nodes.
    *
    * @example
    * ```ts
    * import { JSDOM } from 'jsdom';
    * const { document } = new JSDOM(html).window;
    * const data = SchemaOrgData.fromDocument(document);
    * ```
    *
    * @source
    */
    static fromDocument(root: ParentNode = document): SchemaOrgData {
    const nodes = parseScripts(root)
    .flatMap(collectNodes)
    .filter((node) => isSchemaContext(node['@context']));
    return new SchemaOrgData(nodes);
    }

    /**
    * Build directly from already-parsed object(s), applying the same `@graph`
    * flattening and schema.org context filtering as
    * {@link SchemaOrgData.fromDocument}. Useful for tests or when the JSON-LD is
    * obtained without a DOM.
    *
    * @param input - A single parsed object or an array of them.
    * @returns A {@link SchemaOrgData} over the schema.org nodes found.
    *
    * @example
    * ```ts
    * const data = SchemaOrgData.fromNodes([
    * { '@context': 'https://schema.org', '@type': 'Product', sku: 'CHEM027' },
    * ]);
    * ```
    *
    * @source
    */
    static fromNodes(input: JsonObject | JsonObject[]): SchemaOrgData {
    const nodes = toArray(input)
    .flatMap(collectNodes)
    .filter((node) => isSchemaContext(node['@context']));
    return new SchemaOrgData(nodes);
    }

    /**
    * Deep-scan an arbitrary object (or array) and collect every sub-object that
    * carries a schema.org `@context`. Use this when the JSON-LD is embedded in a
    * larger app-state blob under unpredictable keys — as many storefronts do —
    * rather than provided as a clean node. Detection is by `@context`, not
    * `@type`, so unrelated framework types with their own `@type` are ignored.
    *
    * Recursion stops at each match: a context-bearing object is taken whole, and
    * its nested typed children (which inherit that context) stay inside it —
    * reach them later with {@link SchemaOrgData.all | all}.
    *
    * @param data - Any parsed JSON value to search.
    * @returns A {@link SchemaOrgData} over every schema.org node found at any depth.
    *
    * @example
    * ```ts
    * // A Bloomreach/ATG page-state object with schema buried inside:
    * const data = SchemaOrgData.fromObject(pageState);
    * data.types(); // ['BreadcrumbList', 'Product']
    * data.first('Product'); // the nested Product node
    * ```
    *
    * @source
    */
    static fromObject(data: unknown): SchemaOrgData {
    const found: SchemaNode[] = [];
    const visit = (value: JsonValue): void => {
    if (Array.isArray(value)) {
    value.forEach(visit);
    return;
    }
    if (!isPlainObject(value)) {
    return;
    }
    if (isSchemaContext(value['@context'])) {
    found.push(value); // take the node whole; don't descend into it
    return;
    }
    for (const key of Object.keys(value)) visit(value[key]);
    };
    visit(data as JsonValue);
    return new SchemaOrgData(found);
    }

    /**
    * List the distinct `@type` values across all top-level nodes.
    *
    * @returns Each declared top-level type once, in first-seen order.
    *
    * @example
    * ```ts
    * data.types(); // ['Organization', 'WebPage', 'BreadcrumbList', 'Product']
    * ```
    *
    * @source
    */
    types(): string[] {
    const seen = new Set<string>();
    for (const node of this.nodes) {
    for (const type of typeList(node)) seen.add(type);
    }
    return [...seen];
    }

    /**
    * Test whether any top-level node declares `type`.
    *
    * @param type - The schema.org type name to look for (e.g. `'Product'`).
    * @returns `true` if at least one top-level node has that type.
    *
    * @example
    * ```ts
    * data.has('Product'); // true
    * data.has('Recipe'); // false
    * ```
    *
    * @source
    */
    has(type: string): boolean {
    return this.nodes.some((node) => typeList(node).includes(type));
    }

    /**
    * All top-level nodes declaring `type`. A node with several types matches each
    * of them, so it can be returned by more than one `get` call.
    *
    * @param type - The schema.org type name to filter by.
    * @returns Matching top-level nodes, in document order (empty if none).
    *
    * @example
    * ```ts
    * data.get('Product'); // every top-level Product node
    * data.get('Product').length; // how many there are
    * ```
    *
    * @source
    */
    get(type: string): SchemaNode[] {
    return this.nodes.filter((node) => typeList(node).includes(type));
    }

    /**
    * The first top-level node of `type`, if any.
    *
    * @param type - The schema.org type name to look for.
    * @returns The first matching node, or `undefined` when none exists.
    *
    * @example
    * ```ts
    * data.first('Product')?.sku; // 'CHEM027'
    * data.first('Recipe'); // undefined
    * ```
    *
    * @source
    */
    first(type: string): SchemaNode | undefined {
    return this.get(type)[0];
    }

    /**
    * Depth-first search for every typed object anywhere in the tree, including
    * nested ones — the `Offer` inside a `Product`, or a `PropertyValue` used as
    * an `identifier`. Omit `type` to collect all typed objects.
    *
    * @param type - Optional schema.org type to filter by; when omitted, every
    * object carrying an `@type` is returned.
    * @returns All matching nodes at any depth, in depth-first document order.
    *
    * @example
    * ```ts
    * data.all('Offer'); // Offers wherever they are nested
    * data.all('PropertyValue'); // every identifier-style value pair
    * data.all(); // all typed objects on the page
    * ```
    *
    * @source
    */
    all(type?: string): SchemaNode[] {
    const found: SchemaNode[] = [];
    const visit = (value: JsonValue): void => {
    if (Array.isArray(value)) {
    value.forEach(visit);
    return;
    }
    if (!isPlainObject(value)) {
    return;
    }
    if ('@type' in value && (type === undefined || typeList(value).includes(type))) {
    found.push(value);
    }
    for (const key of Object.keys(value)) visit(value[key]);
    };
    this.nodes.forEach(visit);
    return found;
    }

    /**
    * A type-keyed view of the data: each node's `@type` becomes the wrapping key
    * and `@type`/`@context` drop out of the body. Arrays whose elements are all
    * single-typed objects are grouped by type (a `BreadcrumbList`'s items become
    * `{ ListItem: [...] }`), and multiple top-level nodes of the same type
    * collapse into an array under that key. Optimized for readable inspection
    * rather than programmatic access.
    *
    * @returns A plain object keyed by schema.org type.
    *
    * @example
    * ```ts
    * data.toNested().BreadcrumbList.itemListElement.ListItem[0].name; // 'Home'
    * data.toNested().Product.offers.Offer.price; // '5.8'
    * ```
    *
    * @source
    */
    toNested(): JsonObject {
    const out: JsonObject = {};
    for (const node of this.nodes) {
    const nested = nestObject(node);
    if (!isPlainObject(nested)) continue;
    for (const [type, body] of Object.entries(nested)) {
    if (out[type] === undefined) {
    out[type] = body;
    } else {
    const existing = out[type];
    out[type] = Array.isArray(existing) ? [...existing, body] : [existing, body];
    }
    }
    }
    return out;
    }
    }
    Index

    Constructors

    • Wrap an already-collected list of nodes. Prefer the static factories unless you have pre-filtered nodes of your own. Each node is normalized on the way in: schema.org enumeration URLs in string values are reduced to their bare member name (see stripSchemaEnumPrefix), leaving @context intact.

      Parameters

      • nodes: JsonObject[]

        Top-level schema.org nodes to hold.

      Returns SchemaOrgData

      const data = new SchemaOrgData([
      { '@context': 'https://schema.org', '@type': 'Product' },
      ]);

    Methods

    • Parse every <script type="application/ld+json"> block under root, flatten @graph wrappers and arrays, and keep only nodes whose @context is schema.org.

      Parameters

      • root: ParentNode = document

        A document or element subtree to search. Defaults to the global document in a browser; pass a jsdom/linkedom document in Node.

      Returns SchemaOrgData

      A SchemaOrgData over the discovered nodes.

      import { JSDOM } from 'jsdom';
      const { document } = new JSDOM(html).window;
      const data = SchemaOrgData.fromDocument(document);
        static fromDocument(root: ParentNode = document): SchemaOrgData {
      const nodes = parseScripts(root)
      .flatMap(collectNodes)
      .filter((node) => isSchemaContext(node['@context']));
      return new SchemaOrgData(nodes);
      }
    • Build directly from already-parsed object(s), applying the same @graph flattening and schema.org context filtering as SchemaOrgData.fromDocument. Useful for tests or when the JSON-LD is obtained without a DOM.

      Parameters

      Returns SchemaOrgData

      A SchemaOrgData over the schema.org nodes found.

      const data = SchemaOrgData.fromNodes([
      { '@context': 'https://schema.org', '@type': 'Product', sku: 'CHEM027' },
      ]);
        static fromNodes(input: JsonObject | JsonObject[]): SchemaOrgData {
      const nodes = toArray(input)
      .flatMap(collectNodes)
      .filter((node) => isSchemaContext(node['@context']));
      return new SchemaOrgData(nodes);
      }
    • Deep-scan an arbitrary object (or array) and collect every sub-object that carries a schema.org @context. Use this when the JSON-LD is embedded in a larger app-state blob under unpredictable keys — as many storefronts do — rather than provided as a clean node. Detection is by @context, not @type, so unrelated framework types with their own @type are ignored.

      Recursion stops at each match: a context-bearing object is taken whole, and its nested typed children (which inherit that context) stay inside it — reach them later with all.

      Parameters

      • data: unknown

        Any parsed JSON value to search.

      Returns SchemaOrgData

      A SchemaOrgData over every schema.org node found at any depth.

      // A Bloomreach/ATG page-state object with schema buried inside:
      const data = SchemaOrgData.fromObject(pageState);
      data.types(); // ['BreadcrumbList', 'Product']
      data.first('Product'); // the nested Product node
        static fromObject(data: unknown): SchemaOrgData {
      const found: SchemaNode[] = [];
      const visit = (value: JsonValue): void => {
      if (Array.isArray(value)) {
      value.forEach(visit);
      return;
      }
      if (!isPlainObject(value)) {
      return;
      }
      if (isSchemaContext(value['@context'])) {
      found.push(value); // take the node whole; don't descend into it
      return;
      }
      for (const key of Object.keys(value)) visit(value[key]);
      };
      visit(data as JsonValue);
      return new SchemaOrgData(found);
      }
    • List the distinct @type values across all top-level nodes.

      Returns string[]

      Each declared top-level type once, in first-seen order.

      data.types(); // ['Organization', 'WebPage', 'BreadcrumbList', 'Product']
      
        types(): string[] {
      const seen = new Set<string>();
      for (const node of this.nodes) {
      for (const type of typeList(node)) seen.add(type);
      }
      return [...seen];
      }
    • Test whether any top-level node declares type.

      Parameters

      • type: string

        The schema.org type name to look for (e.g. 'Product').

      Returns boolean

      true if at least one top-level node has that type.

      data.has('Product'); // true
      data.has('Recipe'); // false
        has(type: string): boolean {
      return this.nodes.some((node) => typeList(node).includes(type));
      }
    • All top-level nodes declaring type. A node with several types matches each of them, so it can be returned by more than one get call.

      Parameters

      • type: string

        The schema.org type name to filter by.

      Returns JsonObject[]

      Matching top-level nodes, in document order (empty if none).

      data.get('Product');        // every top-level Product node
      data.get('Product').length; // how many there are
        get(type: string): SchemaNode[] {
      return this.nodes.filter((node) => typeList(node).includes(type));
      }
    • The first top-level node of type, if any.

      Parameters

      • type: string

        The schema.org type name to look for.

      Returns undefined | JsonObject

      The first matching node, or undefined when none exists.

      data.first('Product')?.sku; // 'CHEM027'
      data.first('Recipe'); // undefined
        first(type: string): SchemaNode | undefined {
      return this.get(type)[0];
      }
    • Depth-first search for every typed object anywhere in the tree, including nested ones — the Offer inside a Product, or a PropertyValue used as an identifier. Omit type to collect all typed objects.

      Parameters

      • Optionaltype: string

        Optional schema.org type to filter by; when omitted, every object carrying an @type is returned.

      Returns JsonObject[]

      All matching nodes at any depth, in depth-first document order.

      data.all('Offer');         // Offers wherever they are nested
      data.all('PropertyValue'); // every identifier-style value pair
      data.all(); // all typed objects on the page
        all(type?: string): SchemaNode[] {
      const found: SchemaNode[] = [];
      const visit = (value: JsonValue): void => {
      if (Array.isArray(value)) {
      value.forEach(visit);
      return;
      }
      if (!isPlainObject(value)) {
      return;
      }
      if ('@type' in value && (type === undefined || typeList(value).includes(type))) {
      found.push(value);
      }
      for (const key of Object.keys(value)) visit(value[key]);
      };
      this.nodes.forEach(visit);
      return found;
      }
    • A type-keyed view of the data: each node's @type becomes the wrapping key and @type/@context drop out of the body. Arrays whose elements are all single-typed objects are grouped by type (a BreadcrumbList's items become { ListItem: [...] }), and multiple top-level nodes of the same type collapse into an array under that key. Optimized for readable inspection rather than programmatic access.

      Returns JsonObject

      A plain object keyed by schema.org type.

      data.toNested().BreadcrumbList.itemListElement.ListItem[0].name; // 'Home'
      data.toNested().Product.offers.Offer.price; // '5.8'
        toNested(): JsonObject {
      const out: JsonObject = {};
      for (const node of this.nodes) {
      const nested = nestObject(node);
      if (!isPlainObject(nested)) continue;
      for (const [type, body] of Object.entries(nested)) {
      if (out[type] === undefined) {
      out[type] = body;
      } else {
      const existing = out[type];
      out[type] = Array.isArray(existing) ? [...existing, body] : [existing, body];
      }
      }
      }
      return out;
      }

    Properties

    nodes: readonly JsonObject[]

    Top-level schema.org nodes, in document order, with @type and @context left intact. @graph wrappers are already flattened into this list.

    const data = SchemaOrgData.fromNodes(blocks);
    data.nodes.length; // number of top-level entities
    data.nodes[0]['@type']; // 'Organization'
      readonly nodes: readonly SchemaNode[];