Maps
The ::map component (used by the job.offered example's driver email) needs a static map image URL. The mapbox package builds that URL and fetches the image; it never renders anything into the template itself — you fetch the bytes at send time, upload them to your own CDN, and put the CDN URL in ::map{src}.
Configure Static
type Point struct{ Lng, Lat float64 }
type Static struct {
Token string
Style string
Width int
Height int
Padding int
RouteColor string // route line stroke
PickupColor string // pickup pin
DropoffColor string // drop-off pin
}
Width, Height and Padding default to 600, 240 and 80 pixels at their zero value (1280px is the maximum for either dimension). Style is owner/id.
The three colours are required fields (D34): each must be six hex digits without #, or URL and Fetch fail with mapbox: {name} colour is required or must be 6 hex digits without '#'. The mapbox package carries no default brand colours — SPEC's literal #0b4740/#c84a0e pins are superseded by this rule, since the library itself must stay brand-neutral. examples/quiet.Mapbox supplies the Quiet brand's own colours:
// Mapbox returns the Static map builder for the T4 band in the Quiet
// colours: the pickup pin in the accent orange, the drop-off pin and the
// route in the action teal, both read from Theme.
func Mapbox(token, style string) mapbox.Static {
l := Theme().Light
hex := func(c string) string { return strings.TrimPrefix(c, "#") }
return mapbox.Static{
Token: token,
Style: style,
RouteColor: hex(string(l.Action)), // "0B4740"
PickupColor: hex(string(l.Accent)), // "C84A0E"
DropoffColor: hex(string(l.Action)), // "0B4740"
}
}
Write the equivalent for your own brand: read the two theme colours you want for the pins and route, strip their #, and pass them through.
Build the URL
func (s Static) URL(pickup, dropoff Point, route []Point) (string, error)
When route is empty, the line runs directly from pickup to dropoff; pass your own points (e.g. a decoded polyline) to draw the actual path instead. Every point is validated: finite, longitude in -180, 180, latitude in -90, 90.
package main
import (
"fmt"
"github.com/sulv-io/lttr/mapbox"
)
func main() {
m := mapbox.Static{
Token: "pk.example-token",
Style: "caribhubs/quiet-map",
RouteColor: "0B4740",
PickupColor: "C84A0E",
DropoffColor: "0B4740",
}
pickup := mapbox.Point{Lng: -62.72, Lat: 17.30}
dropoff := mapbox.Point{Lng: -62.70, Lat: 17.28}
u, err := m.URL(pickup, dropoff, nil)
if err != nil {
panic(err)
}
fmt.Println(u)
}
prints a URL of the shape:
https://api.mapbox.com/styles/v1/caribhubs/quiet-map/static/geojson(%7B%22type%22...),pin-s+C84A0E(-62.72000,17.30000),pin-s+0B4740(-62.70000,17.28000)/auto/600x240@2x?padding=80&access_token=pk.example-token
The overlay list is, in order: a path-escaped GeoJSON LineString feature ("stroke": "#{RouteColor}", "stroke-width": 4) for the route, then pin-s+{PickupColor}(lng,lat) for the pickup, then pin-s+{DropoffColor}(lng,lat) for the drop-off — coordinates to 5 decimal places. No error URL returns contains the token — errors name only what was wrong (a colour, a style, a point), never the request.
Fetch the image bytes
func (s Static) Fetch(ctx context.Context, c *http.Client, pickup, dropoff Point, route []Point) ([]byte, error)
Fetch builds the URL, issues the GET, and returns the body. Three things make it safe to call from a send path:
- The response body is capped at 8 MiB. A larger body is an error (
mapbox: response body exceeds 8 MiB) rather than an unbounded read. - A non-200 response names only the status code —
mapbox: unexpected status 404— never the URL, so a logged error can't leak the token. - Transport errors are sanitised too.
net/httpwraps client errors in*url.Error, whoseError()string embeds the request URL;Fetchunwraps to the inner error before returning, so a DNS failure or a timeout doesn't print the token either.
Upload to your own CDN
The Static Images API URL is short-lived working state, not something to persist or hand to a recipient. The intended flow:
- Call
Fetchat send time (or ahead of time, in a batch job) to get the PNG bytes. - Upload those bytes to your own object storage / CDN.
- Put only the CDN URL — never the Mapbox URL — in
::map{src="{{map_url}}"}(see thejob.offeredexample in Modules).
package main
import (
"context"
"fmt"
"net/http"
"github.com/sulv-io/lttr/mapbox"
)
func main() {
m := mapbox.Static{
Token: "pk.example-token", Style: "caribhubs/quiet-map",
RouteColor: "0B4740", PickupColor: "C84A0E", DropoffColor: "0B4740",
}
pickup := mapbox.Point{Lng: -62.72, Lat: 17.30}
dropoff := mapbox.Point{Lng: -62.70, Lat: 17.28}
img, err := m.Fetch(context.Background(), http.DefaultClient, pickup, dropoff, nil)
if err != nil {
fmt.Println("fetch failed:", err) // safe to log: never contains the token
return
}
mapURL := uploadToCDN(img) // your own storage; not part of lttr
fmt.Println("map_url =", mapURL)
}
func uploadToCDN(img []byte) string {
// upload img to S3 / R2 / GCS / wherever your assets already live
return "https://cdn.caribhubs.com/maps/ch-20417.png"
}