mapsight / embed
PHP host library: emit Mapsight embed fragments (client-only + SSR try/fallback)
Requires
- php: ^8.2
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- ext-curl: Separate connect vs total timeouts when calling the SSR sidecar
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-12 18:38:13 UTC
README
PHP adapter for the Mapsight embed protocol. It emits a fragment you splice
into a page you already own: stylesheet, modulepreload, mount container,
optional SSR try, mountEmbed boot.
Composer package: mapsight/embed. Source: open-mapsight/embed (GitHub org).
Those names differ on purpose — Packagist vendor vs GitHub org.
Preset name and config are opaque. You own wrappers, first-paint chrome,
and <head>. This package does not.
composer require mapsight/embed:^0.4
Requires PHP 8.2+. MIT.
Who owns what
┌──────────────────────── host page (your CMS) ─────────────────────────┐
│ layout, wrappers, <head> │
│ │
│ ┌────────────── this library ──────────────┐ │
│ │ <link mapsight.css> │ │
│ │ <link rel=modulepreload> │ │
│ │ <div id="…"> ← empty on miss │ │
│ │ <script type=module> ← mountEmbed │ │
│ │ │ │ │
│ │ │ POST /v1/render │ ┌───────────────┐ │
│ │ └───────────────────────────────►│────►│ SSR sidecar │ │
│ │ JSON { html, state, │ │ (optional, │ │
│ │ pageMeta } │ │ private) │ │
│ └──────────────────────────────────────────┘ └───────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
│
▼
browser: preset.js + mountEmbed
reads data-dehydrated-state if present
| Piece | Owner |
|---|---|
| Page shell, wrappers, preset chrome | Host |
| Preset string + embed config | Host (forwarded as-is) |
| Assets, mount container, boot script | This library |
| Sidecar try / timeout / circuit breaker / fail-open | This library |
| React render + dehydrated GIS state | Sidecar (ghcr.io/open-mapsight/ssr-sidecar) + your render.js |
| Hydrate in the browser | @mapsight/ui mountEmbed |
SSR is acceleration, not a hard dependency. If the sidecar is down, slow, or returns junk, the page still boots client-only.
request
│
├─ no SsrClient ───────────────────────► empty <div id> + boot
│
├─ cache hit ──────────────────────────► cached fragment
│
├─ breaker open ───────────────────────► <!-- mapsight-ssr-skipped -->
│
├─ POST /v1/render
│ │
│ ├─ 2xx + html ─► sidecar fragment (data-dehydrated-state)
│ │
│ └─ miss / timeout / 5xx ─► <!-- mapsight-ssr-skipped -->
│ empty <div id> + boot
└─ browser always runs mountEmbed(preset(config))
Usage
Build SsrClient once (DI / container). EmbedRequest is one placement.
use OpenMapsight\Embed\EmbedRequest; use OpenMapsight\Embed\Psr16SsrResultCache; use OpenMapsight\Embed\Renderer; use OpenMapsight\Embed\SsrClient; use OpenMapsight\Embed\SsrPublish; $ssr = new SsrClient( ssrUrl: getenv('MAPSIGHT_SSR_URL') ?: 'http://127.0.0.1:4123', resultCache: new Psr16SsrResultCache($psr16), // Redis / APCu / filesystem logger: $logger, // optional PSR-3 ); $renderer = new Renderer($ssr); // omit $ssr for client-only $result = $renderer->render(new EmbedRequest( preset: 'simpleMap', containerId: 'mapsight-embed-1', config: [ // opaque options for the preset factory your host build exports ], assetBase: '/mapsight/plan', requestUrl: $request->getRequestUri(), // host-owned, explicit pageOrigin: 'https://www.example.com', ogImage: 'https://www.example.com/plan/img/og-default.png', requestId: $request->headers->get('X-Request-Id'), locale: 'de', deviceClass: 'desktop', )); // Host <head> APIs, or: // echo $result->stylesheetHtml; // echo $result->preloadHtml; // echo \OpenMapsight\Embed\PageMetaTags::html($result->pageMeta); echo $result->html;
$result->html is a fragment, not a document. The same content is also
split into stylesheetHtml, preloadHtml, containerHtml, and
bootScriptHtml so a CMS head-injection API can take the links.
$result->pageMeta is set only when the normalised request URL has a share
parameter (?feature= / ?module= by default) and the sidecar returned
meta. $result->ssr is disabled / rendered / cached /
skipped_breaker / skipped_error.
preset becomes /assets/{preset}.js next to embed.js under assetBase
and is interpolated as a JS import binding, so it must be a JavaScript
identifier (not my-map, not a reserved word). containerId must match
^[A-Za-z][A-Za-z0-9_:.-]*$ and stay stable across renders or the cache
will miss. containerClassName is optional. locale and deviceClass are
forwarded in sidecar options when set. assetVersion cache-busts CSS and
both modules. scriptNonce is emitted on the inline module script.
Pass requestUrl (path + search) and, when that URL is path-only,
pageOrigin so the sidecar can make absolute canonical / og:url /
default og:image. The client keeps only configured share parameters on
that URL (default feature, module) before it reaches the cache key, the
sidecar, or the pageMeta gate.
Server-Timing
header(sprintf( 'Server-Timing: mapsight-ssr;dur=%.1f;desc=%s', $result->ssrDurationMs, $result->ssr->value, ));
Suggested series if you already scrape metrics:
mapsight_ssr_requests_total{outcome}, mapsight_ssr_duration_seconds,
mapsight_ssr_cache_hits_total, mapsight_ssr_breaker_open.
Trust boundaries
| Input | Trust | Notes |
|---|---|---|
Host fields on EmbedRequest / SsrClient |
Trusted | You constructed them. |
| Sidecar JSON | Private network, mostly trusted | Parsed; HTML must start with a real element whose id matches containerId. Size-capped. |
requestUrl |
Untrusted | Normalised to path + share-param whitelist, capped at 2048 bytes. Never read from $_SERVER. |
requestId and assetVersion must match [A-Za-z0-9._-]{1,200} because they
become HTTP headers. Validate or regenerate inbound X-Request-Id in the host
before passing it in.
Sidecar
This library POSTs to {ssrUrl}/v1/render and expects JSON
{ v: 1, html, state, pageMeta } with Content-Type: application/json.
state is HTML-escaped onto data-dehydrated-state. Optional requestId /
assetVersion become X-Request-Id / X-Mapsight-Asset-Version.
v1 request
| Field | Role |
|---|---|
v |
Contract version (1) |
preset |
JS identifier / /assets/{preset}.js |
requestId / assetVersion |
Optional, also sent as headers |
options.containerId |
Required by the sidecar |
options.containerClassName |
Optional |
options.requestUrl |
Normalised path + share params |
options.pageOrigin / options.ogImage |
Absolute / root-absolute |
options.locale / options.deviceClass |
Optional, forwarded when set |
options.* |
Host config keys, overwritten by the documented keys above |
The process is generic and stays off public ingress. Hosts pull
ghcr.io/open-mapsight/ssr-sidecar
and bind-mount their own render.js. The image does not contain a host bundle.
| Method | Path | Role |
|---|---|---|
GET |
/health |
Liveness (SsrClient::health()) |
POST |
/v1/render |
One placement → { html, state, pageMeta } |
POST |
/purge |
Drop sidecar caches |
There is no POST /render.
Timeouts are split: connectTimeoutSeconds (default 0.1) and
timeoutSeconds (default 2.0 total). Keep the total ≥ the sidecar’s
MAPSIGHT_SSR_AWAIT_TIMEOUT_MS when your module awaits GeoJSON. After 5
connect / timeout / 5xx failures a process-local breaker skips Node for 15s.
4xx, encode errors, size caps, and v1 parse errors are logged and fail open
without opening the breaker.
Pass an SsrResultCache (e.g. Psr16SsrResultCache, or ArraySsrResultCache
in tests — 256-entry LRU) to skip Node on a warm {html,state} hit. The
cache is consulted before the circuit breaker. Entries expire (ttl, default
3600 s). The key is SsrCacheKey: config + locale + deviceClass +
assetVersion + normalised requestUrl + contract v.
SsrClient::warm($request) forces a sidecar call and stores the result
(publish then warm).
Wire and hydration details live in the Mapsight monorepo — do not fork them here:
Publish / purge
When a feature-source or GeoJSON file changes, call SsrPublish before
the next page render. It POSTs sidecar /purge (prefer absolute list URLs;
omit or pass [] to clear all) and flush()es the PHP fragment cache only
after a successful sidecar purge. A list that filters down to no URLs
(e.g. ['']) throws instead of purging everything.
$result = (new SsrPublish($ssr))->purge([ 'https://www.example.com/geojson/places.geojson', ]); if (!$result->sidecarPurged) { // POST failed — PHP cache was not flushed }
Do not use a feature-source revision env var as the bust protocol.
Develop
composer install
composer test
composer phpstan
composer validate --strict