type: guide title: "Put a map on the page" description: "Install @honua/sdk-js, point it at a public FeatureServer or OGC API Features endpoint, and render the result on MapLibre. No Honua server and no account required." resource: "https://www.npmjs.com/package/@honua/sdk-js"

Put a map on the page

The SDK's map workflow is five verbs in order: connect → discover → explain → query → mount. This page runs them against a public endpoint. Nothing of Honua's needs to be running, and no account or API key is involved.

1. Install

npm install @honua/sdk-js maplibre-gl

The SDK supports MapLibre GL JS 5 and 6. Use 6.4.1 or newer: every release at or below 6.4.0 carries GHSA-jrc7-96c5-q579, a sanitizer bypass the runtime cannot protect you from. MapLibre 6 is ESM-only and loads its worker as a separate module; a Vite host configures it once with maplibregl.setWorkerUrl(...) before the first map is created (see MapLibre 5 and 6 compatibility).

2. Connect, query, explain

This runs against a public Esri Living Atlas FeatureServer and walks the first verbs without a map. Capability gaps throw HonuaCapabilityNotSupportedError rather than returning empty data.

import { connect, explainQuery, envelope, queryFilter, type Query } from "@honua/sdk-js";

// 1. connect — a public FeatureServer; nothing of Honua's is running.
const data = await connect({
  endpoint:
    "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/2020_Census_State_Apportionment/FeatureServer/0",
  protocol: "auto",
  authorizationScopeFingerprint: "public",
});
const states = data.source<{ NAME: string; Total_Pop_2020: number }>();

// 2. query — one typed, protocol-neutral filter expression.
const query: Query = {
  filter: queryFilter.and(
    queryFilter.gt("Total_Pop_2020", 1_000_000),
    queryFilter.spatial("intersects", envelope(-125, 24, -66, 50)),
  ),
  outFields: ["NAME", "Total_Pop_2020"],
  pagination: { limit: 100 },
};

// 3. explain — the serializable plan, inspectable before anything executes.
const plan = explainQuery({ descriptor: states.descriptor, query });
console.log(plan.fingerprint, plan.steps.map((step) => `${step.engine}:${step.operation}`));

const result = await states.queryAll(query);
console.log(`Loaded ${result.features.length} states`);

Query.filter compiles to GeoServices SQL-92, CQL2, FES 2.0, OData $filter, or DuckDB SQL depending on the endpoint, so the same query runs against any GeoServices, OGC API Features, WFS, OData, or STAC source.

3. Mount it on a map

The kernel connection can own the whole path from discovery to a rendered MapLibre layer. Give it a container and the renderer:

import { createHonua } from "@honua/sdk-js";
import { maplibreRenderer } from "@honua/sdk-js/runtime";
import * as maplibregl from "maplibre-gl";

const endpoint =
  "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/2020_Census_State_Apportionment/FeatureServer/0";

const honua = createHonua();
const connection = await honua.connect(
  { url: endpoint, protocol: "geoservices-feature-service" },
  { authorizationScopeFingerprint: "anonymous-public" },
);
const inspection = await connection.inspect();
const sourceId = inspection.defaultSourceId ?? inspection.sources[0]?.descriptor.id;
const plan = await connection.explain({ returnGeometry: true, pagination: { limit: 250 } }, { sourceId });
const mounted = await connection.mount("#map", { renderer: maplibreRenderer(maplibregl), query: plan, sourceId });
await mounted.ready; // first usable frame, not merely source mutation

// Later: releases the connection, its layers and sources, and the owned map.
await honua.dispose();

inspect() reports the sources and capabilities the endpoint advertises. explain() returns the accepted plan, and mount() executes exactly that plan for the renderer, so what is drawn is what was reviewed. Pass an existing maplibregl.Map instead of a selector to borrow a map you already own.

For an OGC API Features landing page, use protocol: "ogc-features" and add collectionId when the service exposes more than one collection. For a lower-level, caller-owned map with popups, hover, filters and fit-bounds controls, use mountSource.

Start from a scaffold instead

npm create honua-app@0.1.6 my-map -- --sdk-version 0.1.13
cd my-map
npm install
npm run dev

The starter already contains the code above, served against a committed fixture so it works with no third-party network call; set VITE_HONUA_ENDPOINT to run it against a live endpoint. See Scaffold a new app, or open the same starters in a browser from Zero-install playgrounds.

The full example

examples/maplibre-quickstart is the complete version of this page: endpoint input, a table and filter over the bounded result, popup selection, the accepted plan and its degradation reasons, copyable code, and managed cleanup. It runs from a clone of the repository (Node.js >=20.19 and git):

git clone --depth 1 https://github.com/honua-io/honua-sdk-js.git
cd honua-sdk-js
npm ci

Then run the fixture lane from the clone, and return to where you started:

npm run demo:quickstart:mock -- --evidence-once   # build, serve the fixture, probe it once, exit
cd ..

The mock lane serves a committed fixture and needs no network. To keep the server running instead, go back into the clone (cd honua-sdk-js), run npm run demo:quickstart:mock without -- --evidence-once, and open the printed quickstartMockUrl in a browser. To run it against a public endpoint, copy examples/maplibre-quickstart/.env.example in the clone to .env, set VITE_HONUA_QUICKSTART_ENDPOINT and VITE_HONUA_QUICKSTART_PROTOCOL (auto, geoservices-feature-service, or ogc-features), and run npm run demo:quickstart from the clone. Never put an API key or bearer token in a VITE_* variable: Vite embeds those values in public JavaScript.

If the map does not render, see When the quickstart does not render.