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

@kopflos-labs/pages

v0.2.8

Published

Quick-start guide for the `@kopflos-labs/pages` package, a data-driven page builder for Kopflos using Lit and RDF.

Downloads

274

Readme

@kopflos-labs/pages

Quick-start guide for the @kopflos-labs/pages package, a data-driven page builder for Kopflos using Lit and RDF.


1. Installation

Install the package via npm:

npm install @kopflos-labs/pages

2. Configuration in kopflos.config.(js|ts)

To use the plugin, you must register it in your Kopflos configuration. For the time being, it is necessary to include the standard Pages API graph to ensure pages can be served correctly.

import PluginPages from '@kopflos-labs/pages'

const baseIri = process.env.API_BASE || 'http://localhost:1429'

export default {
  baseIri,
  plugins: [
    new PluginPages({
      // Optional:
      ssrOptions: {
        // if present, do not call connectedCallback on these custom elements
        disallowConnectedCallback: [
          /^sl-/, // e.g., Shoelace components
        ],
        // if present, only call connectedCallback on these custom elements
        allowConnectedCallback: [],
        // for other SSR options, consult '@lir-labs/ssr package
      },
    }),
  ],
}

3. Creating Pages (Multi-file structure)

By default, pages are located in a pages/ directory, relative to the config. For dynamic routes such as plaque/[id], you can split the page into several files for better organization:

  • [id].ts: The main page definition, containing the template and data logic.
  • [id].html: The static HTML shell (meta tags, global styles, and basic body structure).
  • [id].html.ts: Server-side imports, such as custom elements required for SSR.
  • [id].client.ts: Client-side imports, including runtime scripts and interactive components.

Example: [id].ts

import { html, definePage } from '@kopflos-labs/pages'
import plaqueQuery from './plaque.rq'

export default definePage({
  // The IRI of the main entity for this page
  mainEntity: '/plaque/[id]',
  
  // Data sources: query results will be available in the 'data' object
  queries: {
    plaque: plaqueQuery,
  },
  
  // Dynamic <head> content
  head({ env, data }) {
    const name = data.plaque
      .has(env.ns.rdf.type, env.kopflos.appNs('/api/schema/Plaque'))
      .out(env.ns.schema.name).value

    return `<title>${name} ::: Read the Plaque</title>`
  },
  
  // Page body template
  body({ env }) {
    const PlaqueClass = env.kopflos.appNs('/api/schema/Plaque')
    
    return html`
      <rdf-environment>
        <data-graph data-graph="plaque">
          <target-node target-class="${PlaqueClass.value}">
            <header>
              <traverse-graph property-path="schema:name">
                <my-header></my-header>
              </traverse-graph>
            </header>
            <!-- Page content goes here -->
          </target-node>
        </data-graph>
      </rdf-environment>`
  },
})

4. Loading Data with SPARQL (*.rq files)

You can maintain your SPARQL queries in separate .rq files. These queries are automatically parameterized. Use the sparqlc:param function to bind URL parameters (like id) or the mainEntity IRI.

Example: plaque.rq

PREFIX schema: <http://schema.org/>
PREFIX sparqlc: <https://sparqlc.described.at/>

CONSTRUCT {
  ?s ?p ?o
} {
  # Bind the page's main entity IRI to a variable
  BIND(IRI(sparqlc:param(schema:mainEntity)) as ?plaque)
  
  GRAPH ?plaque {
    ?s ?p ?o
  }
}

Queries defined in the queries object of definePage are executed, and the resulting RDF graphs are provided to the frontend via the <data-graph> component.

Currently, only CONSTRUCT queries are supported for declarative data binding.


5. Configuring queries

The queries option of definePage lets you declare one or more data sources that will be executed server‑side and exposed to your page via the data argument.

Each entry under queries supports multiple forms:

  • Direct function (default export of a .rq file compiled by sparqlc)
  • Object with query execute function (imported statically)
  • Object with query as string (imported dynamically)
    • this method is necessary when the query uses relative URI references which will be resolved against API base URL
  • Object with load (lazy/dynamic import)
  • Optional endpoint to target a named SPARQL client instead of the default

Supported forms

import { definePage, html } from '@kopflos-labs/pages'
import plaqueQuery from './plaque.rq' // ExecuteConstruct

