Skip to content

Configuration

Getting Started: Installation · Quick Start · This Page

Every Alap instance starts with a config object. At minimum you need allLinks — a dictionary of your links. Everything else is optional.

Live version with interactive examples: https://docs.alap.info/getting-started/configuration

Config shape

typescript
interface AlapConfig {
  // --- Data layer (pure JSON, serializable) ---
  allLinks: Record<string, AlapLink>;                          // required
  settings?: AlapSettings;
  macros?: Record<string, AlapMacro>;
  searchPatterns?: Record<string, AlapSearchPattern | string>;

  // --- Code layer (JavaScript only) ---
  protocols?: Record<string, AlapProtocol>;
}

The config has two layers:

  • DataallLinks, macros, settings, searchPatterns. Pure JSON. This is what editors produce, what gets saved to a database, and what can be loaded from a <script type="application/json"> block or a remote endpoint.
  • Codeprotocols. Custom protocol handlers are functions, so they can only be defined in JavaScript/TypeScript. They are part of your application code, not your stored data.

You can define everything at the code layer — write your entire config in a .ts file, skip JSON entirely, and be done. But you can't go the other direction: a pure JSON config gives you links, tags, macros, expressions, and access to Alap's built-in protocols (:web:, :atproto:, :json:, etc.), but no custom protocols. Custom protocols require code.

Most projects land somewhere in between: data lives in JSON (portable, editor-friendly, storable), and the code layer wraps it with any protocols the project needs:

typescript
// alap-config.ts
import linkData from './links.json';           // data layer — from editor, DB, API

export const config: AlapConfig = {
  ...linkData,                                  // allLinks, macros, settings
  protocols: {                                  // code layer — defined in your app
    price: {
      handler: (segments, link) => { /* ... */ },
    },
  },
};

The traditional web model: each link knows its destination. One <a> tag, one href.

Alap flips this. You build a library of links — a single collection of everything you might want to link to — and then your links query into it.

typescript
allLinks: {
  golden_gate: {
    url: 'https://en.wikipedia.org/wiki/Golden_Gate_Bridge',
    label: 'Golden Gate Bridge',
    tags: ['bridge', 'sf', 'landmark'],
  },
  brooklyn: {
    url: 'https://en.wikipedia.org/wiki/Brooklyn_Bridge',
    label: 'Brooklyn Bridge',
    tags: ['bridge', 'nyc', 'landmark'],
  },
  bluebottle: {
    url: 'https://bluebottlecoffee.com',
    label: 'Blue Bottle Coffee',
    tags: ['coffee', 'sf'],
  },
}

Each entry has an ID (the key), a URL, a label, and tags. Both the ID and the tags are queryable:

  • By IDgolden_gate in an expression picks that one specific link. Good for hand-curated picks where you know exactly which item you want.
  • By tag.coffee picks every link carrying the coffee tag. Tags are the connective tissue — they describe what a link is about without deciding where it should appear. Add a new coffee shop to your library tomorrow, and every .coffee query picks it up automatically.

Mix them freely: golden_gate, .coffee + .sf means "the Golden Gate link, plus every SF coffee spot." IDs for precision, tags for breadth.

FieldTypeRequiredDescription
urlstringYesDestination URL
labelstringNoDisplay text (required unless image is set)
tagsstring[]NoTags for .tag queries
cssClassstringNoCSS class applied to the menu item
imagestringNoImage URL rendered instead of text
altTextstringNoAlt text for image
targetWindowstringNo_self, _blank, etc. Default: "fromAlap"
descriptionstringNoUsed by search patterns and hooks
thumbnailstringNoPreview image for hover/context events
hooksstring[]NoEvent hooks this item participates in
guidstringNoPermanent UUID that survives renames
createdAtstring | numberNoISO 8601 or Unix ms. Used by age filters
metaRecord<string, unknown>NoArbitrary metadata for protocol queries

settings — global defaults

Settings control menu behavior. All fields are optional — omit settings entirely to use the defaults.

typescript
settings: {
  listType: 'ul',
  menuTimeout: 3000,
  maxVisibleItems: 6,
  existingUrl: 'prepend',
  placement: 'S',
  placementGap: 8,
  hooks: ['item-hover'],
}
FieldTypeDefaultDescription
listType'ul' | 'ol''ul'Menu list element type
menuTimeoutnumber5000Auto-dismiss timeout (ms) after mouse leaves
maxVisibleItemsnumber10Items before the menu scrolls. 0 = no limit
existingUrl'prepend' | 'append' | 'ignore''prepend'How to handle an existing href on the trigger
placementstring'SE'Comma-separated placement string: compass direction + strategy (e.g. 'SE', 'SE, clamp')
placementGapnumber4Pixel gap between trigger edge and menu edge
viewportPaddingnumber8Minimum distance the menu keeps from viewport edges
viewportAdjustbooleantrueEnable smart placement with viewport containment
preventFocusScrollbooleantruePrevent viewport scrolling when focus moves to menu items on keyboard open
hooksstring[]Default hooks for all items (per-link hooks overrides)

Placement

The placement setting controls where the menu appears relative to the trigger. Think of it as a compass:

     NW    N    NE
      ┌────┬────┐
   W  │ trigger │  E
      └────┴────┘
     SW    S    SE
         (C = centered over trigger)

The default is SE — below the trigger, left edge aligned with the trigger's left edge. The default strategy is flip — if the preferred direction doesn't fit, the engine tries fallback positions.

