getreadystack's picture
Upload index.html with huggingface_hub
5d2c90c verified
Raw History Blame Contribute Delete
44.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mapbox Migration 2026 — check your code for free</title>
<meta name="description" content="This free browser tool checks Mapbox GL JS v2+ code for migration issues and shows a MapLibre fix for each flagged line.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/maplibre-migration-lint"><style data-yj-mfit="1">#out{max-width:100%}#out td,#out th{overflow-wrap:anywhere}#out pre,#out code{white-space:pre-wrap;overflow-wrap:anywhere}@media (max-width:640px){table{display:block;max-width:100%;overflow-x:auto}pre{max-width:100%;overflow-x:auto}code,a{overflow-wrap:anywhere}img,svg,video,canvas,iframe{max-width:100%}textarea,input,select{max-width:100%;box-sizing:border-box}.yj-lgbar{position:static!important;justify-content:flex-end;margin:0 0 10px}#out table:not(:has(tr>:nth-child(4))){display:table;table-layout:fixed;width:100%}#out table:has(tr>:nth-child(4)),#out table:has(tr>:nth-child(4)) tbody,#out table:has(tr>:nth-child(4)) tr,#out table:has(tr>:nth-child(4)) td{display:block;width:auto;overflow:visible}#out table:has(tr>:nth-child(4)) thead{display:none}#out table:has(tr>:nth-child(4)) tr{padding:10px 0;border-bottom:1px solid rgba(128,128,128,.28)}#out table:has(tr>:nth-child(4)) td{border:0;padding:1px 0}#out table:has(tr>:nth-child(4)) td:nth-child(-n+3){display:inline-block;margin-right:10px;font-size:12px;opacity:.78}}</style>
</head>
<body>
<div class="wrap">
<h1>Mapbox Migration 2026 — check your code for free</h1>
<p class="lede">MapLibre migration check for Mapbox GL JS v2+ code: flags the proprietary mapbox-gl dependency, tokens, mapbox:// styles, CDN tags and plugins, with the maplibre-gl fix for each line. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">// store-locator.js: the map on /stores, still on Mapbox GL JS v3
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
mapboxgl.accessToken = 'pk.eyJ1Ijoic3RvcmVzIiwiYSI6ImNsZGVtbzAwMSJ9.sample';
export function initStoreMap(container, stores) {
const map = new mapboxgl.Map({
container,
style: 'mapbox://styles/mapbox/streets-v12',
center: [-73.99, 40.73],
zoom: 11
});
map.addControl(new mapboxgl.NavigationControl(), 'top-right');
map.addControl(new MapboxGeocoder({ accessToken: mapboxgl.accessToken, mapboxgl }));
map.addControl(new MapboxDraw({ displayControlsDefault: false }));
map.on('load', () =&gt; {
map.setFog({ color: 'rgb(220, 230, 240)', 'horizon-blend': 0.1 });
map.addSource('buildings', { type: 'vector', url: 'mapbox://mapbox.mapbox-streets-v8' });
map.addSource('stores', { type: 'geojson', data: stores });
map.addLayer({ id: 'stores', type: 'circle', source: 'stores', paint: { 'circle-radius': 6 } });
});
for (const s of stores.features) {
new mapboxgl.Marker().setLngLat(s.geometry.coordinates).addTo(map);
}
document.querySelector('.mapboxgl-ctrl-top-right').classList.add('store-controls');
return map;
}
export async function staticThumb(lng, lat) {
const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${lng},${lat},14/300x200?access_token=${mapboxgl.accessToken}`;
return fetch(url);
}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> every Mapbox GL JS v2+ line in the pasted file, with why it bills or breaks and the MapLibre fix, free and offline. <strong>Full version (VS Code):</strong> scan the whole workspace in one pass and export one migration report for the pull request. <a href="https://getreadystack.com/api/buy/cl/polar_cl_jYm4blWtSExV7LvGc7v8hHfID9il3bpCX6vw444Vmis" data-polar-checkout data-polar-checkout-theme="light">Get the full version — $29</a> · $29 once · one licence key per person or team seat. Mapbox bills $950 a month at 300,000 map loads.</div>
<script defer data-auto-init src="https://cdn.jsdelivr.net/npm/@polar-sh/checkout@latest/dist/embed.global.js"></script>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var ML_RULES = [
{"id": "mapbox-gl-v2-dependency", "sev": "error",
"pats": [{"re": "\"mapbox-gl\"\\s*:\\s*\"[\\^~\u003e=\u003cv\\s]*[23]\\."}, {"re": "\"mapbox-gl\"\\s*:\\s*\"(?:latest|next|\\*|x)\""}, {"re": "\"mapbox-gl\"\\s*:\\s*\"\u003e=?\\s*1"}],
"msg": "mapbox-gl v2+ dependency: since v2.0.0 (8 December 2020) mapbox-gl is licensed under the Mapbox TOS, not BSD-3-Clause. Use needs an active Mapbox account, and every Map object initialised is a billable map load. \"latest\", \"*\" and \"\u003e=1\" all resolve to a 3.x release.",
"fix": "npm uninstall mapbox-gl \u0026\u0026 npm install maplibre-gl (BSD-3-Clause), then fix the import lines this lint flags."},
{"id": "mapbox-gl-v1-frozen", "sev": "info",
"pats": [{"re": "\"mapbox-gl\"\\s*:\\s*\"[\\^~=v\\s]*1\\."}],
"msg": "mapbox-gl 1.x is still BSD-3-Clause, but 1.13.3 is the last BSD release: any upgrade past 1.x accepts the Mapbox TOS and map-load billing.",
"fix": "Pin it exactly, or move to maplibre-gl: MapLibre GL JS was forked from mapbox-gl 1.13 and is maintained under BSD-3-Clause."},
{"id": "mapbox-gl-v2-lockfile", "sev": "error",
"pats": [{"re": "\"version\"\\s*:\\s*\"[23]\\.", "near": "\"(?:node_modules/)?mapbox-gl\"\\s*:\\s*\\{", "within": 2}, {"re": "^\\s+version:?\\s+\"?[23]\\.", "near": "^\"?mapbox-gl@", "within": 2}, {"re": "^\\s*['\"]?/?mapbox-gl@[23]\\."}],
"msg": "Lockfile resolves mapbox-gl 2.x or 3.x: the Mapbox TOS licence ships in your bundle even if package.json looks harmless (a transitive or loose range).",
"fix": "Find the parent with npm ls mapbox-gl, replace it with its MapLibre build, and reinstall so the lockfile no longer lists mapbox-gl."},
{"id": "mapbox-gl-cdn", "sev": "error",
"pats": [{"re": "api\\.mapbox\\.com/mapbox-gl-js/v[23]\\."}, {"re": "(?:unpkg\\.com|cdn\\.jsdelivr\\.net/npm)/mapbox-gl@[23]\\."}],
"msg": "Mapbox GL JS v2/v3 loaded from a CDN: this is the Mapbox TOS build, and every page view that creates a map is a billable map load.",
"fix": "Load https://unpkg.com/maplibre-gl/dist/maplibre-gl.js and maplibre-gl.css instead (pin a version in production)."},
{"id": "mapbox-gl-import", "sev": "warn",
"pats": [{"re": "(?:\\bfrom\\s+|\\brequire\\(\\s*|\\bimport\\(\\s*)['\"]mapbox-gl['\"]"}],
"msg": "Import of the mapbox-gl package: this is the line that pulls the Mapbox TOS code into your bundle.",
"fix": "import maplibregl from 'maplibre-gl'; the Map, Marker, Popup and control classes keep the same names."},
{"id": "mapbox-gl-css", "sev": "warn",
"pats": [{"re": "mapbox-gl(?:\\.min)?\\.css"}],
"msg": "Mapbox GL stylesheet: MapLibre ships its own stylesheet with maplibregl- class names, so the Mapbox CSS styles nothing after the switch.",
"fix": "import 'maplibre-gl/dist/maplibre-gl.css' (or link maplibre-gl.css from the CDN)."},
{"id": "mapbox-access-token", "sev": "error",
"pats": [{"re": "\\bmapboxgl\\.accessToken\\s*="}, {"re": "\\bmapboxAccessToken\\b"}, {"re": "\\baccessToken\\s*:\\s*['\"`]pk\\."}, {"re": "['\"`]pk\\.eyJ[\\w.-]*"}],
"msg": "Mapbox access token: every Map initialised with it is a billable map load on your Mapbox account (50,000 a month free, then $5.00 per 1,000). MapLibre GL JS removed accessToken in its 2.0.0 release.",
"fix": "Delete the token line. If your tiles still come from Mapbox, the Mapbox TOS and billing still apply: point the style at another tile host."},
{"id": "mapbox-api-url", "sev": "warn",
"pats": [{"re": "api\\.mapbox\\.com/(?:styles|v4|fonts|geocoding|search|directions|tokens)/"}, {"re": "[?\u0026]access_token=pk\\."}],
"msg": "Direct Mapbox API request: MapLibre will fetch it, but the request is billed and governed by the Mapbox TOS, so the migration is not finished.",
"fix": "Replace it with your own tile/style host or an open service, and remove access_token from the URL."},
{"id": "mapbox-style-url", "sev": "error",
"pats": [{"re": "['\"`]mapbox://styles/"}],
"msg": "mapbox:// style URL: MapLibre GL JS 2.0.0 removed all mapbox:// URL handling, so this map loads blank after the switch.",
"fix": "Use an https style.json URL, e.g. https://demotiles.maplibre.org/style.json for a test, or the style.json of your tile host."},
{"id": "mapbox-source-url", "sev": "error",
"pats": [{"re": "['\"`]mapbox://(?!styles/)[a-z]"}],
"msg": "mapbox:// tileset, sprite or glyphs URL: MapLibre does not resolve mapbox:// URLs, so this layer, icon set or font stays empty.",
"fix": "Use a TileJSON or {z}/{x}/{y} URL for sources and https URLs for sprite and glyphs."},
{"id": "mapboxgl-namespace", "sev": "warn",
"pats": [{"re": "\\bnew\\s+mapboxgl\\."}, {"re": "\\bmapboxgl\\.(?!accessToken\\b)[A-Za-z]\\w*"}],
"msg": "mapboxgl global: after the switch the global is maplibregl, so this line throws \"mapboxgl is not defined\".",
"fix": "Rename mapboxgl. to maplibregl. (or import maplibregl and use it)."},
{"id": "mapboxgl-css-class", "sev": "warn",
"pats": [{"re": "\\bmapboxgl-[a-z][\\w-]*"}],
"msg": "mapboxgl- CSS class: MapLibre GL JS 3.0.0 removed the deprecated mapboxgl- class names, so this selector silently matches nothing.",
"fix": "Rename the prefix to maplibregl- (mapboxgl-ctrl becomes maplibregl-ctrl, mapboxgl-popup becomes maplibregl-popup)."},
{"id": "react-map-gl-mapbox-entry", "sev": "warn",
"pats": [{"re": "\\bfrom\\s+['\"]react-map-gl(?:/mapbox(?:-legacy)?)?['\"]"}],
"msg": "react-map-gl Mapbox entry point: the bare 'react-map-gl' import (v7) and 'react-map-gl/mapbox' (v8) wrap mapbox-gl.",
"fix": "import Map from 'react-map-gl/maplibre' (react-map-gl 8) or from '@vis.gl/react-maplibre', and drop mapboxAccessToken."},
{"id": "mapbox-geocoder", "sev": "warn",
"pats": [{"re": "['\"]@mapbox/mapbox-gl-geocoder['\"]"}, {"re": "mapbox-gl-geocoder(?:\\.min)?\\.(?:js|css)"}],
"msg": "Mapbox geocoder plugin: it sends every search to the Mapbox Geocoding API with your token, so geocoding stays on Mapbox billing after the map moves.",
"fix": "Use @maplibre/maplibre-gl-geocoder (ISC) and pass it a forwardGeocode function for the geocoding service you choose."},
{"id": "mapbox-gl-draw-classes", "sev": "info",
"pats": [{"re": "['\"]@mapbox/mapbox-gl-draw['\"]"}],
"msg": "@mapbox/mapbox-gl-draw is ISC-licensed and runs on MapLibre, but its toolbar uses mapboxgl-ctrl class names, so the buttons render unstyled.",
"fix": "Before new MapboxDraw(), set MapboxDraw.constants.classes.CONTROL_BASE = 'maplibregl-ctrl', CONTROL_PREFIX = 'maplibregl-ctrl-' and CONTROL_GROUP = 'maplibregl-ctrl-group'."},
{"id": "mapbox-setfog", "sev": "warn",
"pats": [{"re": "\\.setFog\\s*\\("}],
"msg": "map.setFog() is Mapbox-only: MapLibre has no setFog, so this call throws.",
"fix": "Use map.setSky({...}): MapLibre puts sky, horizon and fog colours in one sky specification (fog-color, horizon-fog-blend)."},
{"id": "mapbox-standard-config", "sev": "warn",
"pats": [{"re": "\\.setConfigProperty\\s*\\("}, {"re": "mapbox://styles/mapbox/standard"}],
"msg": "Mapbox Standard style configuration (setConfigProperty, v3): there is no MapLibre equivalent, the call throws and the Standard style cannot be loaded.",
"fix": "Pick a style.json you host, then change its look with setPaintProperty / setLayoutProperty on named layers."}
]
;</script>
<script>/* MapLibre migration lint: one engine for VS Code and the free web page. */
(function () {
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.ML_RULES;
var COMMENT = /^\s*(\/\/|\/\*|\*|<!--|#)/;
var COMPILED = RULES.map(function (r) {
return {
rule: r,
pats: (r.pats || []).map(function (p) {
return { re: new RegExp(p.re), near: p.near ? new RegExp(p.near) : null, within: p.within || 0 };
})
};
});
function check(text, opts) {
var lines = String(text || '').split(/\r?\n/);
var findings = [];
for (var i = 0; i < lines.length; i++) {
var l = lines[i];
if (COMMENT.test(l)) continue;
for (var k = 0; k < COMPILED.length; k++) {
var r = COMPILED[k].rule;
for (var p = 0; p < COMPILED[k].pats.length; p++) {
var pat = COMPILED[k].pats[p];
if (!pat.re.test(l)) continue;
// Lockfile rules: the version line only counts under a mapbox-gl entry a few lines up.
if (pat.near) {
var ok = false;
for (var b = i - 1; b >= 0 && b >= i - pat.within; b--) if (pat.near.test(lines[b])) { ok = true; break; }
if (!ok) continue;
}
findings.push({ check: r.id, sev: r.sev, msg: r.msg + ' Fix: ' + r.fix, line: i + 1 });
break;
}
}
}
return { findings: findings };
}
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined') module.exports = api;
if (typeof window !== 'undefined') window.MLENGINE = api;
})();
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var MLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = MLREPORT;
else window.MLREPORT = MLREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.MLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.MLREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.maplibre-migration-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'maplibre-migration-lint',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/maplibre-migration-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'maplibre-migration-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.maplibre-migration-lint</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind MapLibre Migration Lint — Mapbox GL JS v2+ every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"maplibre-migration-lint",lang:"en",source:"rule_alert"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);document.addEventListener("click",function(ev){var b=ev.target&&ev.target.closest?ev.target.closest("button,input[type=submit]"):null;if(b&&!b.closest("[data-yj],[data-yj-cta],[data-yj-install],[data-yj-alert],.yj-find,.yj-related,#yj-ask,.yjp"))mv(ev);},true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the full version <span class="yj-price">$29</span></h2><p class="yj-cta-paid" style="margin:.25rem 0 .6rem;color:#1f2a37">Scan the whole workspace in one pass and export one migration report for the pull request (every file, line, rule and fix in one Markdown report), $29 once, one licence key per person or team seat.</p><p class="yj-cta-pay" style="margin:.25rem 0 1rem;color:#5b6b7c;font-size:.94rem">One payment, one licence key for this tool. The key is shown right after payment.</p><p class="yj-cta-anchor" style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Mapbox price list for Mapbox GL JS: 50,000 map loads a month free, then $5.00, $4.00, $3.00 and $2.50 per 1,000 by tier; 300,000 map loads a month comes to $950 a month.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_jYm4blWtSExV7LvGc7v8hHfID9il3bpCX6vw444Vmis" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-29">Updated 2026-09-29</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-29","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku390572_result_card.jpg" alt="MapLibre Migration Lint — Mapbox GL JS v2+" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>$950 a month: that is what the Mapbox price list charges a web app for 300,000 Mapbox GL JS map loads, and every front-end developer who upgraded mapbox-gl past 1.x is on that meter. This guide is for teams in the US, the EU and anywhere else who ship store locators, delivery maps or dashboards on Mapbox GL JS v2 or v3 and want to move to MapLibre GL JS.</p>
<h3>What Mapbox changed, and when</h3>
<p>mapbox-gl v2.0.0 was published on 8 December 2020. Its changelog opens with the breaking change: mapbox-gl-js is no longer under the 3-Clause BSD license, and by upgrading you agree to the Mapbox terms of service. The LICENSE.txt in the repository now says the SDK may be used only by developers with a current active Mapbox account. The same release made a Map object initialisation a billable map load.</p>
<p>The price list for Mapbox GL JS map loads is tiered: up to 50,000 a month free, then $5.00 per 1,000 up to 100,000, $4.00 up to 200,000, $3.00 up to 1,000,000 and $2.50 up to 5,000,000. For 300,000 loads that is 50,000 free, $250, $400 and $300: $950 a month.</p>
<p>Version 1.13.3 is the last BSD release of mapbox-gl. MapLibre GL JS was forked from 1.13 and is still BSD-3-Clause.</p>
<h3>Why a rename is not a migration</h3>
<p>The obvious move is to replace mapbox-gl with maplibre-gl in package.json and in the import. The map then fails in quieter ways:</p>
<ul><li>MapLibre GL JS 2.0.0 removed accessToken and every mapbox:// URL. A style of mapbox://styles/mapbox/streets-v12 loads a blank canvas.</li><li>MapLibre 3.0.0 removed the old mapboxgl- CSS classes. A selector like .mapboxgl-ctrl-top-right now matches nothing.</li><li>map.setFog() does not exist in MapLibre; sky and fog live in map.setSky().</li><li>The Mapbox geocoder plugin still sends every search to the Mapbox Geocoding API with your token.</li><li>A static image URL with access_token=pk. still bills Mapbox.</li></ul>
<h3>What the lint found in a real-shaped file</h3>
<p>We ran MapLibre Migration Lint on a sample store-locator.js: a Mapbox GL JS v3 page with a geocoder, a draw control, fog, a vector source and a static thumbnail. It gave 13 findings from 11 of the 17 rules: 3 errors, 9 warnings and 1 info. The errors were the access token, the mapbox:// style and the mapbox:// tileset. The warnings covered the import, the CSS, the geocoder, three uses of the mapboxgl global, setFog, the mapboxgl- selector and the api.mapbox.com URL. The info note was mapbox-gl-draw, which runs on MapLibre once its control classes point at maplibregl-ctrl.</p>
<p>The MapLibre version of the same file gave 0 findings.</p>
<h3>The fix, line by line</h3>
<pre><code>import maplibregl from &#x27;maplibre-gl&#x27;;
import &#x27;maplibre-gl/dist/maplibre-gl.css&#x27;;
const map = new maplibregl.Map({ container, style: &#x27;https://demotiles.maplibre.org/style.json&#x27; });
map.setSky({ &#x27;fog-color&#x27;: &#x27;rgb(220, 230, 240)&#x27; });</code></pre>
<p>React apps on react-map-gl 8 import Map from &#x27;react-map-gl/maplibre&#x27; and drop mapboxAccessToken. Lockfiles matter too: a loose range or a transitive dependency can still resolve mapbox-gl 3.x, so the lint reads package-lock.json and yarn.lock.</p>
<h3>One caveat</h3>
<p>Changing the library ends the Mapbox licence on your bundle. It does not end Mapbox billing if the style or tiles still come from api.mapbox.com. The token and API URL rules point at exactly those lines, so you know what is left to move.</p>
<h3>Try it</h3>
<p>The web version runs the same engine in the browser: paste a file and read the findings. The VS Code extension checks the open file for free and offline.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku390572_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku390572_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does MapLibre Migration Lint check?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads package.json, lockfiles, JavaScript, TypeScript, HTML, Vue, Svelte and CSS files and flags each line that keeps a map on Mapbox GL JS v2+: the proprietary mapbox-gl dependency, CDN tags, access tokens, mapbox:// styles and sources, mapboxgl- CSS classes, setFog and the Mapbox geocoder. It has 17 rules and prints the MapLibre GL JS fix next to every finding.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is MapLibre Migration Lint for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Front-end and GIS developers who ship web maps on Mapbox GL JS v2 or v3 and want to move to MapLibre GL JS: store locators, delivery and real-estate maps, dashboards and React apps on react-map-gl. It also suits tech leads checking whether a project still pulls the Mapbox TOS licence through package.json or a lockfile.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not ask a chatbot or do a find-and-replace?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A find-and-replace of mapbox-gl with maplibre-gl leaves the parts that break or keep billing: mapbox:// style URLs load a blank map, because MapLibre 2.0.0 removed them; mapboxgl- CSS classes match nothing since MapLibre 3.0.0; setFog throws; and the access token and Mapbox geocoder still send billable requests to Mapbox.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what does the full version add?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: every finding in the open file, with the MapLibre fix, in VS Code or on the web page, offline. The sample store-locator.js gives 13 findings. The full version scans the whole workspace in one pass and exports one Markdown migration report for the pull request, $29 once, with one licence key per person or team seat.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does staying on Mapbox GL JS cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Since mapbox-gl v2.0.0 every Map object initialised is a billable map load. Mapbox lists 50,000 map loads a month free, then $5.00 per 1,000 up to 100,000, $4.00 up to 200,000 and $3.00 up to 1,000,000. At 300,000 map loads a month that is $950 a month; MapLibre GL JS itself is BSD-3-Clause and free.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does MapLibre Migration Lint check?", "acceptedAnswer": {"@type": "Answer", "text": "It reads package.json, lockfiles, JavaScript, TypeScript, HTML, Vue, Svelte and CSS files and flags each line that keeps a map on Mapbox GL JS v2+: the proprietary mapbox-gl dependency, CDN tags, access tokens, mapbox:// styles and sources, mapboxgl- CSS classes, setFog and the Mapbox geocoder. It has 17 rules and prints the MapLibre GL JS fix next to every finding."}}, {"@type": "Question", "name": "Who is MapLibre Migration Lint for?", "acceptedAnswer": {"@type": "Answer", "text": "Front-end and GIS developers who ship web maps on Mapbox GL JS v2 or v3 and want to move to MapLibre GL JS: store locators, delivery and real-estate maps, dashboards and React apps on react-map-gl. It also suits tech leads checking whether a project still pulls the Mapbox TOS licence through package.json or a lockfile."}}, {"@type": "Question", "name": "Why not ask a chatbot or do a find-and-replace?", "acceptedAnswer": {"@type": "Answer", "text": "A find-and-replace of mapbox-gl with maplibre-gl leaves the parts that break or keep billing: mapbox:// style URLs load a blank map, because MapLibre 2.0.0 removed them; mapboxgl- CSS classes match nothing since MapLibre 3.0.0; setFog throws; and the access token and Mapbox geocoder still send billable requests to Mapbox."}}, {"@type": "Question", "name": "What is free and what does the full version add?", "acceptedAnswer": {"@type": "Answer", "text": "Free: every finding in the open file, with the MapLibre fix, in VS Code or on the web page, offline. The sample store-locator.js gives 13 findings. The full version scans the whole workspace in one pass and exports one Markdown migration report for the pull request, $29 once, with one licence key per person or team seat."}}, {"@type": "Question", "name": "What does staying on Mapbox GL JS cost?", "acceptedAnswer": {"@type": "Answer", "text": "Since mapbox-gl v2.0.0 every Map object initialised is a billable map load. Mapbox lists 50,000 map loads a month free, then $5.00 per 1,000 up to 100,000, $4.00 up to 200,000 and $3.00 up to 1,000,000. At 300,000 map loads a month that is $950 a month; MapLibre GL JS itself is BSD-3-Clause and free."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:390572,slug:"maplibre-migration-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/mv3-manifest-preflight"><span class="yj-rel-t">MV3 Manifest Preflight - Chrome Web Store</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/resend-migration-check"><span class="yj-rel-t">Resend Migration Check: SendGrid code lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/web-ui-kit-figma-sketch"><span class="yj-rel-t">Web UI Kit for SaaS Dashboards — 8 Components, 32 Variant States (Figma plugin</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/almalinux-migration-lint"><span class="yj-rel-t">AlmaLinux Migration Lint — CentOS 7/8</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>