export default definePage({
  // 1) Direct function
  queries: {
    plaque: plaqueQuery,

    // 2) Explicit object with `query`
    plaqueExplicit: { query: plaqueQuery },

    // 3) Select a named endpoint (must exist in env.sparql)
    plaqueOnNamed: { query: plaqueQuery, endpoint: 'analytics' },

    // 5) Lazy load (declarative dynamic import)
    plaqueRelative: {
      query: './plaque.rq',
      importMeta: import.meta,
    },

    // 5) Lazy load (imperative dynamic import)
    lazyPlaque: {
      endpoint: 'readonly',
      load: () => import('./plaque.rq'),
    },
    
    // 5) lazy load (declarative)
    //    also allowed as object with `query/endpoint` properties
    lazyPlaqueDeclarative: new URL('./plaque.rq', import.meta.url),
  },

  body() {
    return html`...`
  },
})

Note: import attributes cannot be used in load function untile vite supports them. See discussion. To resolve realtive URL references against the API base, pass the query path as URL as shown in the last example.

When using endpoint, the query runs against env.sparql[endpoint].stream. If omitted, the default client env.sparql.default.stream is used.

Parameter binding into queries

All queries receive a parameter map that is available to sparqlc:param(...) inside your .rq files. Parameters are populated from several sources, merged in the following order (later items do not overwrite earlier ones):

  1. Route variables from the page path
  • Example route: pages/plaque/[id].tsid is available
  • Bound as literal values under their plain names: sparqlc:param("id")
  1. URL query string parameters (HTTP GET)
  • Arrays are supported and bound as multiple literal values
  • Empty values are skipped, but the string '0' is kept
  • Example: ?tags=foo&tags=bar&empty=&zero=0
    • sparqlc:param("tags")("foo", "bar")
    • sparqlc:param("zero")"0"
  1. parameters map in the page definition (template expansion)
  • You can provide additional parameters that are only set if not already present from (1) or (2)
  • Keys may be plain strings or CURIEs (e.g., schema:about). CURIEs are expanded using known prefixes
  • Values support simple template substitution with the page variables, using [var]
export default definePage({
  // Bind the page's mainEntity and add custom parameters
  parameters: {
    'schema:about': 'http://example.org/[slug]', // CURIE key, absolute IRI value
    'custom': '[slug]-suffix',                    // plain key, string value
  },
  queries: { details: plaqueQuery },
})

In your query you can then read:

BIND(sparqlc:param(schema:about) AS ?about)
BIND(sparqlc:param("custom") AS ?suffix)
  1. mainEntity convenience binding
  • mainEntity can be a template string too, using [var]
  • If it starts with http, it is used as an absolute IRI
  • Otherwise it is resolved against your app namespace (env.kopflos.appNs), so '[slug]' becomes <app#[slug]>
  • The resulting node is bound under schema:mainEntity, so you can access it as sparqlc:param(schema:mainEntity)
export default definePage({
  // Absolute IRI with template
  mainEntity: 'http://example.org/[slug]',
  // or relative to the app namespace
  // mainEntity: '[slug]',
  queries: { details: plaqueQuery },
})

Summary of precedence when setting a parameter key:

  • If a key is already present from route variables or query string, parameters will not overwrite it
  • mainEntity always sets schema:mainEntity (after template expansion)

6. Navigating the Graph with lit-rdf

The @kopflos-labs/pages plugin leverages lit-rdf to provide a declarative way of binding RDF data to web components.

Declarative Components

  • <rdf-environment>: Injects the RDF environment (including namespaces and utilities) into the component tree.
  • <data-graph>: Binds the results of a SPARQL query to the DOM tree.
  • <target-node>: Focuses on a specific node within the current graph (usually the main resource).
  • <traverse-graph>: Shifts the "focus node" by following a property path (e.g., schema:address/schema:addressLocality).

For full documentation, visit the lit-rdf package.

Creating Data-Bound Components

To create custom components that consume the RDF graph, use the consumeEnvironment and consumeFocusNode mixins from lit-rdf/mixins.js.

import { consumeEnvironment, consumeFocusNode } from 'lit-rdf/mixins.js'
import { html, LitElement } from 'lit'
import { customElement } from 'lit/decorators.js'

@customElement('my-header')
export default class extends consumeEnvironment(consumeFocusNode(LitElement)) {
  render() {
    // this.focusNode is automatically updated by parent <traverse-graph> or <target-node>
    const name = this.focusNode?.value
    
    return html`<h1>${name}</h1>`
  }
}

In your component, this.focusNode is a clownface pointer. You can use methods like .out(), .in(), and .has() to navigate further or extract values.