自身のサイトにGoogle Mapを埋め込む
このページでは、Google Mapを自身のサイトに埋め込み、フロントエンドから位置情報を更新する方法を説明します。
概要
Kuroco のコンテンツにはGoogle Mapの位置情報を保存するための拡張形式が準備されています。 Kuroco管理画面から位置情報を入力する場合は、Kuroco管理画面のコンテンツ編集画面で表示される地図上の位置をクリックするだけで位置情報を指定できますが、フロントエンドでマップを表示する場合や、指定した位置情報を保存するインターフェースを提供する場合にはAPIを利用した実装が必要になります。 このチュートリアルでは、フロントエンドにGoogle Mapsのインターフェースを表示し、位置情報を更新する方法を説明します。
前提条件
このページはKurocoでのプロジェクトが構築済みであることを前提としています。
まだ構築していない場合は、下記のチュートリアルを参照してください。
Kurocoビギナーズガイド  
本チュートリアルでは以下のバージョンでコードを書いています。
Nuxt2: v2.15.8
Nuxt3: v3.8.0  
GoogleMapの設定
GCPで新しいプロジェクトを作成
まず、Google Cloud Platform(GCP)で新しいプロジェクトを作成します。
Google Cloud Platformにログインし、ヘッダー部のプロジェクトプルダウンをクリックしてください。
表示されたポップアップ上部の「新しいプロジェクト」をクリックします。

新しいプロジェクト設定画面で、任意のプロジェクト名を入力し、「作成」をクリックします。

以上で新しいプロジェクトが作成されました。
APIの有効化
続いてGCPのコンソールで必要なAPIを有効にします。
まずGCPコンソールのヘッダー部のプルダウンから作成したプロジェクトを選択します。

続いてサイドバーから「APIとサービス」- 「有効なAPIとサービス」を選択してください。

サイドバーから「ライブラリ」を選択するとAPIライブラリページが表示されます。

ここで「Places API」と「Maps JavaScript API」を検索しそれぞれAPIを有効にしてください。


次にAPIキーを発行します。サイドバーから「認証情報」を選択し、「+認証情報を作成」をクリックします。

APIキーを選択します。

これにより、APIキーが生成されます。後で使用しますので控えてください。

許可ドメインの設定
ウェブサイトのドメインをAPIキー登録することで、APIの使用を制御します。これにより、他のウェブサイトからの不正アクセスが防げます。
認証情報の画面内にて、先ほど作成したAPIキーの右にあるボタンから「APIキーを編集」を選択します。

APIキーに任意の名前を入力し、アプリケーションの制限の設定から「ウェブサイト」を選択します。

「ADD」ボタンをクリックし、利用する予定のサイトのドメインを入力、「完了」をクリックします。

Kurocoの設定
コンテンツ定義の設定
Kurocoのコンテンツ定義で拡張項目の任意の箇所に、以下の項目を設定します。
| 項目名 | 入力値 | 
|---|---|
| 項目名 | 地図(googleマップ) | 
| 識別子 | gmap | 
| 項目設定 | 地図 | 

エンドポイントの作成
Kurocoのコンテンツからマップの位置情報を取得するエンドポイントを作成します。
以下のように入力してください。
| 項目名 | 入力値 | 
|---|---|
| パス | /rmcs-api/(API ID)/map/details/{topics_id} | 
| モデル | コンテンツ - Topics - details | 
| APIリクエスト制限 | (閲覧を許可するグループまたはメンバーフィルタを選択) | 
| キャッシュ | 86400 | 
| topics_group_id | (表示するコンテンツのコンテンツ定義ID) | 

次に、マップの位置情報更新用のエンドポイントを作成します。
以下のように入力してください。
| 項目名 | 入力値 | 
|---|---|
| パス | /rmcs-api/(API ID)/map/update/{topics_id} | 
| モデル | コンテンツ - Topics - update | 
| APIリクエスト制限 | (変更を許可するグループまたはメンバーフィルタを選択) | 
| topics_group_id | (更新したいコンテンツのコンテンツ定義ID) | 
| use_columns | gmap | 

フロントエンドの実装
モジュールのインストール
Google MapsのVue.jsコンポーネントを提供するモジュールをインストールします。
- Nuxt2
 - Nuxt3
 
npm install vue2-google-maps
npm install vue3-google-map
GCP KEYをconfigに記載
生成されたAPIキーを、ウェブサイトの設定ファイル(通常はconfigファイル)に記述します。これにより、ウェブサイトがAPIを利用できるようになります。
Nuxt.jsで実装している場合、.env などに以下のように記載してください。
BASE_URL=https://sample-service-site.g.kuroco.app
GCP_KEY=**************************************
.envファイルの内容をデプロイ時に利用する場合は、.gitignoreから.envを削除して、.envもGitHubで管理するか、 GitHubのシークレットに登録した内容をGitHubActionsで読み込んで使用します。
上記の設定を読み込むため、設定ファイルに以下のように追記してください。
- Nuxt2
 - Nuxt3
 
