# Workout Places — Full AI Discovery Guide Extended guide for AI agents: entity model, URL patterns, data relationships, and real examples. ## 1. Entity model (detail) ### Place - **humanId** (number): Stable public ID. Use in URLs: /places/{humanId}. Do not rely on internal _id. - **name** (string, optional): Custom name; often empty. - **generatedName** (string): Stable fact-based name generated from the address (e.g. "Calisthenics Park at Queen Street, Toronto"). Display name = name, else generatedName, else description. - **description** (string): Short description of the spot. - **latitude**, **longitude** (strings): WGS84 coordinates. - **photos** (array of URLs): Image URLs. Also exposed as **images** (array of { url, thumbnail }) in some responses. - **equipment** (array): Each item has **key** (e.g. pull_up_bars), **name**, **type** (EQUIPMENT | FACILITIES | GAMES). - **address**: city, countryName, countryCode, adminName; plus **citySlug**, **countrySlug** for URLs. - **limitedAccess** (boolean): true if access is restricted. - Only **status: "published"** places appear in public API and sitemap. RU/BY are excluded. - Place pages show user **ratings** (1–5 stars, aggregate near the title) and **reviews** (text), also emitted as aggregateRating/review JSON-LD once at least one published review exists. - **Deleted places return HTTP 410 Gone** (never-existed IDs return 404) — treat a 410 URL as permanently removed. ### City - Not a stored entity. Cities are derived by grouping places by **address.city**. - **Slug**: Lowercase, transliterated (e.g. kyiv, london, new-york). Use in URLs: /city/{slug}. - **List**: GET /api/cities returns cities with slug, name, placeCount, countryName. ### Country - Not a stored entity. Countries are derived by grouping places by **address.countryName** / **address.countryCode**. - **Slug**: From country name/code (e.g. ukraine, romania). Use in URLs: /countries/{slug}. ### Equipment - **key**: URL-safe identifier (e.g. pull_up_bars, parallel_bar). - **name**, **description**, **type** (EQUIPMENT | FACILITIES | GAMES). - Equipment+city pages: /equipment/{equipmentSlug}/{citySlug} where equipmentSlug = key with underscores replaced by hyphens (e.g. pull-up-bars). ## 2. URL patterns | Pattern | Example | Description | |---------|---------|-------------| | /places/{humanId} | /places/12345 | Place detail page | | /city/{citySlug} | /city/kyiv | City hub: places in that city | | /countries/{countrySlug} | /countries/ukraine | Country hub: places in that country | | /equipment/{equipmentSlug}/{citySlug} | /equipment/pull-up-bars/kyiv | Places in city that have this equipment | | /studies/outdoor-gyms-per-capita-2026 | /studies/outdoor-gyms-per-capita-2026 | Data study: mapped outdoor gyms per 100,000 residents, ranked by city and country, with methodology | | /places | /places | Map and list of all places | | /cities | /cities | List of cities | | /countries | /countries | List of countries | ## 3. Data relationships - **Place → City**: place.address.city + place.address.citySlug. Link to city: /city/{address.citySlug}. - **Place → Country**: place.address.countryName + place.address.countrySlug. Link to country: /countries/{address.countrySlug}. - **Place → Equipment**: place.equipment[].key. Link to equipment in city: /equipment/{slugifyEquipment(key)}/{address.citySlug}. - **City → Country**: From aggregation; city has countryName, countryCode. Country slug from slugifyCountry(countryName, countryCode). ## 4. Example place (structure) Example response shape for a single place (e.g. GET /api/public/places/12345): { "humanId": 12345, "description": "Waterfront calisthenics park", "latitude": "50.4501", "longitude": "30.5234", "photos": ["https://res.cloudinary.com/..."], "equipment": [ { "key": "pull_up_bars", "name": "Pull-up bars", "type": "EQUIPMENT" }, { "key": "parallel_bar", "name": "Parallel bar", "type": "EQUIPMENT" } ], "address": { "city": "Kyiv", "countryName": "Ukraine", "countryCode": "UA", "citySlug": "kyiv", "countrySlug": "ukraine" }, "limitedAccess": false } Canonical URL: https://workout-places.com/places/12345 ## 5. Example city page (structure) City hub URL: https://workout-places.com/city/kyiv - Contains: city name, country, a "Key facts" block (mapped spot count, most common equipment with counts, free-access count, community photo count — discrete citable facts), list of places in that city, featured places (top 5 by completeness), equipment summary (equipment types present in the city with place counts), nearby cities (same country), and an FAQ. - API equivalent: GET /api/public/places?city=kyiv or GET /api/cities/kyiv (returns city + places + featuredPlaces + equipmentSummary + nearbyCities). ## 6. Public read-only API summary - **Places by bbox**: GET /api/public/places?bbox=minLat,minLng,maxLat,maxLng&limit=500 - **Places by city**: GET /api/public/places?city={citySlug} - **Place details**: GET /api/public/places/{humanId} All return only published places; RU/BY excluded. Full API spec: https://workout-places.com/api/public/openapi.json ## 7. Sitemap https://workout-places.com/sitemap.xml includes: homepage, static pages, blog posts, every place (/places/{humanId}), every city (/city/{slug}), every country (/countries/{slug}), every equipment+city page (/equipment/{equipmentSlug}/{citySlug}).