Add a strategy to control how hard the engine tries: "SE" (flip, default), "SE, clamp" (constrain to viewport), or "SE, place" (pinned, no fallback). When no placement is set at all, the engine doesn't run — CSS positions the menu.

Per-element override: data-alap-placement="N, clamp" (DOM mode) or placement="N, clamp" (web component / framework adapters).

See Placement for the full guide on placement, strategies, and CSS styling.

macros — reusable expressions

Macros name a query so you can reference it with @:

typescript
macros: {
  nyc_bridges:  { linkItems: '.nyc + .bridge' },
  sf_coffee:    { linkItems: '.coffee + .sf' },
  all_bridges:  { linkItems: '@nyc_bridges | @sf_bridges' },
}
html
<alap-link query="@nyc_bridges">NYC bridges</alap-link>

Macros can reference other macros. See Macros for nesting rules and cycle protection.

searchPatterns — named regex searches

Define regex queries you can reference with /name/ syntax:

typescript
searchPatterns: {
  bridges: 'bridge|viaduct',
  recent_coffee: {
    pattern: 'coffee|cafe',
    options: { fields: 'lt', age: '30d', sort: 'newest', limit: 10 },
  },
}

A plain string is shorthand for { pattern: "..." } with default options. See Search Patterns for the full options reference.

protocols — dimensional queries

Protocol expressions extend the query language with domain-specific filtering — time, location, or any custom dimension. Because protocol handlers are functions, they can't live in a JSON config — they must be defined in a JS/TS module (e.g. alap-config.ts), alongside allLinks, macros, and settings:

typescript
const config: AlapConfig = {
  allLinks: {
    bluebottle: {
      label: 'Blue Bottle',
      url: 'https://bluebottlecoffee.com',
      tags: ['coffee', 'sf'],
      meta: { price: 5 },
    },
    stumptown: {
      label: 'Stumptown',
      url: 'https://stumptowncoffee.com',
      tags: ['coffee', 'portland'],
      meta: { price: 4 },
    },
  },
  protocols: {
    price: {
      handler: (segments, link) => {
        if (!link.meta?.price) return false;
        const min = parseFloat(segments[0]);
        const max = parseFloat(segments[1]);
        return link.meta.price >= min && link.meta.price <= max;
      },
    },
  },
};

The :price:0:10: syntax in an expression invokes the price protocol handler, filtering items by their meta.price value:

html
<alap-link query=".coffee + :price:0:10:">affordable cafes</alap-link>

See Protocols for handler contracts and source chains.

Inline configuration

Alap's config is content — labels, URLs, tags, relationships. It can live in the document itself as a <script type="application/json"> block:

html
<script type="application/json" id="alap-config">
{
  "allLinks": {
    "golden":     { "url": "https://...", "label": "Golden Gate",    "tags": ["bridge", "sf"] },
    "brooklyn":   { "url": "https://...", "label": "Brooklyn Bridge", "tags": ["bridge", "nyc"] }
  }
}
</script>

<script>
  const el = document.getElementById('alap-config');
  const config = JSON.parse(el.textContent);
  Alap.registerConfig(config);
</script>

The browser won't execute it, but anything that reads the page — human or machine — can parse it. One artifact, two purposes: runtime configuration and a readable description of the page's link structure.

Complete example

typescript
const config: AlapConfig = {
  settings: {
    listType: 'ul',
    menuTimeout: 5000,
    maxVisibleItems: 8,
    existingUrl: 'prepend',
    hooks: ['item-hover'],
  },

  macros: {
    nyc_bridges:  { linkItems: '.nyc + .bridge' },
    sf_outdoors:  { linkItems: '(.sf + .park) | (.sf + .beach)' },
    staff_picks:  { linkItems: 'golden_gate, bluebottle, highline' },
  },

  searchPatterns: {
    bridges:      { pattern: 'bridge', options: { fields: 'lt', sort: 'alpha' } },
    recent_items: { pattern: '.', options: { age: '7d', sort: 'newest', limit: 5 } },
  },

  allLinks: {
    golden_gate: {
      url: 'https://en.wikipedia.org/wiki/Golden_Gate_Bridge',
      label: 'Golden Gate Bridge',
      tags: ['bridge', 'sf', 'landmark'],
      description: 'Iconic suspension bridge spanning the Golden Gate strait.',
      createdAt: '2026-01-15T10:30:00Z',
    },
    brooklyn: {
      url: 'https://en.wikipedia.org/wiki/Brooklyn_Bridge',
      label: 'Brooklyn Bridge',
      tags: ['bridge', 'nyc', 'landmark'],
      createdAt: '2026-01-20T09:00:00Z',
    },
    bluebottle: {
      url: 'https://bluebottlecoffee.com',
      label: 'Blue Bottle Coffee',
      tags: ['coffee', 'sf'],
      createdAt: '2026-02-10T14:00:00Z',
    },
    highline: {
      url: 'https://www.thehighline.org',
      label: 'The High Line',
      tags: ['park', 'nyc', 'landmark'],
      createdAt: '2026-03-01T11:00:00Z',
    },
  },
};

With this config:

html
<alap-link query="@nyc_bridges">NYC bridges</alap-link>
<alap-link query="@staff_picks">our picks</alap-link>
<alap-link query="/recent_items/">this week</alap-link>
<alap-link query=".landmark - .nyc">non-NYC landmarks</alap-link>

Next steps

  • Expressions — the query language that makes this work
  • Types — full TypeScript interface definitions