Alap v3.2 — Rethinking the Humble Link

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.

Built on What Already Works

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).

What's New in v3.2

A digest of the big changes since v3.1:

  • Progressive async rendering — every menu, lens, and lightbox opens instantly. Fetch-backed content settles in place with FLIP transitions. No mystery pauses.
  • :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.
  • Markdown → Obsidian pipeline. 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).
  • RSS / Atom → markdown pipeline. 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.
  • Lens renderer. A structured metadata card for detail inspection. Clickable tags, image zoom, set navigation. Not every click wants to be a menu item.
  • Lightbox renderer. Fullscreen media with zoom and carousel navigation. For photo-heavy sets where the visual is the content.
  • :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 parity. Every trigger-attached renderer — menu, lens, lightbox, web component — now parses the same placement="SE, clamp" grammar with the same strategy behavior.
  • Data-only configs. Handlers move out of the config object: 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.
  • Security pass. A focused security-release pass closed 29 follow-on findings on top of the existing per-protocol guards. New: tiered trust model (sources stamped 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.
  • 3.0 and 3.1 unsupported as of this release. The 3.2 security work isn't practically backportable.

An Expression Language for Links

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>

Progressive Async — Menus That Open Instantly

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.

Beyond the Menu

The default dropdown is one shape. v3.2 ships two more, production-ready:

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.

Your Own Knowledge Base

: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.

Works Everywhere You Do

Framework Adapters

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.

Integrations

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

Plugins

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

Server-Side Parsing in Your Language

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.

Tested, Not Just Written

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.

Proof-of-Concept Editors

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.

See It in Action

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

Author Note

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 :)