v3.1 shipped the plumbing — the expression language, nine framework adapters, six parser ports, every way to put a link on a page.
v3.2 fills it. Live sources from Hacker News, Bluesky, and the web. Vault-backed menus from your own Obsidian notes. New renderers beyond drop-downs.
That's Alap v3.2.
Alap extends the idea of what we expect from a link. Dynamic menus via a simple attribute and a lightweight expression language. No proprietary formats, no lock-in, no framework required.
The project is open source (Apache 2.0) and published across five package registries (npm, and language parsers on crates.io, PyPI, Go Modules, and Packagist).
A digest of the big changes since v3.1:
:hn: protocol — Hacker News as a live source. Listings, user submissions, Algolia search, single items — CORS-friendly, no auth, runs browser-side or server-side. Bonus: a tutorial walks through how this example protocol was written.:obsidian: protocol - Obsidian integration. Turn any Obsidian vault into a live link source — Core mode (filesystem, zero plugin) or REST mode (Local REST API plugin for fuzzy search). Inline #tag scanning in note bodies and tag aliases for Obsidian tag shapes Alap's grammar can't accept literally.vault_convert.py turns any markdown tree into a vault Alap can read. Handles Hugo / Jekyll / MkDocs / Docusaurus shortcodes via a plugin system. Trust-model aware: source content is treated as untrusted by default, with explicit opt-in flags to relax each strip category (unsafe HTML, frontmatter HTML, active-content blocks).feed_to_md.py lets you drop a feed archive into the front of the converter pipeline — so a year of subscribed feeds becomes a browsable, queryable vault.:location: sub-modes. :location:radius:lat,lng:5mi: and :location:bbox:…: replace the earlier positional form. Sub-mode verbs leave room for polygon, geojson, and route variants to land cleanly.placement="SE, clamp" grammar with the same strategy behavior.new AlapEngine(config, { handlers: { web: webHandler } }). Configs themselves are deep-frozen at validation time. One-line migration in docs/migration-3_2-handlers-out-of-config.md; adapter examples and integration packages already updated.author / protocol:* / storage:*, sanitizers apply tier-appropriate strictness), socket-level SSRF re-check at connect time (closes the gap left by the syntactic hostname check), settings.hooks doubles as an allowlist for non-author-tier links, refcounted in-flight fetch cancellation, and a metadata-key blocklist. Full breakdown in docs/api-reference/security.md.Tag-based query language. Use regular anchors with Alap attributes or the <alap-link> web component — your choice:
<!-- Standard anchor with Alap attributes --> <a class="alap" data-alap-linkitems=".coffee">coffee shops</a> <!-- Or as a web component --> <alap-link query=".coffee">coffee shops</alap-link> <!-- Intersection — NYC bridges only --> <alap-link query=".nyc + .bridge">NYC bridges</alap-link> <!-- Specific items by ID, plus a tag query --> <alap-link query="golden_gate, .coffee + .sf">the bridge, or coffee nearby</alap-link> <!-- Macro — a named set of favorites --> <alap-link query="@favorites">favorites</alap-link>
Tags, IDs, intersections, unions, subtraction — compose menus from a shared link library. Macros let you name and reuse common sets. The grammar is small enough to learn in minutes, powerful enough to express complex selections.
Protocols pull live data. v3.2 ships with :web:, :atproto:, :json:, :hn:, :obsidian:, :time:, and :location: built in:
<!-- Hacker News — top stories from the last 24 hours --> <alap-link query=":hn:top: + :time:1d: *limit:10*">HN today</alap-link> <!-- Obsidian vault — notes tagged #research, most recent first --> <alap-link query=":obsidian:core:research: *sort:modified* *limit:5*">Recent research notes</alap-link> <!-- Live search — Open Library JSON API --> <alap-link query=":web:books:decentralization:">Books: "decentralization"</alap-link> <!-- AT Protocol — recent posts from a Bluesky account --> <alap-link query=":atproto:feed:eff.org:limit=3:">EFF — recent posts</alap-link> <!-- Mix static tags, Hacker News, and Bluesky in one expression --> <alap-link query="(.orgs *limit:2*), (:hn:search:$ai_safety:), (:atproto:feed:eff.org:limit=3:)"> AI safety: orgs + HN + EFF </alap-link>
A protocol fetch takes real time. In v3.1, the menu waited. In v3.2, it doesn't.
Every trigger now opens instantly with a loading placeholder and swaps in the resolved content in place — with a FLIP transition so the menu doesn't jump. In-flight duplicates are deduped, aborts cancel pending fetches cleanly, and timeouts apply per-request. The same pipeline runs under the menu, lens, and lightbox renderers, and all seven framework adapters honor it.
[data-alap-placeholder="loading|error|empty"] lets you style each state from CSS. No runtime branching, no framework-specific hooks — your existing stylesheet already controls it.
The default dropdown is one shape. v3.2 ships two more, production-ready:
meta fields. Good for places where the menu item wants a second look, not a navigation.All three renderers compose through RendererCoordinator — open a lens while a menu is showing, the menu dismisses. Escape cascades through the stack. One expression, three possible shapes, one coordination layer.
:obsidian:core: turns an Obsidian vault into a live link source. Zero plugin required — it reads the filesystem directly.
// server.mjs
import { AlapEngine } from 'alap';
import { obsidianHandler } from 'alap/protocols/obsidian';
// Data-only protocol config
const config = {
protocols: {
obsidian: {
vault: 'MyVault',
vaultPath: '/absolute/path/to/vault',
},
},
};
// Handler passed at engine construction
const engine = new AlapEngine(config, { handlers: { obsidian: obsidianHandler } });
Then in the page:
<alap-link query=":obsidian:core:rust: *limit:5*">Rust notes</alap-link>
Substring grep across frontmatter and body, $preset for named field narrows, inline #tag support in note bodies, alias map for Obsidian tag shapes that collide with Alap's grammar. Symlink rejection, path-traversal guard, per-file size cap, file-count cap — on by default.
:obsidian:rest: adds fuzzy search via the Local REST API plugin when Obsidian is running. Loopback-gated TLS, API-key redaction in logs, path-traversal guard on every vault GET.
Don't have a vault yet? vault_convert.py turns a markdown tree (including Hugo / Jekyll / MkDocs / Docusaurus sites) into one. feed_to_md.py turns an RSS or Atom archive into markdown ready for the converter. Trust-model-aware sanitization on every step.
Idiomatic components, hooks, and state management — not a wrapper around a generic widget.
| Adapter | Components | Notable |
|---|---|---|
| DOM | <alap-link> web component |
Zero dependencies, no build step |
| React | AlapProvider, AlapLink, useAlap() |
Hooks and context |
| Vue | AlapProvider, AlapLink, useAlap() |
Composition API |
| Svelte | AlapProvider, AlapLink, useAlap() |
Svelte 5 |
| Solid | AlapProvider, AlapLink, useAlap() |
Fine-grained reactivity |
| Qwik | AlapProvider, AlapLink, useAlap() |
Resumable, lazy by default |
| Alpine | x-alap directive |
No build step needed |
| Astro | AlapLink.astro, AlapSetup.astro |
Works in .astro and MDX |
React, Vue, Svelte, Solid, and Qwik support three rendering modes: DOM, Web Component, and Popover API. Every adapter includes keyboard navigation, ARIA accessibility, click-outside dismissal, and the compass-based placement engine.
Drop Alap into your static-site generator or documentation platform with zero-config setup.
| Integration | Platform | What It Does |
|---|---|---|
| astro-alap | Astro | Auto-injects web component setup; optional remark plugin for .md and .mdx |
| eleventy-alap | Eleventy | Dual-mode: static build-time resolution (plain <ul>) or interactive web components |
| hugo-alap | Hugo | Hugo module with alap shortcode; full expression syntax client-side |
| next-alap | Next.js | 'use client' re-exports; AlapLayout for app/layout.tsx; optional MDX support |
| nuxt-alap | Nuxt 3 | Client plugin factory; Vue component re-exports; Nuxt Content markdown support |
| vitepress-alap | VitePress | Vite plugin for custom element registration; use <alap-link> directly in markdown |
| qwik-alap | Qwik City | Vite plugin with auto-injection; supports both web components and Qwik components |
Content-level transforms for CMS pipelines, rich-text editors, and markdown toolchains.
| Plugin | Environment | What It Does |
|---|---|---|
| remark-alap | Markdown (unified) | Transforms [text](alap:query) links to <alap-link> web components |
| rehype-alap | HTML (unified) | Transforms <a href="alap:query"> for headless CMS content (Contentful, Sanity, WordPress REST, etc.) |
| mdx | MDX / React | Remark plugin + React provider; emits <AlapLink> components resolved via context |
| tiptap-alap | Tiptap / ProseMirror | Inline <alap-link> nodes with insert/update/remove commands; Mod-Shift-A shortcut |
| WordPress | WordPress (PHP) | [alap query=".tag"] shortcode; SQLite-based; file config, no database tables |
Idiomatic parsers in six languages — implementations that feel native to each ecosystem, not mechanical ports.
| Language | Package | Capabilities |
|---|---|---|
| Rust | alap (crates.io) |
Expression parsing, config merging, URL sanitization |
| Python | alap-python (PyPI) |
Expression parsing, config merging, ReDoS guard |
| Go | alap-go (Go modules) |
Expression parsing, config merging, URL sanitization |
| PHP | danielsmith/alap (Packagist) |
Expression parsing, config merging, ReDoS validation |
| Ruby | alap gem |
Expression parsing, config merging, SSRF guard |
| Java | info.alap |
Expression parsing, config merging, URL sanitization |
All six implement the same expression grammar — item IDs, tag queries, operators, macros, regex search, grouping, protocols, refiners — with full test suites.
1,926 tests across 89 files — core parser, every framework adapter, integrations, plugins, protocols (:hn:, :obsidian:, :web:, :json:, :atproto:, :location:, :time:), renderers (menu, lens, lightbox, embed), progressive async pipeline, security (URL sanitization, SSRF guard, ReDoS protection, path traversal, XSS fixes, config validation, tiered trust model). Plus Python tests covering the markdown-to-vault and feed-to-markdown pipelines.
Eight config editors in as many frameworks — React, React ShadCN, Vue, Svelte, Solid, Alpine, Astro, and a React Design reference — to prove the same editing experience works consistently across stacks.
Each editor supports JSON import/export and can call any of the 11 example servers (Express, Bun, Hono, Flask, Django, FastAPI, Laravel, Gin, Axum, Spring Boot, Sinatra) for server-side resolution. Same UI: build a link library, write test queries, preview resolved menus — all with drag-and-drop link import and client-side metadata extraction.
The point isn't that you need all of these editors. It's that any framework can drive the same config surface with the same results.
Live examples across every adapter and protocol — DOM, web components, React, Vue, Svelte, Solid, Qwik, Alpine, Astro, htmx, CMS content, markdown, Hacker News, Obsidian, the lightbox renderer, the lens renderer, and all-together (menu + lens + lightbox on one page sharing one config).
Browse the examples | Documentation | GitHub | npm
npm install alap
I'm Daniel Smith, and I've been thinking about aspects of this for 30 years. I published an early version of this back in 2012, and rewrote it in 2022, with an eye towards Vue and React. Version 3 is a complete rewrite in TypeScript. I worked in tandem with Claude Code to get this going, and I assure you this is no vibe coding project :)