https://github.com/azat-io/pinbook
📌 YAML-first CLI for building Google My Maps-ready KML
https://github.com/azat-io/pinbook
cli google-my-maps kml maps travel yaml
Last synced: 2 months ago
JSON representation
📌 YAML-first CLI for building Google My Maps-ready KML
- Host: GitHub
- URL: https://github.com/azat-io/pinbook
- Owner: azat-io
- License: mit
- Created: 2026-03-27T20:54:58.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-05T12:40:24.000Z (2 months ago)
- Last Synced: 2026-04-05T14:27:14.083Z (2 months ago)
- Topics: cli, google-my-maps, kml, maps, travel, yaml
- Language: TypeScript
- Homepage: https://google.com/maps/d/u/0/viewer?mid=1am89OiTz6iQ7sreXEYzyjCbvvY6DR9I
- Size: 6.63 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: readme.md
- Changelog: changelog.config.ts
- Contributing: contributing.md
- License: license.md
- Code of conduct: .github/code_of_conduct.md
- Security: .github/security.md
Awesome Lists containing this project
README
# Pinbook

[](https://npmjs.com/package/pinbook)
[](https://codecov.io/gh/azat-io/pinbook)
[](https://github.com/azat-io/pinbook/blob/main/license.md)
Pinbook is a YAML-first CLI for building Google My Maps-ready KML.
It is designed for travel planning workflows where you want a map format that is
readable by humans, easy to generate with AI, and safe to keep in git.
Plan the trip in plain YAML first, then turn it into a visual map.
## Example
Pinbook can describe a real multi-city trip, not just a tiny demo config.
The canonical example in this repository covers Tokyo, Kyoto, Osaka, Nara, and
Hiroshima as a multi-file travel map:
- Source config: [example/index.yaml](./example/index.yaml)
- Live Google My Maps example:
[Japan Example Map](https://www.google.com/maps/d/viewer?mid=1am89OiTz6iQ7sreXEYzyjCbvvY6DR9I)
## What It Does
- Scaffolds a new map project with `pinbook create`
- Reads an `index.yaml` map config
- Geocodes addresses during build when coordinates are missing
- Writes a `.pinbook/map.kml` file you can import into Google My Maps
- Offers an optional Pinbook skill install for AI agents
## Quick Start
```bash
npx pinbook create my-map
cd my-map
# Edit index.yaml and add at least one pin
# Optional: authorize Google Drive uploads
# when `photo` points to local files
# pnpm exec pinbook drive-auth
pnpm install
pnpm build
```
Then import `.pinbook/map.kml` into Google My Maps.
Optional AI skill:
```bash
npx skills add azat-io/pinbook
```
## Workflow
1. Create a new project with `pinbook create`.
2. Edit `index.yaml`.
3. Run `pnpm build`.
4. Import `.pinbook/map.kml` into Google My Maps.
5. Repeat as the trip plan changes.
## Multi-File Maps
Pinbook also supports root-level `imports` so one map can be split across
multiple YAML files.
This works well for trips organized by city:
```text
index.yaml
cities/
tokyo/
food.yaml
sights.yaml
kyoto.yaml
osaka.yaml
```
Two common conventions:
- **layers for categories** — group by `food`, `sights`, `entertainment` (shown
below)
- **layers for geography** — group by city such as `tokyo`, `kyoto`, `osaka`
(used in the [canonical example](./example/index.yaml))
Both work well. Pick whichever makes the map easier to read at a glance.
Example root config with category-based layers:
```yaml
map:
title: Japan Trip
layers:
- id: food
title: Food
- id: sights
title: Sights
imports:
- ./cities/tokyo/*.yaml
- ./cities/kyoto.yaml
- ./cities/osaka.yaml
pins: []
```
Imported files may contain only `pins`:
```yaml
pins:
- id: onibus-coffee-nakameguro
title: Onibus Coffee Nakameguro
address: Onibus Coffee Nakameguro, Tokyo
layer: food
```
## Minimal Example
```yaml
map:
title: Tokyo First Week
description: >
A first-pass map for a week in Tokyo.
layers:
- id: sights
title: Sights
pins:
- id: senso-ji
title: Senso-ji
address: Senso-ji Temple, Asakusa, Tokyo
layer: sights
color: red-500
icon: places-viewpoint
description: >
Good early-morning stop before the street gets crowded.
photo: https://example.com/photos/senso-ji.jpg
```
## Config Shape
Pinbook maps use four top-level keys:
- `map` for the map title and optional description
- `layers` for optional logical groups such as `food`, `stay`, or `sights`
- `imports` for optional relative YAML paths or glob patterns
- `pins` for the actual places that should appear on the map
Each pin should describe a real place using either:
- `address` when a clear human-readable address exists
- `coords` when no reliable address is available
Pins can also define `color`, `icon`, `description`, `layer`, and `photo`.
`imports` is root-only. Imported files may contain only `pins`.
## Schema Stability
Pinbook treats the current YAML shape as the public map format.
For the future `1.x` line:
- all current root keys and documented fields are considered stable
- new fields may be added in minor releases
- removing, renaming, or changing the meaning of an existing field requires a
major release
## Build Behavior
- If a pin includes `coords`, Pinbook uses them as the final coordinates and
does not geocode the pin.
- If a pin includes `address` and does not include `coords`, Pinbook may call
the Google Geocoding API during build.
- If a pin includes both `address` and `coords`, `coords` are authoritative and
are used as the final coordinates.
- If `imports` is present, Pinbook expands the imported YAML files before final
validation and build.
- Resolved addresses are stored in the local resolution cache so later builds
stay faster and more repeatable.
## Photos
`photo` is supported as either:
- a single full public `http://` or `https://` image URL
- a single local image path such as `./photos/senso-ji.jpg`
- a list that mixes public URLs and local paths
During build, Pinbook includes those images in the generated placemark
description so they can appear in Google My Maps after import.
When `photo` points to a local file, Pinbook uploads it to Google Drive during
build and rewrites it to a public URL before generating KML.
Before upload, Pinbook automatically normalizes local photos by rotating them
when the image says it should be shown differently, cropping from the center to
a fixed 3:2 frame, resizing them to a consistent size, and converting them to
WebP.
By default, Pinbook creates or reuses this folder structure in Google Drive:
```text
Pinbook/
/
```
For example, a map with `map.title: Japan Trip` uploads local photos into:
```text
Pinbook/Japan Trip
```
If `GOOGLE_DRIVE_FOLDER_ID` is set, Pinbook uses that folder as the parent and
creates or reuses:
```text
/
```
For imported YAML files, local photo paths are resolved relative to the file
that declared them.
To authorize local photo uploads:
```bash
pnpm exec pinbook drive-auth
```
That flow stores the Google Drive OAuth client ID, client secret, and refresh
token in the local project `.env` file.
## Geocoding
If a pin has an `address` but no `coords`, Pinbook geocodes the address during
build.
To do that, set `GOOGLE_MAPS_API_KEY` in your environment or in a local `.env`
file inside the map project. In an interactive terminal, Pinbook can also ask
for the key and save it for you.
Resolved addresses are cached locally at
`node_modules/.cache/pinbook/cache.json` so repeated builds stay fast and
stable.
## Google Drive Auth
Pinbook uses two separate Google integrations:
- `GOOGLE_MAPS_API_KEY` for address geocoding
- Google Drive OAuth credentials for uploading local photo files
Google Drive uploads do **not** use a Drive API key.
Run `pinbook drive-auth` to save a refresh token locally, or provide these
variables in the local `.env` file:
```bash
GOOGLE_DRIVE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_DRIVE_CLIENT_SECRET=your-google-oauth-client-secret
GOOGLE_DRIVE_REFRESH_TOKEN=your-google-drive-refresh-token
# Optional parent folder. Pinbook will then upload into
# / instead of Pinbook/.
GOOGLE_DRIVE_FOLDER_ID=your-google-drive-folder-id
```
Uploaded photo metadata is cached locally at
`node_modules/.cache/pinbook/photo-cache.json` so unchanged files are not
uploaded again on every build.
`pinbook drive-auth` stores these values in the local project `.env` file next
to your YAML config and ensures that `.env` is ignored by Git.
## Google Drive Setup
Use this once per Pinbook map project when you want to reference local photo
paths such as `./photos/senso-ji.jpg`.
1. Create or choose a Google Cloud project.
2. Enable the Google Drive API for that project.
3. Open `Google Auth Platform`.
4. Complete the initial app setup in `Branding`. For personal use, a simple app
name and support email are enough.
5. Open `Audience`. If you are using a personal Google account, choose
`External`.
6. Decide whether the app should stay in `Testing` or move to `Production`.
`Testing` is fine for quick experiments, but Google limits it to test users
and test-user authorizations expire after 7 days. `Production` is better for
long-lived personal use.
7. Open `Clients` and create an OAuth client with application type
`Desktop app`.
8. In your Pinbook project, run:
```bash
pnpm exec pinbook drive-auth
```
9. Paste the `Client ID` and `Client Secret`.
10. Open the Google URL printed by Pinbook and finish the sign-in flow.
11. Wait for the terminal message:
```text
Google Drive auth saved to the local .env file.
```
After that, `pnpm build` can upload local photos automatically.
## Google Drive Notes
- `pinbook drive-auth` must run in a local interactive terminal with access to a
browser. Google explicitly documents desktop OAuth as a local flow.
- Pinbook uses the `https://www.googleapis.com/auth/drive.file` scope. Google
classifies it as `Recommended / Non-sensitive`.
- The OAuth exchange can take a little while after the browser says
`Pinbook authorization complete`. The refresh token is not saved until the
terminal prints the success message.
## Google Drive Troubleshooting
- `access_denied` after sign-in usually means the app is still in `Testing` and
your Google account was not added as a test user in `Audience`.
- If local photos are uploaded into Drive root, `GOOGLE_DRIVE_FOLDER_ID` is not
set and the map title folder has not been created yet. Pinbook now creates
`Pinbook/` automatically on the next build.
- If unchanged photos are already in
`node_modules/.cache/pinbook/photo-cache.json`, Pinbook reuses their public
URLs and skips re-uploading them.
- If Pinbook reports that `photo-cache.json` is invalid after an upgrade, delete
it and run build again so the cache can be recreated in the new format.
## Compatibility Target
Pinbook targets manual KML import into Google My Maps.
The generated KML is designed around that workflow, including Pinbook's color,
icon, layer, and photo conventions.
## Known Limitations
- Import behavior ultimately depends on Google My Maps.
- Rich import details such as folders, custom icon rendering, photos, and
description HTML may vary with My Maps behavior.
- Large maps may hit Google My Maps import limits.
- Address-based builds depend on network access to Google Geocoding unless the
required coordinates are already present in the local cache.
- Local photo uploads depend on Google Drive OAuth and public Drive download
links.
- Imported files may contain only `pins`; nested imports are not supported.
## AI-Assisted Workflow
Pinbook publishes a repo-level skill that can be installed into coding agents
with:
```bash
npx skills add azat-io/pinbook
```
That gives AI agents a reusable Pinbook reference for:
- the expected YAML shape
- supported fields
- color and icon conventions
- a consistent authoring style for map configs
## Current Scope
Pinbook currently focuses on one job: building KML from YAML for Google My Maps.
That means:
- it exports KML for manual import into Google My Maps
- it does not sync directly with Google My Maps
- it can upload local pin photos to Google Drive during build
- it is optimized for travel-planning style maps with readable YAML configs
## Contributing
See
[Contributing Guide](https://github.com/azat-io/pinbook/blob/main/contributing.md).
## License
MIT © [Azat S.](https://azat.io)