export default {
  env: {
    GCP_KEY: process.env.GCP_KEY
  },
  // (中略)
  plugins: [
    // (中略)
    '@/plugins/vue2-google-maps.client'
  ],
export default defineNuxtConfig({
    runtimeConfig: {
        // Public keys that are exposed to the client
        public: {
            gcpKey: process.env.GCP_KEY,
            apiBase: 'https://*********.g.kuroco.app'
        }
    },
(Nuxt2のみ)
plugins/ ディレクトリにvue2-google-maps.client.js というファイルを追加し、以下のように記載してください。
- Nuxt2
 
import Vue from 'vue';
import * as VueGoogleMaps from 'vue2-google-maps';
Vue.use(VueGoogleMaps, {
    load: {
        key: process.env.GCP_KEY,
        libraries: 'places'
    }
});
GoogleMapを表示するページの実装
ウェブサイトにGoogle Mapを表示してみましょう。 インストールしたコンポーネントを使用し、以下のように実装します。
- Nuxt2
 - Nuxt3
 
<template>
  <div class="container">
    <h3>地図(googleマップ)</h3>
    <form id="topics_edit" @submit.prevent="update">
      <div>
        <GmapMap
          ref="gmap"
          :center="mapCenter"
          :zoom="gmap_zoom"
          :map-type-id="gmap_type"
          style="width: 500px; height: 300px"
        >
          <GmapMarker
            v-if="markPlace"
            :position="markPlace"
          />
        </GmapMap>
      </div>
    </form>
  </div>
</template>
<script>
export default {
  async asyncData({ $axios, route }) {
    const id = route.params.id;
    const url = `/rcms-api/1/test/${id}`;
    const contents = await $axios
      .$get(url)
      .then((response) => {
        if (response.details) {
          return response.details;
        }
        return {};
      })
      .catch((error) => {
        console.log(error);
        return {};
      });
    // Googleマップの初期状態をセット
    let mapCenter = { lat: 35.66107078220203, lng: 139.7584319114685 };
    let markPlace = null;
    if (contents.gmap?.gmap_x && contents.gmap?.gmap_y) {
      const lat = Number(contents.gmap.gmap_y);
      const lng = Number(contents.gmap.gmap_x);
      mapCenter = { lat, lng };
      markPlace = { lat, lng };
    }
    return {
      mapCenter,
      markPlace,
      id,
      contents,
      errors: []
    };
  },
  computed: {
    gmap_zoom() {
      return Number(this.contents.gmap?.gmap_zoom) || 15;
    },
    gmap_type() {
      return this.contents.gmap?.gmap_type || 'roadmap';
    }
  }
};
</script>
<template>
  <div v-if="data" class="container">
    <h3>地図(googleマップ)</h3>
    <form id="topics_edit" @submit.prevent="update">
      <div>
        <GoogleMap
          :mapId="MAP_ID"
          ref="gmap"
          :center="mapCenter"
          :zoom="gmap_zoom"
          :map-type-id="gmap_type"
          style="width: 500px; height: 300px"
          @click="mark($event)"
          @zoom_changed="setZoom"
          @maptypeid_changed="gmap_type = $event"
          :api-key="key"
        >
          <AdvancedMarker
            v-if="markPlace"
            :options="markerOptions"
            @click="mapClicked"
          />
        </GoogleMap>
      </div>
      <input type="submit" value="Save" />
    </form>
  </div>
</template>
<script setup>
import { AdvancedMarker, GoogleMap } from "vue3-google-map";
const route = useRoute();
const config = useRuntimeConfig();
const key = config.public.gcpKey;
const gmap = ref(null);
const mapCenter = ref({ lat: 35.66107078220203, lng: 139.7584319114685 });
const markPlace = ref(null);
const markerOptions = computed(() => ({
  position: markPlace.value || mapCenter.value,
  draggable: true,
}));
const id = ref(route.params.id);
const contents = ref({});
const MAP_ID = "DEMO_MAP_ID";
console.log("googleMap", gmap.value);
const { data } = await useAsyncData("mapDetails", async () => {
  const url = `/rcms-api/1/newsdetail/${id.value}`;
  try {
    const response = await $fetch(url, {
      method: "GET",
      baseURL: config.public.apiBase,
      credentials: "include",
    });
    if (response.details) {
      return response.details;
    }
    return {};
  } catch (error) {
    return {};
  }
});
onMounted(() => {
  contents.value = data.value;
  if (contents.value.gmap?.gmap_x && contents.value.gmap?.gmap_y) {
    const lat = Number(contents.value.gmap.gmap_y)
      ? Number(contents.value.gmap.gmap_y)
      : 35.66107078220203;
    const lng = Number(contents.value.gmap.gmap_x)
      ? Number(contents.value.gmap.gmap_x)
      : 139.7584319114685;
    mapCenter.value = { lat, lng };
    markPlace.value = { lat, lng };
  }
});
const gmap_zoom = computed({
  get: () => Number(contents.value.gmap?.gmap_zoom) || 15,
  set: (val) => {
    if (!contents.value.gmap) contents.value.gmap = {};
    contents.value.gmap.gmap_zoom = String(val);
  },
});
const gmap_type = computed({
  get: () => contents.value.gmap?.gmap_type || "roadmap",
  set: (val) => {
    if (!contents.value.gmap) contents.value.gmap = {};
    contents.value.gmap.gmap_type = val;
  },
});
function mapClicked(event) {
  // console.log("mapCLicked", { event });
}
function setZoom() {
  contents.value.gmap.gmap_zoom = gmap.value.zoom;
}
</script>
利用するエンドポイントのURLはご自身のものに調整してください。
実行結果は以下のようになります。

GoogleMapの位置情報を更新するページの実装
続いて、フロントエンドから位置情報を更新できるようにしてみましょう。
- Nuxt2
 - Nuxt3
 
<template>
  <div class="container">
    <h3>地図(googleマップ)</h3>
    <div>
      地図上をクリックすると設定される位置が変わります。ズームなどの状態も設定できます。
    </div>
    <form id="topics_edit" @submit.prevent="update">
      <div>
        <form onsubmit="return false;">
          <GmapAutocomplete
            :options="{fields: ['geometry']}"
            :select-first-on-enter="true"
            @place_changed="setPlace"
          />
          <GmapMap
            ref="gmap"
            :center="mapCenter"
            :zoom="gmap_zoom"
            :map-type-id="gmap_type"
            style="width: 500px; height: 300px"
            @click="mark($event)"
            @zoom_changed="gmap_zoom = $event"
            @maptypeid_changed="gmap_type = $event"
          >
            <GmapMarker
              v-if="markPlace"
              :position="markPlace"
            />
          </GmapMap>
        </form>
      </div>
      <input
        type="submit"
        value="保存"
      >
    </form>
  </div>
</template>
<script>
export default {
  async asyncData({ $axios, route }) {
    const id = route.params.id;
    const url = `/rcms-api/1/test/${id}`;
    const contents = await $axios
      .$get(url)
      .then((response) => {
        if (response.details) {
          return response.details;
        }
        return {};
      })
      .catch((error) => {
        console.log(error);
        return {};
      });
    // Googleマップの初期状態をセット
    let mapCenter = { lat: 35.66107078220203, lng: 139.7584319114685 };
    let markPlace = null;
    if (contents.gmap?.gmap_x && contents.gmap?.gmap_y) {
      const lat = Number(contents.gmap.gmap_y);
      const lng = Number(contents.gmap.gmap_x);
      mapCenter = { lat, lng };
      markPlace = { lat, lng };
    }
    return {
      mapCenter,
      markPlace,
      id,
      contents,
      errors: []
    };
  },
  computed: {
    gmap_zoom: {
      get() { return Number(this.contents.gmap?.gmap_zoom) || 15; },
      set(val) { this.contents.gmap.gmap_zoom = String(val); }
    },
    gmap_type: {
      get() { return this.contents.gmap?.gmap_type || 'roadmap'; },
      set(val) { this.contents.gmap.gmap_type = val; }
    }
  },
  methods: {
    setPlace(place) {
      if (place.geometry) {
        this.markPlace = {
          lat: place.geometry.location.lat(),
          lng: place.geometry.location.lng()
        };
        if (place.geometry.viewport) {
          this.$refs.gmap.fitBounds(place.geometry.viewport);
        } else {
          this.$refs.gmap.panTo(place.geometry.location);
        }
      }
    },
    mark(event) {
      this.markPlace = {
        lat: event.latLng.lat(),
        lng: event.latLng.lng()
      };
    },
    async update() {
      const params = {
        gmap: {
          gmap_x: '',
          gmap_y: '',
          gmap_zoom: (this.contents?.gmap?.gmap_zoom || 15),
          gmap_type: (this.contents?.gmap?.gmap_type || 'roadmap')
        }
      };
      if (this.markPlace) {
        params.gmap.gmap_x = String(this.markPlace.lng);
        params.gmap.gmap_y = String(this.markPlace.lat);
      }
      await this.$axios.post(
        '/rcms-api/1/update_news/' + this.$route.params.id,
        params
      ).then((response) => {
        if (response.data.errors?.length) {
          console.log(response.data.errors);
        }
        this.errors = [];
      }).catch((error) => {
        console.log(error);
      });
    }
  }
};
</script>
<template>
  <div v-if="data" class="container">
    <h3>地図(googleマップ)</h3>
    <div>
      地図上をクリックすると設定される位置が変わります。ズームなどの状態も設定できます。
    </div>
    <form id="topics_edit" @submit.prevent="update">
      <div>
        <GoogleMap
          :mapId="MAP_ID"
          ref="gmap"
          :center="mapCenter"
          :zoom="gmap_zoom"
          :map-type-id="gmap_type"
          style="width: 500px; height: 300px"
          @click="mark($event)"
          @zoom_changed="setZoom"
          @maptypeid_changed="gmap_type = $event"
          :api-key="key"
        >
          <AdvancedMarker
            v-if="markPlace"
            :options="markerOptions"
            @click="mapClicked"
          />
        </GoogleMap>
      </div>
      <input type="submit" value="保存" />
    </form>
  </div>
</template>
<script setup>
import { AdvancedMarker, GoogleMap } from "vue3-google-map";
const route = useRoute();
const config = useRuntimeConfig();
const key = config.public.gcpKey;
const gmap = ref(null);
const mapCenter = ref({ lat: 35.66107078220203, lng: 139.7584319114685 });
const markPlace = ref(null);
const markerOptions = computed(() => ({
  position: markPlace.value || mapCenter.value,
  draggable: true,
}));
const id = ref(route.params.id);
const contents = ref({});
const errors = ref([]);
const MAP_ID = "DEMO_MAP_ID";
console.log("googleMap", gmap.value);
const { data } = await useAsyncData("mapDetails", async () => {
  const url = `/rcms-api/1/newsdetail/${id.value}`;
  try {
    const response = await $fetch(url, {
      method: "GET",
      baseURL: config.public.apiBase,
      credentials: "include",
    });
    if (response.details) {
      return response.details;
    }
    return {};
  } catch (error) {
    return {};
  }
});
onMounted(() => {
  contents.value = data.value;
  if (contents.value.gmap?.gmap_x && contents.value.gmap?.gmap_y) {
    const lat = Number(contents.value.gmap.gmap_y)
      ? Number(contents.value.gmap.gmap_y)
      : 35.66107078220203;
    const lng = Number(contents.value.gmap.gmap_x)
      ? Number(contents.value.gmap.gmap_x)
      : 139.7584319114685;
    mapCenter.value = { lat, lng };
    markPlace.value = { lat, lng };
  }
});
const gmap_zoom = computed({
  get: () => Number(contents.value.gmap?.gmap_zoom) || 15,
  set: (val) => {
    if (!contents.value.gmap) contents.value.gmap = {};
    contents.value.gmap.gmap_zoom = String(val);
  },
});
const gmap_type = computed({
  get: () => contents.value.gmap?.gmap_type || "roadmap",
  set: (val) => {
    if (!contents.value.gmap) contents.value.gmap = {};
    contents.value.gmap.gmap_type = val;
  },
});
function mark(event) {
  markPlace.value = {
    lat: event.latLng.lat(),
    lng: event.latLng.lng(),
  };
  update();
}
function setZoom() {
  contents.value.gmap.gmap_zoom = gmap.value.map.zoom;
}
async function update() {
  const params = {
    gmap: {
      gmap_x: "",
      gmap_y: "",
      gmap_zoom: String(contents.value?.gmap?.gmap_zoom) || "15",
      gmap_type: contents.value?.gmap?.gmap_type || "roadmap",
    },
  };
  if (markPlace.value) {
    params.gmap.gmap_x = String(markPlace.value.lng);
    params.gmap.gmap_y = String(markPlace.value.lat);
  }
  try {
    const response = await $fetch(
      "/rcms-api/1/update_news/" + route.params.id,
      {
        method: "POST",
        credentials: "include",
        baseURL: config.public.apiBase,
        body: params,
      }
    );
    // console.log(response);
    if (response.data.errors?.length) {
      console.log(response.data.errors);
    }
    errors.value = [];
  } catch (error) {}
}
</script>
利用するエンドポイントのURLはご自身のものに調整してください。
実行結果は以下のようになります。

保存ボタンをクリックするとピンを立てた位置やズームの状態などがKurocoのDBに書き込まれます。
上記のサンプルコードでは簡単のため、非ログイン状態でもページが表示されるようになってますが、通常はログインしてから更新をおこないます。ログインについては以下をご参照ください。
KurocoとNuxt.jsで、ログイン画面を構築する
以上で、Google Mapをあなたのウェブサイトに埋め込み、ウェブサイト上からKuroco管理画面と同様に位置情報を変更する事ができるようになりました。これらの手順によって、ウェブサイトのユーザーに素晴らしい地図体験を提供できます。
サポート
お探しのページは見つかりましたか?解決しない場合は、問い合わせフォームからお問い合わせいただくか、Slackコミュニティにご参加ください。