@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/pages2. 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
CONSTRUCTqueries 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
.rqfile compiled bysparqlc) - Object with
queryexecute function (imported statically) - Object with
queryas 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
endpointto 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):
- Route variables from the page path
- Example route:
pages/plaque/[id].ts⇒idis available - Bound as literal values under their plain names:
sparqlc:param("id")
- 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"
parametersmap 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)mainEntityconvenience binding
mainEntitycan 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 assparqlc: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,
parameterswill not overwrite it mainEntityalways setsschema: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.
