Skip to content

Instantly share code, notes, and snippets.

@scottfwalter
Created October 10, 2025 00:53
Show Gist options
  • Select an option

  • Save scottfwalter/07d4d7d7245182512c7f684fa2260b0a to your computer and use it in GitHub Desktop.

Select an option

Save scottfwalter/07d4d7d7245182512c7f684fa2260b0a to your computer and use it in GitHub Desktop.
// create-national-park-md.js
// Usage: node create-national-park-md.js [outputDir]
// Example: node create-national-park-md.js ./parks
// If no outputDir is provided, it writes to ./national-parks
import fs from 'node:fs/promises'
import path from 'node:path'
// --- Configuration ---
const OUTPUT_DIR = process.argv[2] || path.resolve('./national-parks')
const RATE_LIMIT_MS = 1100 // be polite to Nominatim
const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org/search'
// Prefer US + territories where NPS parks live
const COUNTRY_CODES = 'us,vi,as,gu,mp,pr'
// Parks list (63). Keep the exact display names for filenames.
const PARKS = [
'Acadia National Park',
'American Samoa National Park',
'Arches National Park',
'Badlands National Park',
'Big Bend National Park',
'Biscayne National Park',
'Black Canyon of the Gunnison National Park',
'Bryce Canyon National Park',
'Canyonlands National Park',
'Capitol Reef National Park',
'Carlsbad Caverns National Park',
'Channel Islands National Park',
'Congaree National Park',
'Crater Lake National Park',
'Cuyahoga Valley National Park',
'Death Valley National Park',
'Denali National Park',
'Dry Tortugas National Park',
'Everglades National Park',
'Gates of the Arctic National Park',
'Gateway Arch National Park',
'Glacier National Park',
'Glacier Bay National Park',
'Grand Canyon National Park',
'Grand Teton National Park',
'Great Basin National Park',
'Great Sand Dunes National Park',
'Great Smoky Mountains National Park',
'Guadalupe Mountains National Park',
'Haleakalā National Park',
'Hawaiʻi Volcanoes National Park',
'Hot Springs National Park',
'Indiana Dunes National Park',
'Isle Royale National Park',
'Joshua Tree National Park',
'Katmai National Park',
'Kenai Fjords National Park',
'Kings Canyon National Park',
'Kobuk Valley National Park',
'Lake Clark National Park',
'Lassen Volcanic National Park',
'Mammoth Cave National Park',
'Mesa Verde National Park',
'Mount Rainier National Park',
'New River Gorge National Park',
'North Cascades National Park',
'Olympic National Park',
'Petrified Forest National Park',
'Pinnacles National Park',
'Redwood National Park',
'Rocky Mountain National Park',
'Saguaro National Park',
'Sequoia National Park',
'Shenandoah National Park',
'Theodore Roosevelt National Park',
'Virgin Islands National Park',
'Voyageurs National Park',
'White Sands National Park',
'Wind Cave National Park',
'Wrangell–St. Elias National Park',
'Yellowstone National Park',
'Yosemite National Park',
'Zion National Park',
]
// Optional manual coordinate overrides for tricky names (lon, lat as strings).
// If you want to pin a specific location (e.g., park HQ or a central point), add it here.
// By default the script will geocode; these entries will skip the API for that park.
const MANUAL_OVERRIDES = {
// Example:
// "Grand Teton National Park": ["-110.6818", "43.7904"],
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
function toYAMLFrontmatter({ lon, lat }) {
// Ensure longitude first, then latitude, both as quoted strings.
return `---
tags:
- nationalpark
coordinates:
- "${lat}"
- "${lon}"
color: green
icon: tree-pine
---`
}
function sanitizeFilename(name) {
// The user wants the filename to be the name of the park.
// Most filesystems support these characters; keep as-is except forbid slashes and colons.
const cleaned = name.replaceAll('/', '⁄').replaceAll(':', 'ː')
return `${cleaned}.md`
}
async function geocodePark(name) {
// Prefer searching for the exact park name as-is; include “National Park” already present.
const params = new URLSearchParams({
q: name,
format: 'jsonv2',
addressdetails: '0',
dedupe: '1',
limit: '1',
countrycodes: COUNTRY_CODES,
})
const url = `${NOMINATIM_BASE}?${params.toString()}`
const res = await fetch(url, {
headers: {
'User-Agent': 'national-parks-markdown/1.0 (contact: example@example.com)',
},
})
if (!res.ok) {
throw new Error(`Geocoding failed for ${name}: ${res.status} ${res.statusText}`)
}
const data = await res.json()
if (!Array.isArray(data) || data.length === 0) {
throw new Error(`No geocoding result for ${name}`)
}
const { lon, lat } = data[0] // strings
if (!lon || !lat) {
throw new Error(`Missing lon/lat for ${name}`)
}
return { lon: String(lon), lat: String(lat) }
}
async function main() {
await fs.mkdir(OUTPUT_DIR, { recursive: true })
for (let i = 0; i < PARKS.length; i++) {
const name = PARKS[i]
const filename = sanitizeFilename(name)
const outPath = path.join(OUTPUT_DIR, filename)
try {
let coords
if (MANUAL_OVERRIDES[name]) {
const [lon, lat] = MANUAL_OVERRIDES[name]
coords = { lon, lat }
} else {
coords = await geocodePark(name)
await sleep(RATE_LIMIT_MS) // be polite between requests
}
const frontmatter = toYAMLFrontmatter(coords)
const content = `${frontmatter}\n\n# ${name}\n`
await fs.writeFile(outPath, content, 'utf8')
console.log(`✓ Wrote ${filename}`)
} catch (err) {
console.error(`✗ Failed ${name}: ${(err && err.message) || err}`)
}
}
console.log('\nDone.')
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment