feat: Kommando zum Nachschlagen der Google-Place-ID
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m37s

Die Place-ID steht nicht in der Google Cloud Console — dort gibt es nur
den API-Schluessel. Sie gehoert zum Eintrag bei Google Maps und muss
ueber die Places API nachgeschlagen werden.

  pnpm places:find "Wabenhain Altenmünster"

gibt Name, Anschrift und GOOGLE_PLACE_ID je Treffer aus.

Das Kommando ist zugleich die schnellste Probe auf die Einrichtung:
kommt eine ID heraus, sind "Places API (New)", Abrechnung und Schluessel
richtig gesetzt und die Startseite kann die Rezensionen abrufen. Kommt
eine Ablehnung, nennt sie die drei tatsaechlich moeglichen Ursachen
(API nicht aktiviert, keine Abrechnung, OAuth-Client statt
API-Schluessel bzw. Referrer-Beschraenkung) — statt wie Google pauschal
auf den Schluessel zu zeigen.

Die Feldmaske fordert nur id, displayName und formattedAddress an: die
API rechnet nach angeforderten Feldern ab.

tsc: 0 Fehler. eslint: 0 Warnungen.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-19 06:32:05 +02:00
1 parent 6639da130e
commit 29f59adc6b
3 files changed
+112 -1

No files matched your search

+9 -1
View File
@@ -216,7 +216,15 @@ Die Bewertungen-Sektion der Startseite zeigt echte Google-Rezensionen, sobald `G
1. **API aktivieren:** In der Cloud Console unter *APIs & Dienste → Bibliothek* nach `Places API (New)` suchen und aktivieren. Es gibt dort zwei fast gleich heißende Einträge — die alte „Places API" ist die falsche, und die „Maps JavaScript API" hilft hier ebenfalls nicht. 1. **API aktivieren:** In der Cloud Console unter *APIs & Dienste → Bibliothek* nach `Places API (New)` suchen und aktivieren. Es gibt dort zwei fast gleich heißende Einträge — die alte „Places API" ist die falsche, und die „Maps JavaScript API" hilft hier ebenfalls nicht.
2. **Abrechnung aktivieren:** Ohne hinterlegtes Rechnungskonto antwortet auch die neue API mit einem Berechtigungsfehler. Das kostenlose Kontingent deckt diese Nutzung (ein Abruf pro Stunde) bei Weitem ab. 2. **Abrechnung aktivieren:** Ohne hinterlegtes Rechnungskonto antwortet auch die neue API mit einem Berechtigungsfehler. Das kostenlose Kontingent deckt diese Nutzung (ein Abruf pro Stunde) bei Weitem ab.
3. **API-Schlüssel erzeugen:** *APIs & Dienste → Anmeldedaten → Anmeldedaten erstellen → API-Schlüssel*. Ein **OAuth-Client ist etwas anderes** und funktioniert hier nicht — er ist für Anmeldungen im Namen eines Nutzers gedacht, nicht für Serverabrufe. 3. **API-Schlüssel erzeugen:** *APIs & Dienste → Anmeldedaten → Anmeldedaten erstellen → API-Schlüssel*. Ein **OAuth-Client ist etwas anderes** und funktioniert hier nicht — er ist für Anmeldungen im Namen eines Nutzers gedacht, nicht für Serverabrufe.
4. **Werte setzen:** Schlüssel als `GOOGLE_PLACES_API_KEY` (Secret), Place-ID als `GOOGLE_PLACE_ID` (ConfigMap). Die Place-ID des eigenen Eintrags findet sich über den [Place ID Finder](https://developers.google.com/maps/documentation/places/web-service/place-id). 4. **Werte setzen:** Schlüssel als `GOOGLE_PLACES_API_KEY` (Secret), Place-ID als `GOOGLE_PLACE_ID` (ConfigMap).
**Die Place-ID steht nicht in der Cloud Console.** Sie gehört zum Eintrag bei Google Maps, nicht zum Projekt. Mit gesetztem Schlüssel liefert sie dieses Kommando:
```bash
pnpm places:find "Wabenhain Altenmünster"
```
Es gibt Name, Anschrift und `GOOGLE_PLACE_ID=…` je Treffer aus — und ist zugleich die schnellste Probe auf die Einrichtung: kommt eine ID heraus, stimmen API, Abrechnung und Schlüssel. Andernfalls benennt es die Ursache. Ohne Schlüssel geht es auch über den [Place ID Finder](https://developers.google.com/maps/documentation/places/web-service/place-id).
Der Schlüssel wird ausschließlich serverseitig verwendet und erscheint nie im Browser — eine Referrer-Beschränkung greift daher **nicht** und würde den Abruf blockieren. Sinnvoll ist stattdessen eine API-Beschränkung auf „Places API (New)" und optional eine IP-Beschränkung auf die ausgehende Adresse des Clusters. Der Schlüssel wird ausschließlich serverseitig verwendet und erscheint nie im Browser — eine Referrer-Beschränkung greift daher **nicht** und würde den Abruf blockieren. Sinnvoll ist stattdessen eine API-Beschränkung auf „Places API (New)" und optional eine IP-Beschränkung auf die ausgehende Adresse des Clusters.
+1
View File
@@ -18,6 +18,7 @@
"test:watch": "vitest", "test:watch": "vitest",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"preflight": "tsx scripts/preflight.ts", "preflight": "tsx scripts/preflight.ts",
"places:find": "tsx scripts/find-place-id.ts",
"orders:release-stale": "tsx scripts/release-stale-orders.ts" "orders:release-stale": "tsx scripts/release-stale-orders.ts"
}, },
"dependencies": { "dependencies": {
+102
View File
@@ -0,0 +1,102 @@
/**
* Sucht die Place-ID eines Google-Maps-Eintrags.
*
* Die Place-ID steht NICHT in der Google Cloud Console — dort gibt es nur den
* API-Schluessel. Sie gehoert zum Eintrag bei Google Maps und muss ueber die
* Places API nachgeschlagen werden. Genau das tut dieses Skript.
*
* Es ist zugleich der schnellste Weg, die Einrichtung zu pruefen: kommt hier
* eine ID heraus, sind API, Abrechnung und Schluessel richtig gesetzt und die
* Startseite kann die Rezensionen abrufen. Kommt stattdessen eine
* Fehlermeldung, benennt sie die Ursache — anders als die Meldung der alten
* API, die bei nicht aktivierter Schnittstelle behauptet, der Schluessel sei
* ungueltig.
*
* Aufruf:
* pnpm places:find "Wabenhain Altenmünster"
*/
import "dotenv/config";
const SEARCH_URL = "https://places.googleapis.com/v1/places:searchText";
const TIMEOUT_MS = 10_000;
interface SearchResponse {
places?: {
id?: string;
displayName?: { text?: string };
formattedAddress?: string;
}[];
error?: { message?: string; status?: string };
}
function fail(message: string): never {
console.error(`\n✖ ${message}\n`);
process.exit(1);
}
async function main(): Promise<void> {
const query = process.argv.slice(2).join(" ").trim();
if (!query) {
fail('Bitte einen Suchbegriff angeben, z.B.: pnpm places:find "Wabenhain Altenmünster"');
}
const apiKey = process.env.GOOGLE_PLACES_API_KEY?.trim();
if (!apiKey) {
fail(
"GOOGLE_PLACES_API_KEY ist nicht gesetzt. Den API-Schluessel aus der Google Cloud Console\n" +
" in die .env eintragen (Anmeldedaten → API-Schluessel, NICHT OAuth-Client)."
);
}
const response = await fetch(SEARCH_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Goog-Api-Key": apiKey,
// Nur die drei Felder, die unten ausgegeben werden — die API rechnet
// nach angeforderten Feldern ab.
"X-Goog-FieldMask": "places.id,places.displayName,places.formattedAddress",
},
body: JSON.stringify({ textQuery: query, languageCode: "de" }),
signal: AbortSignal.timeout(TIMEOUT_MS),
});
const data = (await response.json().catch(() => null)) as SearchResponse | null;
if (!response.ok || data?.error) {
const detail = data?.error?.message ?? `HTTP ${response.status}`;
fail(
`Google hat die Anfrage abgelehnt: ${detail}\n\n` +
" Die drei haeufigsten Ursachen, in dieser Reihenfolge:\n" +
' 1. "Places API (New)" ist im Projekt nicht aktiviert (die gleichnamige ALTE\n' +
" API reicht nicht — sie laesst sich in neuen Projekten gar nicht mehr\n" +
" aktivieren).\n" +
" 2. Am Projekt ist keine Abrechnung hinterlegt.\n" +
" 3. Der Wert ist ein OAuth-Client statt eines API-Schluessels, oder der\n" +
" Schluessel traegt eine Referrer-Beschraenkung — dieser Abruf laeuft\n" +
" serverseitig und hat keinen Referrer."
);
}
const places = data?.places ?? [];
if (places.length === 0) {
fail(`Kein Eintrag zu "${query}" gefunden. Genauer suchen, z.B. mit Ort oder Strasse.`);
}
console.log(`\nTreffer für "${query}":\n`);
for (const place of places) {
console.log(` ${place.displayName?.text ?? "(ohne Namen)"}`);
console.log(` ${place.formattedAddress ?? "(ohne Adresse)"}`);
console.log(` GOOGLE_PLACE_ID=${place.id ?? "(ohne ID)"}\n`);
}
console.log(
"Den passenden Wert als GOOGLE_PLACE_ID eintragen — in der .env fuer lokal,\n" +
"in k8s/01-configmap.yaml fuer den Cluster.\n"
);
}
main().catch((error: unknown) => {
// Netzwerkfehler und Zeitueberschreitungen landen hier; alles Fachliche ist
// oben bereits mit einer deutlicheren Meldung abgefangen.
fail(`Abruf fehlgeschlagen: ${error instanceof Error ? error.message : "Unbekannter Fehler"}`);
});