Happy Fairy Tale Day!
I thought about mapping where Grimm’s Fairy Tales took place, but then decided that, even though that would be cool, I wanted to see what kind of fairy tale an LLM would come up with.
So this fairy tale is 100% invented by an LLM (Perplexity Plus). It wrote the tale, came up with the fake island coordinates, and exported an entire schema for me to feed to Web Mapper GPT. Then, Web Mapper GPT exported this, by-and-large, after I copy-pasted what Perplexity said. I did come up with the name Mette and ask for a fairy tale about a cartographer. But that’s it… and as one might suspect, the resulting story — with no human guidance or guardrails — is kind of boring. But alas… this is a daly thing. Maybe on Folk Hero Day I will have more time. π
Complete Single Prompt to WebMapGPT
ROLE & CONTEXT
You are an expert web cartographer and front-end developer. Your task is to build a complete, self-contained interactive narrative web map called "The Inkward Isles: A Cartographer's Fairy Tale" using Leaflet.js 1.9.4 and vanilla JavaScript. No frameworks, no build tools, no proprietary APIs. The entire application must run from a single index.html file with embedded CSS and JS, plus two external data files: islands.geojson and chapters.json (provided below in full).
PROJECT CONCEPT
A young cartographer named Mette discovers that a map she drew by hand has come to life. The user navigates her fairy tale by clicking through six chapters, each corresponding to a fictional island in a remote archipelago. The map is the story. As the user advances chapters, the map flies to each island, the story panel updates with narrative prose, and a dashed route line draws itself progressively across the sea.
FILE STRUCTURE
text
/inkward-isles/
index.html β entire app (HTML + CSS + JS)
islands.geojson β island polygons + metadata
chapters.json β chapter narrative data
DEPENDENCIES (CDN β no installs)
xml
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Lora:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet"/>
MAP CONFIGURATION
javascript
const map = L.map('map', {
center: [-19.5, 159.5],
zoom: 4,
zoomControl: true,
scrollWheelZoom: true,
dragging: true,
tap: true
});
// Tile layer β CartoDB Positron No Labels (clean, minimal, no real geography labels)
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
attribution: 'Β© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Β© <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd',
maxZoom: 10
}).addTo(map);
// Override map background to pale sea blue
// Add to CSS: .leaflet-container { background: #c9dde8; }
LAYOUT (HTML STRUCTURE)
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>The Inkward Isles</title>
<!-- dependencies here -->
</head>
<body>
<div id="app">
<div id="map"></div>
<div id="story-panel">
<div id="panel-inner">
<div id="chapter-eyebrow"></div>
<div id="chapter-title"></div>
<div id="chapter-tagline"></div>
<div id="chapter-text"></div>
</div>
<div id="nav-buttons">
<button id="btn-prev">β Previous</button>
<div id="chapter-counter"></div>
<button id="btn-next">Next β</button>
</div>
</div>
</div>
</body>
</html>
CSS SPECIFICATION
Apply the following styles precisely:
css
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Lora', serif;
background: #1a1008;
height: 100vh;
overflow: hidden;
}
#app {
display: flex;
flex-direction: row;
height: 100vh;
width: 100vw;
}
#map {
flex: 1;
height: 100%;
background: #c9dde8;
}
.leaflet-container {
background: #c9dde8;
}
#story-panel {
width: 380px;
min-width: 320px;
background: #f5e9c8;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0;
border-left: 4px solid #3a1f0d;
/* Torn parchment left edge effect */
clip-path: polygon(
6px 0%, 0% 2%, 8px 5%, 2px 8%, 6px 12%,
0% 16%, 4px 20%, 0% 24%, 6px 28%, 2px 32%,
4px 36%, 0% 40%, 6px 44%, 0% 48%, 4px 52%,
0% 56%, 6px 60%, 2px 64%, 4px 68%, 0% 72%,
6px 76%, 0% 80%, 4px 84%, 2px 88%, 6px 92%,
0% 96%, 4px 100%,
100% 100%, 100% 0%
);
position: relative;
overflow: hidden;
}
#panel-inner {
padding: 2rem 1.8rem 1rem 2rem;
overflow-y: auto;
flex: 1;
}
#chapter-eyebrow {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #8a6a3a;
margin-bottom: 0.5rem;
}
#chapter-title {
font-family: 'Cinzel', serif;
font-size: 1.3rem;
font-weight: 700;
color: #3a1f0d;
line-height: 1.3;
margin-bottom: 0.4rem;
}
#chapter-tagline {
font-family: 'Lora', serif;
font-style: italic;
font-size: 0.85rem;
color: #8a6a3a;
margin-bottom: 1.2rem;
border-bottom: 1px solid #c9a84c;
padding-bottom: 0.8rem;
}
#chapter-text {
font-family: 'Lora', serif;
font-size: 0.92rem;
color: #2c1a0e;
line-height: 1.75;
}
#chapter-text p {
margin-bottom: 0.9rem;
}
#chapter-text em {
color: #6b4c2a;
}
#chapter-text strong em {
color: #3a1f0d;
font-size: 1rem;
}
#nav-buttons {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-top: 1px solid #c9a84c;
background: #ede0b0;
}
#nav-buttons button {
font-family: 'Cinzel', serif;
font-size: 0.78rem;
letter-spacing: 0.08em;
background: transparent;
border: 1.5px solid #3a1f0d;
color: #3a1f0d;
padding: 0.45rem 0.9rem;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
#nav-buttons button:hover:not(:disabled) {
background: #3a1f0d;
color: #f5e9c8;
}
#nav-buttons button:disabled {
opacity: 0.3;
cursor: default;
}
#chapter-counter {
font-family: 'Lora', serif;
font-style: italic;
font-size: 0.8rem;
color: #8a6a3a;
}
/* ββ MOBILE LAYOUT ββ */
@media (max-width: 768px) {
#app {
flex-direction: column;
}
#map {
height: 50vh;
width: 100%;
}
#story-panel {
width: 100%;
height: 50vh;
border-left: none;
border-top: 4px solid #3a1f0d;
clip-path: none;
}
}
JAVASCRIPT β CORE APPLICATION LOGIC
javascript
// ββ STATE ββ
let currentChapter = 0;
let chapters = [];
let routeLayer = null;
let islandLayers = [];
let markerLayers = [];
// ββ LOAD DATA ββ
async function init() {
const [chaptersRes, islandsRes] = await Promise.all([
fetch('chapters.json'),
fetch('islands.geojson')
]);
chapters = await chaptersRes.json();
const islandsGeoJSON = await islandsRes.json();
renderIslands(islandsGeoJSON);
renderMarkers();
goToChapter(0);
}
// ββ RENDER ISLAND POLYGONS ββ
function renderIslands(geojson) {
L.geoJSON(geojson, {
filter: f => f.geometry.type === 'Polygon',
style: {
color: '#3a1f0d',
weight: 1.5,
fillColor: '#c9a84c',
fillOpacity: 0.35,
dashArray: '4 3'
}
}).addTo(map);
}
// ββ RENDER CLICKABLE MARKERS ββ
function renderMarkers() {
chapters.forEach((ch, i) => {
const icon = L.divIcon({
className: '',
html: `<div style="
width:12px; height:12px;
background:#c9a84c;
border:2px solid #3a1f0d;
border-radius:50%;
cursor:pointer;
"></div>`,
iconSize: [12, 12],
iconAnchor: [6, 6]
});
const marker = L.marker(ch.marker_latlng, { icon })
.addTo(map)
.on('click', () => goToChapter(i));
markerLayers.push(marker);
});
}
// ββ CORE NAVIGATION ββ
function goToChapter(index) {
const ch = chapters[index];
currentChapter = index;
// Fly the map
map.flyTo(ch.marker_latlng, ch.flyto_zoom, {
duration: 1.8,
easeLinearity: 0.4
});
// Update story panel
updateStoryPanel(ch, index);
// Draw route up to this chapter
drawRouteTo(index);
// Update button states
document.getElementById('btn-prev').disabled = index === 0;
document.getElementById('btn-next').disabled = index === chapters.length - 1;
// Update counter
document.getElementById('chapter-counter').textContent =
`${index + 1} of ${chapters.length}`;
}
// ββ UPDATE STORY PANEL ββ
function updateStoryPanel(ch, index) {
document.getElementById('chapter-eyebrow').textContent =
index === 0 ? 'The Inkward Isles' : `The Inkward Isles Β· Chapter ${index}`;
document.getElementById('chapter-title').textContent = ch.title;
document.getElementById('chapter-tagline').textContent = ch.tagline;
const textEl = document.getElementById('chapter-text');
textEl.style.opacity = 0;
setTimeout(() => {
textEl.innerHTML = ch.story_text;
textEl.style.transition = 'opacity 0.6s ease';
textEl.style.opacity = 1;
}, 300);
// Scroll panel back to top on chapter change
document.getElementById('panel-inner').scrollTop = 0;
}
// ββ ANIMATED ROUTE POLYLINE ββ
function drawRouteTo(chapterIndex) {
const coords = chapters
.slice(0, chapterIndex + 1)
.map(ch => ch.marker_latlng);
if (routeLayer) map.removeLayer(routeLayer);
if (coords.length < 2) return;
routeLayer = L.polyline(coords, {
color: '#3a1f0d',
weight: 2,
dashArray: '6 8',
opacity: 0.7,
lineJoin: 'round'
}).addTo(map);
}
// ββ BUTTON LISTENERS ββ
document.getElementById('btn-next').addEventListener('click', () => {
if (currentChapter < chapters.length - 1) goToChapter(currentChapter + 1);
});
document.getElementById('btn-prev').addEventListener('click', () => {
if (currentChapter > 0) goToChapter(currentChapter - 1);
});
// ββ KEYBOARD NAVIGATION (bonus UX) ββ
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight') document.getElementById('btn-next').click();
if (e.key === 'ArrowLeft') document.getElementById('btn-prev').click();
});
// ββ START ββ
init();
islands.geojson β PASTE THIS FILE VERBATIM
json
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": "isle_01",
"name": "The Isle of Unfinished Roads",
"chapter": 1
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[157.65, -17.05], [157.85, -17.00], [158.00, -17.10],
[158.05, -17.25], [157.95, -17.40], [157.75, -17.42],
[157.60, -17.30], [157.58, -17.15], [157.65, -17.05]
]]
}
},
{
"type": "Feature",
"properties": {
"id": "isle_02",
"name": "The Fog Shore",
"chapter": 2
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[160.20, -15.60], [160.45, -15.55], [160.62, -15.68],
[160.60, -15.88], [160.42, -15.98], [160.22, -15.90],
[160.15, -15.75], [160.20, -15.60]
]]
}
},
{
"type": "Feature",
"properties": {
"id": "isle_03",
"name": "The Mountain That Forgot Its Name",
"chapter": 3
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[162.72, -17.88], [162.95, -17.82], [163.12, -17.98],
[163.10, -18.22], [162.90, -18.35], [162.70, -18.28],
[162.62, -18.10], [162.68, -17.95], [162.72, -17.88]
]]
}
},
{
"type": "Feature",
"properties": {
"id": "isle_04",
"name": "The Library of Drowned Maps",
"chapter": 4
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[163.60, -21.10], [163.85, -21.05], [164.00, -21.18],
[164.02, -21.38], [163.88, -21.52], [163.65, -21.50],
[163.52, -21.35], [163.55, -21.18], [163.60, -21.10]
]]
}
},
{
"type": "Feature",
"properties": {
"id": "isle_06",
"name": "The Center",
"chapter": 6
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[158.38, -20.88], [158.55, -20.85], [158.65, -20.95],
[158.62, -21.10], [158.48, -21.18], [158.35, -21.10],
[158.32, -20.97], [158.38, -20.88]
]]
}
}
]
}
chapters.json β PASTE THIS FILE VERBATIM
(Use the complete chapters.json assembled in the previous session β all seven entries, Prologue through Chapter VI, with full story_text HTML, marker_latlng, flyto_zoom, tagline, title, island_id, chapter, route_segment_index, and has_polygon fields.)
BEHAVIOR SPECIFICATION β PLEASE IMPLEMENT EXACTLY
On load: Map initializes at center: [-19.5, 159.5], zoom: 4. All island polygons render immediately. All chapter markers render as small gold dots. Prologue story panel displays. Previous button is disabled.
On Next/Previous click: map.flyTo() animates to the chapter's marker_latlng at flyto_zoom with duration: 1.8. Story panel text fades out then fades in (0.3s delay, 0.6s fade). Route polyline redraws to include all chapters up to and including current. Story panel scrolls to top.
On marker click: Same behavior as Next/Previous β jumps directly to that chapter.
Keyboard: Left/Right arrow keys trigger Previous/Next.
Chapter V (Ink Sea): Has no polygon β marker only. flyTo() zooms to level 5, showing open water. This is intentional and reinforces the "crossing" narrative.
Mobile: Below 768px viewport width, layout stacks vertically β map 50vh on top, story panel 50vh below. clip-path on story panel is removed on mobile. Navigation buttons remain fully accessible.
No external plugins beyond Leaflet 1.9.4 and Google Fonts.
All files are local β fetch calls use relative paths 'chapters.json' and 'islands.geojson'. Must be served from a local server (e.g., python -m http.server) or deployed β not opened as file:// directly.
DELIVERABLE
Produce three complete, copy-paste-ready files:
index.html β full application with all HTML, CSS, and JS inline
islands.geojson β verbatim from above
chapters.json β verbatim from the full assembled data
The application should require zero modification to run. Include a brief README.md noting the python -m http.server requirement.
View Map Here
Original Prompt


View the map (or click the image above)
View the Map Here.
View Map
View Map