From 6dee75b8249fd55eacfaef2920eac1758663f647 Mon Sep 17 00:00:00 2001 From: ryohidaka <39184410+ryohidaka@users.noreply.github.com> Date: Fri, 30 Oct 2026 23:19:47 +0900 Subject: [PATCH] feat: add location properties --- AGENTS.md | 12 +++++++++++- README.md | 15 +++++++++++++++ content/shops/yoshimuraya/info.json | 4 ++++ src/app.ts | 4 ++++ src/graphql.ts | 10 ++++++++++ validation/shops.test.ts | 10 ++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 5d91719..c779673 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ This is the main way people contribute. Add one shop end to end. 3. **photo** — path to a real image the user provides. Do not invent one. 4. **author** — `authorId` (`^[0-9a-zA-Z\-\_]+$`), display `name`, profile `url` (`^https?://.+`). Reuse an existing `content/authors/{authorId}` if present. 5. **prefecture** — optional; see step 4. +6. **location** — optional WGS 84 decimal-degree coordinates; see step 4. ### Steps @@ -39,13 +40,19 @@ This is the main way people contribute. Add one shop end to end. 4. **Prefecture** (optional but encouraged) — the 都道府県 the shop is physically in (must end with 都/道/府/県). **Verify the real location** (search the web); do not infer from the name — a "札幌ラーメン" shop can be in Kanagawa, a 家系 shop can be in Tokyo. For a nationwide chain or any shop whose branch you cannot pin down, **omit the field entirely** rather than recording a guess. -5. **Write `content/shops/{shopId}/info.json`** — field order `id`, `name`, `prefecture` (if known), `photos`. Do **not** add a `url` field (it is generated at runtime). + If the branch is confirmed, add `location` as `{ "lat": number, "lng": number }`; otherwise omit it. + +5. **Write `content/shops/{shopId}/info.json`** — field order `id`, `name`, `prefecture` (if known), `location` (if known), `photos`. Do **not** add a `url` field (it is generated at runtime). ```json { "id": "{shopId}", "name": "{name}", "prefecture": "{prefecture}", + "location": { + "lat": 35.463062, + "lng": 139.615063 + }, "photos": [ { "name": "{shopId}-001.jpg", @@ -68,6 +75,8 @@ This is the main way people contribute. Add one shop end to end. | `photos[].width` / `height` | numbers (the real pixel size) | | `photos[].authorId` | `^[0-9a-zA-Z\-\_]+$`, must resolve to an author | | `prefecture` | optional; if present must end with 都/道/府/県 | +| `location.lat` | optional; number between -90 and 90 | +| `location.lng` | optional; number between -180 and 180 | ### Conventions / gotchas @@ -75,6 +84,7 @@ This is the main way people contribute. Add one shop end to end. - Do not create or edit `content/shops.json`; it is generated from the `content/shops/*/` directories (`vp run generate`) and is not committed. Do not edit `tools/legacy-shops-order.json` either; it only freezes the order of the shops added before. - Keep photos under 300KB, longest side ~800px, EXIF stripped. - Omit `prefecture` when uncertain; never fabricate a location. +- Omit `location` when the shop branch cannot be identified; never fabricate coordinates. ## Optional: local checks diff --git a/README.md b/README.md index eb8bf93..6205ab5 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ type Shop = { id: string name?: string prefecture?: string + location?: { + lat: number + lng: number + } photos?: Photo[] } @@ -99,6 +103,10 @@ GET /shops?pretty&prefecture=神奈川県 "id": "yoshimuraya", "name": "吉村家", "prefecture": "神奈川県", + "location": { + "lat": 35.463062, + "lng": 139.615063 + }, "photos": [ { "name": "yoshimuraya-001.jpg", @@ -233,8 +241,14 @@ type Shop { id: String name: String prefecture: String + location: Location photos: [Photo] } + +type Location { + lat: Float + lng: Float +} ``` #### Photo @@ -387,6 +401,7 @@ Edit `./content/shops/{shopId}/info.json` like this: "id": "yoshimuraya", // <-- must be /^[0-9a-z\-]+$/ "name": "吉村家", "prefecture": "神奈川県", // <-- optional. a prefecture name ending with 都道府県 + "location": { "lat": 35.463062, "lng": 139.615063 }, // <-- optional; WGS 84 decimal degrees "photos": [ { "name": "yoshimuraya-001.jpg", // <-- must be /^[0-9a-z\-\.]+\.(jpg|jpeg|png|gif)$/ diff --git a/content/shops/yoshimuraya/info.json b/content/shops/yoshimuraya/info.json index 260c81a..ba84737 100644 --- a/content/shops/yoshimuraya/info.json +++ b/content/shops/yoshimuraya/info.json @@ -2,6 +2,10 @@ "id": "yoshimuraya", "name": "吉村家", "prefecture": "神奈川県", + "location": { + "lat": 35.463062, + "lng": 139.615063 + }, "photos": [ { "name": "yoshimuraya-001.jpg", diff --git a/src/app.ts b/src/app.ts index 69442bf..d868b1d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -39,6 +39,10 @@ export type Shop = { id: string name?: string prefecture?: string + location?: { + lat: number + lng: number + } photos?: Photo[] } diff --git a/src/graphql.ts b/src/graphql.ts index 5fad33a..9aa75bc 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -5,6 +5,7 @@ import { GraphQLSchema, GraphQLList, GraphQLInt, + GraphQLFloat, } from 'graphql' import { getShop, listShops, getAuthor, type Shop, type Options } from '@/app' import { Pager } from '@/pager' @@ -35,6 +36,15 @@ const shopType = new GraphQLObjectType({ id: { type: GraphQLString }, name: { type: GraphQLString }, prefecture: { type: GraphQLString }, + location: { + type: new GraphQLObjectType({ + name: 'Location', + fields: { + lat: { type: GraphQLFloat }, + lng: { type: GraphQLFloat }, + }, + }), + }, photos: { type: new GraphQLList(photoType), }, diff --git a/validation/shops.test.ts b/validation/shops.test.ts index cfd7a32..0ac955b 100644 --- a/validation/shops.test.ts +++ b/validation/shops.test.ts @@ -50,6 +50,16 @@ describe('Validate shops with SELF and REST API', () => { expect(shop.prefecture).toMatch(/^.+[都道府県]$/) } + // location is optional, but when present it must contain valid coordinates + if (shop.location !== undefined) { + expect(typeof shop.location.lat).toBe('number') + expect(typeof shop.location.lng).toBe('number') + expect(shop.location.lat).toBeGreaterThanOrEqual(-90) + expect(shop.location.lat).toBeLessThanOrEqual(90) + expect(shop.location.lng).toBeGreaterThanOrEqual(-180) + expect(shop.location.lng).toBeLessThanOrEqual(180) + } + // Validate each photo for (const photo of shop.photos) { expect(photo.name).toMatch(/^[0-9a-z.-]+\.(jpg|jpeg|png|gif)$/)