- /
-
- Web Components /
- Building components
Building components
How to create a new OP web component using the shared store pattern
Last modified 2026-05-01
Table of content
This development pattern is accurate to the system in the standard OP server profile provided by the Octothorpes project. Different OP servers and apps may have different approaches, or decline to host web components at all.
The pattern
Each component is a Svelte custom element compiled to a standalone JS bundle. The build target is a plain .js file that anyone can load with a <script> tag — no framework, no npm, no build step on their end.
The store (octo-store.js) is shared across all components. It owns the fetch lifecycle: building the query URL, setting loading/error state, and exposing results as reactive Svelte stores. Components only declare their props and render the data. Adding a new component mostly means picking an endpoint and writing a template.
At compile time, each component and its share of the store get bundled together:
YourComponent.svelte ← props, template, styles
↓ imports
octo-store.js ← fetch, loading state, error handling
↓ queries
/get/[what]/[by] ← OP API endpoint
↓ npm run build:components
static/components/your-component.js ← self-contained bundle
Quick start
1. Copy the template
cp src/lib/web-components/shared/COMPONENT_TEMPLATE.svelte \
src/lib/web-components/your-component/YourComponent.svelte
2. Set the component name and endpoint
Change the customElement name at the top of the file:
<svelte:options customElement="octo-webring" />
Change the query endpoint to match the data you want:
const query = createOctoQuery('domains', 'in-webring');
See available endpoints below. Optionally, customize the rendering template below the CUSTOMIZE comment in the file.
3. Add to the build config
// vite.config.components.js
entry: {
'octo-thorpe': resolve(__dirname, 'src/lib/web-components/octo-thorpe/OctoThorpe.svelte'),
'your-component': resolve(__dirname, 'src/lib/web-components/your-component/YourComponent.svelte')
}
4. Build
npm run build:components
The output lands at static/components/your-component.js and is served automatically by SvelteKit.
Available endpoints
Pass these as createOctoQuery(what, by):
what |
by |
Returns |
|---|---|---|
pages |
thorped |
Pages tagged with octothorpes |
pages |
linked |
Pages linking to specific URLs |
pages |
backlinked |
Pages backlinking to URLs |
pages |
bookmarked |
Pages bookmarking URLs |
pages |
posted |
Pages from specific domains |
pages |
in-webring |
Pages in a webring |
domains |
in-webring |
Domains in a webring |
domains |
posted |
Domains that have posted |
thorpes |
used |
Octothorpe terms that have been used |
everything |
thorped |
All items tagged with octothorpes |
everything |
posted |
All items from specific domains |
The store
createOctoQuery returns a Svelte store with three pieces of reactive state:
import { createOctoQuery } from '../shared/octo-store.js';
const query = createOctoQuery('pages', 'thorped');
await query.fetch({ server: 'https://your-op-server.example.com', o: 'demo', limit: 10 });
$query.results // Array of result objects
$query.loading // Boolean
$query.error // String or null
$query.count // Number of results
query.fetch() accepts the same parameters as the HTML attributes: s, o, nots, noto, match, limit, offset, when, server.
Standard props
Copy these into every component — web components require individual exports for HTML attributes.
export let server = new URL(import.meta.url).origin;
export let s = '';
export let o = '';
export let nots = '';
export let noto = '';
export let match = '';
export let limit = '10';
export let offset = '0';
export let when = '';
export let autoload = false;
export let render = 'list';
$: params = { server, s, o, nots, noto, match, limit, offset, when };
Testing
Create a demo HTML file in static/components/:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Your Component</title></head>
<body>
<your-component o="test" autoload></your-component>
<script type="module" src="/components/your-component.js"></script>
</body>
</html>
Then run npm run dev and open http://localhost:5173/components/your-component-demo.html.
Troubleshooting
Component doesn't appear
- Verify the
customElementname is unique across the codebase - Confirm the entry is in
vite.config.components.js - Run
npm run build:componentsand check the browser console
Attributes not working
- HTML attributes are lowercase: use
autoload, notautoLoad - Boolean attributes don't need a value:
autoload, notautoload="true"
Styles not applying
- CSS custom properties must be on
:host - Component styles are scoped — they can't target children outside the shadow DOM
- Inspect the shadow DOM in DevTools