These examples use the current Roam Map outline grammar and correspond to
fixture shapes exercised in Roam Desktop. Placeholder page titles such as
[[People]]/Example Person should be replaced with pages from your graph.
They use ordinary Roam blocks: indentation below represents child blocks.
For the concepts behind each form, supported values, extension points, and current limitations, see the customization reference.
- Give each location page a valid
Coordinates:: geo:latitude,longitude, either directly or through Roam Places. - Put each page reference or block-backed point in its own block beneath
{{map}}. - Use a real Roam code block as the only child of each
MapLibre layeror inlineMarker clickblock. - Enable Roam's custom-components setting before using a
Marker clickcode block. - Keep network attribution visible.
{{map}}
[[Port Louis]]
[[Curepipe]]
[[Mahébourg]]
The map follows source order, deduplicates repeated page references, and fits the first non-empty result once. Later graph edits update the data without discarding the user's viewport. Clicking a marker opens the stock Blueprint card; Open in sidebar uses the source entity's stable UID.
The first fixture is a saved native Roam query. It returns the roam/meta::
blocks beneath located Effort pages; Roam Map promotes each result block to its
containing Effort page, whose location metadata supplies the point.
Located Efforts:
{{map}}
{{[[query]]: {and: [[Efforts]] {search: roam/meta::}}}}
The adjacent map independently executes that saved query by block UID and maps the containing Effort pages. It does not read the rendered result DOM.
Located People:
{{map}}
```clojure
[:find [?uid ...]
:where
[?page :node/title ?title]
[(clojure.string/starts-with? ?title "[[People]]/")]
[?coordinate-block :block/page ?page]
[?coordinate-block :block/string ?coordinates]
[(clojure.string/starts-with? ?coordinates "Coordinates::")]
[?page :block/uid ?uid]]
```
The more interesting fixture asks which [[San Francisco]]/… location pages
were mentioned in the same daily-note block as a Person page:
{{map}}
```clojure
[:find [?place-uid ...]
:where
[?person :node/title "[[People]]/Example Person"]
[?place :node/title ?place-title]
[(clojure.string/starts-with? ?place-title "[[San Francisco]]/")]
[?mention :block/refs ?person]
[?mention :block/refs ?place]
[?mention :block/page ?daily-page]
[?daily-page :log/id ?date]
[?place :block/uid ?place-uid]]
```
The shared ?mention variable means the person and place must occur in the
same block; :log/id restricts that block to a daily-note page. Joining only on
the same daily-note page can find extra candidates without establishing the
same relationship. The narrower query chooses precision instead of silently
asserting it.
The same shape can be reused with a second Person page:
{{map}}
```clojure
[:find [?place-uid ...]
:where
[?person :node/title "[[People]]/Example Person B"]
[?place :node/title ?place-title]
[(clojure.string/starts-with? ?place-title "[[San Francisco]]/")]
[?mention :block/refs ?person]
[?mention :block/refs ?place]
[?mention :block/page ?daily-page]
[?daily-page :log/id ?date]
[?place :block/uid ?place-uid]]
```
Returned place pages still need location metadata. Roam Map does not rename, classify, geocode, or otherwise rewrite them as a rendering side effect.
Use [:find [?uid ...] ...] for a flat UID collection or [:find ?uid ...]
for a one-column UID relation. A query that returns titles, entity IDs, maps,
or several columns is deliberately rejected. The same source may instead be
saved elsewhere and included as one direct-child block reference.
Do not use Roam-specific current/* symbols in these fenced definitions. They
depend on a rendered :q component context, while Roam Map executes fenced
Datalog text directly through the frontend API.
The native fixture keeps returned pages and turns returned blocks into their containing pages. It does not infer which referenced page inside a result block might be the intended place. Datalog is therefore the clearer choice when the desired map entity differs from the result block's containing page.
This fixture is also present on the live [[Roam Map Test]] page. It shows the
two supported block-backed forms together:
{{map}}
geo:-20.1609,57.5012;u=14.4
Curepipe block point
Coordinates:: geo:-20.3163,57.5251
Address:: Curepipe, Mauritius
The first marker belongs to the bare geo: block. The second belongs to the
named parent block, whose child supplies its coordinates. Opening either
marker in the sidebar opens that source block. The u=14.4 parameter records
14.4 metres of uncertainty; it does not move the point.
Hover over a map to reveal the small grip just outside its bottom-right corner.
Drag it and Roam Map creates or updates one ordinary map/size child. The same
result can be authored directly:
{{map}}
map/size:: 900 × 520
[[Port Louis]]
[[Curepipe]]
The first number is a responsive maximum width; the second is height. Use
auto for either dimension, such as map/size:: auto × 520. Use Reset map
size in the toolbar to remove the child and restore both defaults.
This is the tested People-map pattern. Each person page has a location and a
Profile Picture containing complete HTTP(S) image Markdown:
Profile Picture:: 
Coordinates:: geo:34.0522,-118.2437
The map adds two ordinary MapLibre layers over Roam Map's compiled source. The base circle keeps every point visible, including pages with missing or broken images. The symbol layer presents pages with a registered picture.
{{map}}
[[People]]/Andy Matuschak
[[People]]/Bret Victor
[[People]]/Alan Kay
MapLibre layer
```json
{
"id": "people-base",
"type": "circle",
"source": "roam-map-features",
"paint": {
"circle-radius": 12,
"circle-color": "#6f42c1",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 2
}
}
```
MapLibre layer
```json
{
"id": "people-portraits",
"type": "symbol",
"source": "roam-map-features",
"filter": ["has", "Profile Picture"],
"layout": {
"icon-image": [
"coalesce",
["image", ["concat", ["get", "Profile Picture"], "#circle"]],
["image", "roam-map/default-marker"]
],
"icon-size": 1,
"icon-overlap": "always"
}
}
```
The compiler projects the readable attribute title Profile Picture as the
feature key. Valid image Markdown becomes an opaque runtime image ID. Roam Map
loads Roam-hosted files through Roam's supported file API, center-crops them,
and registers both square and alpha-clipped circular variants. Appending
#circle selects the circular registration without exposing the underlying URL
to the MapLibre expression.
If your pages use Image instead, change both occurrences of Profile Picture
in the symbol layer to Image.
This tested example replaces the stock marker card with graph-authored JSX. It
receives only the click snapshot, then uses the clicked entityUid and the Roam
Alpha API to read Image::, Description::, and URL:: itself. The image is
not passed through a special plugin argument.
The component reuses Roam Map's public MarkerPopover, MarkerCard, details,
and action components. A graph author could replace any of those pieces or
return null and run only an effect.
{{map}}
[[[[Efforts]]/Coral Vita]]
[[[[Efforts]]/PlanetCare]]
[[[[Efforts]]/Open Source Ecology]]
Marker click
```jsx
function effortMarkerClick({ args }) {
const context = JSON.parse(decodeURIComponent(args[1]));
const {
MarkerCard,
MarkerCardActions,
MarkerCardDetails,
MarkerPopover,
} = window.RoamMap.components;
const entityAttributesPull = `[
{:harc/_e [
{:harc/a [:node/title]}
{:harc/v [:node/title :block/string :harc/v-string :harc.text/string]}
]}
{:block/children [
:block/string
{:harc/_e [
{:harc/a [:node/title]}
{:harc/v [:node/title :block/string :harc/v-string :harc.text/string]}
]}
{:block/children [:block/string]}
]}
]`;
const toArray = (value) =>
value == null ? [] : Array.isArray(value) ? value : [value];
function displayValue(value) {
if (["string", "number", "boolean"].includes(typeof value)) return value;
return value?.[":node/title"] ??
value?.[":block/string"] ??
value?.[":harc/v-string"] ??
value?.[":harc.text/string"] ??
null;
}
function addAttribute(attributes, title, rawValue) {
const name = typeof title === "string" ? title.trim() : "";
const display = displayValue(rawValue);
const value = typeof display === "string" ? display.trim() : display;
if (!name || value == null || value === "") return;
const values = attributes[name] ?? (attributes[name] = []);
if (!values.includes(value)) values.push(value);
}
function collectCurrentAttributes(attributes, entity) {
for (const relation of toArray(entity?.[":harc/_e"])) {
const title = toArray(relation[":harc/a"])[0]?.[":node/title"];
for (const value of toArray(relation[":harc/v"])) {
addAttribute(attributes, title, value);
}
}
}
function collectAttributeBlock(attributes, block) {
const source = String(block?.[":block/string"] ?? "");
const separator = source.indexOf("::");
if (separator < 1) return;
addAttribute(attributes, source.slice(0, separator), source.slice(separator + 2));
}
function readEntityAttributes(entityUid) {
const entity = window.roamAlphaAPI.data.pull(
entityAttributesPull,
[":block/uid", entityUid]
);
const attributes = {};
// Read current HARC attributes and compatibility `Attribute:: value` blocks.
collectCurrentAttributes(attributes, entity);
for (const child of toArray(entity?.[":block/children"])) {
collectCurrentAttributes(attributes, child);
const attributeBlocks = child[":block/string"] === "roam/meta::"
? toArray(child[":block/children"])
: [child];
attributeBlocks.forEach((block) => collectAttributeBlock(attributes, block));
}
return attributes;
}
const firstValue = (attributes, title) => attributes[title]?.[0] ?? null;
function imageUrlFromMarkdown(value) {
if (typeof value !== "string") return null;
return value.trim().match(/^!\[[^\]]*\]\((https?:\/\/.+)\)$/)?.[1] ?? null;
}
return (
<MarkerPopover context={context}>
{({ close }) => (
<MarkerCard context={context} onClose={close}>
{(card) => {
const attributes = readEntityAttributes(card.entityUid);
const imageUrl = imageUrlFromMarkdown(firstValue(attributes, "Image"));
const description = firstValue(attributes, "Description");
const website = firstValue(attributes, "URL");
const label = card.feature.properties["roam/label"];
return (
<>
{imageUrl && (
<img
src={imageUrl}
alt={label}
style={{ width: "100%", height: 110, objectFit: "contain" }}
/>
)}
<MarkerCardDetails {...card} />
{description && <p>{description}</p>}
{typeof website === "string" && (
<a href={website} target="_blank" rel="noreferrer">
Visit website ↗
</a>
)}
<MarkerCardActions {...card} />
</>
);
}}
</MarkerCard>
)}
</MarkerPopover>
);
}
```
The context contains version, mapUid, clickId, entityUid,
identityKind, entityUids, coincidentEntityUids, feature, features,
point, lngLat, clientPoint, and keyboard modifiers. entityUid
identifies the nearest marker; coincidentEntityUids preserves sources
occupying the same visible point.
To reuse the same component on several maps, put the JSX in one code block and reference that exact block:
Marker click
((reusable-code-block-uid))
Roam Map watches referenced code, invokes it through Roam's documented
renderString API, and calls unmountNode when the component is replaced or
the map is removed.
This example uses a complete style URL that is neither built into Roam Map nor derived from its MapTiler shortcut. The URL is MapLibre's public demonstration style and requires no key.
Open Settings → Roam Depot → Roam Map → Basemap catalog, then add:
| Field | Value |
|---|---|
| Name | MapLibre Demo |
| Format | Complete MapLibre style URL |
| Style URL | https://demotiles.maplibre.org/style.json |
After saving the catalog entry, use its readable name in Roam:
{{map}}
map/basemap:: MapLibre Demo
[[Port Louis]]
[[Curepipe]]
Roam Map passes the URL to Map#setStyle, then restores its compiled feature
source, marker layer, authored layers, and runtime images after the external
style loads. The URL is saved once in graph-synced extension settings; map
blocks contain only its name. The demo endpoint currently returns a valid
version-8 style with browser CORS enabled, but it is a demonstration service,
not a promise of production hosting.
Use a readable map attribute to save a basemap choice without changing any place page:
{{map}}
map/basemap:: EOX Satellite Context
[[Port Louis]]
[[Curepipe]]
EOX Satellite Context is the attributed 2016 global Sentinel-2 mosaic at 10 m resolution. It is useful for regional context through zoom 14; it is not current, building-level satellite photography. The toolbar may preview another catalog entry without overwriting this saved value.
For current higher-resolution imagery, configure a MapTiler browser key in the Roam Map settings and use either:
map/basemap:: MapTiler Satellite
or:
map/basemap:: MapTiler Hybrid
The key is graph-synced and appears in browser requests, so treat it as a restricted public browser key rather than a secret.
Check the map's source, mapped, and unmapped counts and expand its diagnostics.
Common causes are an invalid Coordinates geo URI, multiple distinct page links
in one source block, a Datalog result that is not a UID collection, invalid
layer JSON, a duplicate or reserved layer ID, a foreign MapLibre source ID, or
disabled custom components.
Roam Map deliberately reports these locally and does not repair or rewrite the source outline.
