Google Sitesで、用語集を管理するようになったので、備忘録
こんなの。
スプレッドシートでメンテできるので、チームで管理できるので便利。Q&Aにも応用できる。
Google Sitesに用語集を簡単に載せる方法
スプレッドシート×Apps Scriptで作る検索機能付き動的用語集
Google Sitesに用語集を載せる一つの方法
企業の用語集について、以下のような課題をお持ちではないでしょうか:
❌ PDFやWordの用語集 → 更新が大変、検索しにくい、Google Sitesに載せにくい
❌ 社内システムの奥に隠れた用語集 → アクセスしにくい、存在を忘れられがち
❌ 静的なHTMLページ → 作成・更新に技術スキルが必要
❌ Google Sitesに用語集を載せる具体的な方法がわからない
今回は、Google Workspaceだけでこれらの課題を解決する一つの方法をご紹介します。
なぜこのシステムが企業に最適なのか
🎯 Google Sitesとの完璧な統合
- 会社のWebサイトにiframe一行で埋め込み完了
- Google Workspaceのシングルサインオンでセキュアなアクセス
- 企業サイトのデザインと自然に統合
📊 スプレッドシートでメンテナンス
従来: HTML編集 → FTPアップロード → テスト → 公開
新方式: スプレッドシートに1行追加 → 完了!
💰 コスト0円
- Googleの無料サービスのみ使用
- サーバー不要、ドメイン不要、専用ソフト不要
👥 複数人管理が簡単
- 各部署の担当者がスプレッドシートを直接編集
- 権限管理はGoogleスプレッドシートの共有機能で完結
- 編集履歴・コメント機能で運用も安心
完成イメージ:企業用語集の理想形
✨ 主な機能
- リアルタイム検索: 用語・読み方・説明文から瞬時に検索
- タグフィルタ: 「AI」「技術」「ツール」などで絞り込み
- あいうえお順ナビ: 日本語用語のスムーズなブラウジング
- アコーディオン表示: クリックで詳細表示/非表示
- レスポンシブ対応: PC・スマホ・タブレット完全対応
- 自動同期: スプレッドシート更新で即座にサイトに反映
🏢 企業サイトでの活用例
IT企業の場合
https://company.com/glossary
┌─────────────────────────────────┐
│ ◯◯株式会社 - 技術用語集 │
├─────────────────────────────────┤
│ 🔍 [検索ボックス] │
│ タグ: [AI] [クラウド] [セキュリティ] │
│ │
│ 【API】 │
│ 読み方: エーピーアイ │
│ ▼ 説明: アプリケーション間の... │
│ │
│ 【Gemini】 │
│ 読み方: ジェミニ │
│ ▼ 説明: Googleが開発した... │
└─────────────────────────────────┘
医療機関の場合
◯◯病院 - 医療用語集
- 診療科別フィルタ
- 緊急度別色分け
- 患者様向け平易な説明
製造業の場合
◯◯工業 - 技術用語集
- 工程別分類
- 安全基準用語
- 品質管理用語
2つのアプローチから選択可能
🔄 アプローチの選択
用語集の構成に応じて、以下の3つのアプローチからお選びいただけます:
パターンA: 統合版(1つのアプリ)
- 日本語・英語・数字すべてを1つのアプリで管理
- あいうえお順 + A-Z + 数字のナビゲーション
- シンプルな構成でメンテナンスしやすい
パターンB: 日本語専用版
- ひらがな・カタカナ・漢字の用語のみ
- あ行〜わ行の美しいナビゲーション
- 日本語に特化した最適化
パターンC: 英語専用版
- アルファベット・数字で始まる用語のみ
- A〜Z + 0-9のクリアなナビゲーション
- 英語圏の用語集スタイル
💡 どのアプローチを選ぶべきか
用語の構成 | おすすめアプローチ | 理由 |
---|---|---|
日英混在(少量) | 統合版 | シンプルで管理しやすい |
日本語メイン | 日本語専用版 | 美しいあいうえお順ナビ |
英語メイン | 英語専用版 | アルファベット順の明快さ |
大量の日英混在 | 2つの専用版 | ユーザーが迷わない |
アプローチA: 統合版の実装
Step 1: スプレッドシートの準備
データ構造(4列構成)
A列:用語 | B列:読み方 | C列:説明 | D列:タグ |
---|---|---|---|
Gemini | ジェミニ | Googleの大規模言語モデル | AI,Google |
API | エーピーアイ | アプリケーション間の連携仕様 | 技術,Web |
テキスト埋め込み | テキストうめこみ | 文章を数値ベクトルに変換する技術 | 技術 |
サンプルデータ例
用語,読み方,説明,タグ
Gemini,ジェミニ,Googleが開発した、マルチモーダルに対応した大規模言語モデルです,AI,Google
AI,エーアイ,人間のように学習や推論、判断を行うコンピューター技術やシステムです,AI,技術
テキスト埋め込み,テキストうめこみ,文章を数値のベクトルに変換する技術,技術
HTML,エイチティーエムエル,ウェブページの構造を記述するためのマークアップ言語です,技術,Web
スプレッドシート,スプレッドシート,表計算ソフトです。データ管理、分析、視覚化に役立ちます,ツール,Google
Step 2: Google Apps Scriptの設定
2.1 スクリプトエディタを開く
- Googleスプレッドシートで「拡張機能」→「Apps Script」
- プロジェクト名を「企業用語集」に変更
2.2 Code.gs の実装
function doGet(e) {
// あなたのスプレッドシートIDに変更してください
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID_HERE';
const SHEET_NAME = 'シート1';
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
return HtmlService.createHtmlOutput('シートが見つかりませんでした。');
}
const data = sheet.getDataRange().getValues();
const termsData = data.slice(1); // ヘッダー行をスキップ
// 用語を五十音順でソート
termsData.sort((a, b) => {
const termA = a[0] || '';
const termB = b[0] || '';
return termA.localeCompare(termB, 'ja', { numeric: true });
});
const tagsSet = new Set();
const termsByCategory = {};
termsData.forEach(row => {
const term = row[0] || '';
const reading = row[1] || '';
const description = row[2] || '';
const tagsString = row[3] || '';
const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
tags.forEach(tag => tagsSet.add(tag));
const category = getCategory(term);
if (!termsByCategory[category]) {
termsByCategory[category] = [];
}
termsByCategory[category].push({ term, reading, description, tags });
});
const template = HtmlService.createTemplateFromFile('index');
template.termsData = termsByCategory;
template.tags = Array.from(tagsSet).sort();
return template.evaluate()
.setTitle('企業用語集')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
} catch (err) {
return HtmlService.createHtmlOutput(`エラーが発生しました: ${err.message}`);
}
}
// 用語を適切なカテゴリに分類
function getCategory(term) {
if (!term) return 'その他';
const firstChar = term.charAt(0);
// ひらがな分類
if (firstChar >= 'あ' && firstChar <= 'ん') {
if (firstChar >= 'あ' && firstChar <= 'お') return 'あ行';
if (firstChar >= 'か' && firstChar <= 'ご') return 'か行';
if (firstChar >= 'さ' && firstChar <= 'ぞ') return 'さ行';
if (firstChar >= 'た' && firstChar <= 'ど') return 'た行';
if (firstChar >= 'な' && firstChar <= 'の') return 'な行';
if (firstChar >= 'は' && firstChar <= 'ぽ') return 'は行';
if (firstChar >= 'ま' && firstChar <= 'も') return 'ま行';
if (firstChar >= 'や' && firstChar <= 'よ') return 'や行';
if (firstChar >= 'ら' && firstChar <= 'ろ') return 'ら行';
if (firstChar >= 'わ' && firstChar <= 'ん') return 'わ行';
}
// カタカナ分類
if (firstChar >= 'ア' && firstChar <= 'ン') {
if (firstChar >= 'ア' && firstChar <= 'オ') return 'あ行';
if (firstChar >= 'カ' && firstChar <= 'ゴ') return 'か行';
if (firstChar >= 'サ' && firstChar <= 'ゾ') return 'さ行';
if (firstChar >= 'タ' && firstChar <= 'ド') return 'た行';
if (firstChar >= 'ナ' && firstChar <= 'ノ') return 'な行';
if (firstChar >= 'ハ' && firstChar <= 'ポ') return 'は行';
if (firstChar >= 'マ' && firstChar <= 'モ') return 'ま行';
if (firstChar >= 'ヤ' && firstChar <= 'ヨ') return 'や行';
if (firstChar >= 'ラ' && firstChar <= 'ロ') return 'ら行';
if (firstChar >= 'ワ' && firstChar <= 'ン') return 'わ行';
}
// アルファベット
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z')) {
return 'A-Z';
}
// 数字
if (firstChar >= '0' && firstChar <= '9') {
return '数字';
}
return 'その他';
}
2.3 index.html の作成
「ファイル」→「新規」→「HTMLファイル」でindex.html
を作成
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Meiryo', sans-serif;
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-item.open .accordion-content {
max-height: 500px;
}
.tag-button {
background-color: #e5e7eb;
color: #374151;
font-weight: 600;
font-size: 0.875rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
transition-property: background-color, color;
transition-duration: 0.2s;
}
.tag-button:hover {
background-color: #d1d5db;
}
.tag-button.active {
background-color: #3b82f6;
color: white;
}
.fixed-nav {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.glossary-container {
padding-bottom: 50vh;
}
.nav-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.nav-buttons a {
padding: 0.5rem 1rem;
background-color: #f3f4f6;
border-radius: 0.5rem;
font-weight: 600;
color: #374151;
text-decoration: none;
transition: all 0.2s;
}
.nav-buttons a:hover {
background-color: #3b82f6;
color: white;
transform: translateY(-1px);
}
</style>
</head>
<body class="bg-gray-100 p-4 sm:p-8">
<div class="max-w-4xl mx-auto bg-white rounded-xl shadow-lg p-6 sm:p-10">
<h1 class="text-3xl sm:text-4xl font-bold text-center text-gray-800 mb-6">企業用語集</h1>
<div class="fixed-nav flex flex-col gap-4 py-4 px-6 mx-6 mb-6">
<!-- 検索ボックス -->
<div class="relative mb-4">
<input type="text" id="searchInput" placeholder="用語を検索..."
class="w-full px-4 py-2 border rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 pl-10">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<!-- タグフィルター -->
<div class="flex flex-wrap gap-2">
<span class="text-gray-600 mr-2">タグ:</span>
<div class="flex flex-wrap gap-2">
<button onclick="filterByTag('all', this)" class="tag-button active">すべて</button>
<? for (let i = 0; i < tags.length; i++) { ?>
<button onclick="filterByTag('<?= tags[i] ?>', this)" class="tag-button"><?= tags[i] ?></button>
<? } ?>
</div>
</div>
<!-- ナビゲーション -->
<div class="nav-buttons">
<a href="#" onclick="scrollToCategory('あ行')">あ行</a>
<a href="#" onclick="scrollToCategory('か行')">か行</a>
<a href="#" onclick="scrollToCategory('さ行')">さ行</a>
<a href="#" onclick="scrollToCategory('た行')">た行</a>
<a href="#" onclick="scrollToCategory('な行')">な行</a>
<a href="#" onclick="scrollToCategory('は行')">は行</a>
<a href="#" onclick="scrollToCategory('ま行')">ま行</a>
<a href="#" onclick="scrollToCategory('や行')">や行</a>
<a href="#" onclick="scrollToCategory('ら行')">ら行</a>
<a href="#" onclick="scrollToCategory('わ行')">わ行</a>
<a href="#" onclick="scrollToCategory('A-Z')">A-Z</a>
<a href="#" onclick="scrollToCategory('数字')">数字</a>
<a href="#" onclick="scrollToCategory('その他')">その他</a>
</div>
</div>
<!-- 用語リスト -->
<div id="glossary-container" class="glossary-container">
<?
const categoryOrder = ['あ行', 'か行', 'さ行', 'た行', 'な行', 'は行', 'ま行', 'や行', 'ら行', 'わ行', 'A-Z', '数字', 'その他'];
categoryOrder.forEach(category => {
if (termsData[category]) {
const terms = termsData[category];
?>
<h2 id="<?= category ?>" class="text-2xl sm:text-3xl font-bold text-blue-600 mt-8 mb-4 pt-16 -mt-16"><?= category ?></h2>
<? terms.forEach(term => { ?>
<div class="accordion-item p-4 border rounded-lg mb-2 cursor-pointer hover:bg-gray-50 transition-colors" onclick="toggleAccordion(this)">
<div class="flex justify-between items-start mb-2">
<div class="flex flex-col">
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-xl sm:text-2xl font-semibold text-gray-800"><?= term.term ?></span>
<? if (term.reading) { ?>
<span class="text-sm text-gray-500">(<?= term.reading ?>)</span>
<? } ?>
</div>
</div>
<? if (term.tags && term.tags.length > 0) { ?>
<div class="flex flex-wrap gap-1 ml-2">
<? term.tags.forEach(tag => { ?>
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full"><?= tag ?></span>
<? }); ?>
</div>
<? } ?>
</div>
<div class="accordion-content mt-2">
<p class="text-gray-700 leading-relaxed"><?= term.description ?></p>
</div>
</div>
<? }); ?>
<? }
}); ?>
</div>
</div>
<script>
let allTermItems = [];
document.addEventListener('DOMContentLoaded', () => {
allTermItems = Array.from(document.querySelectorAll('.accordion-item'));
});
function toggleAccordion(element) {
element.classList.toggle('open');
const content = element.querySelector('.accordion-content');
if (element.classList.contains('open')) {
content.style.maxHeight = content.scrollHeight + 'px';
} else {
content.style.maxHeight = '0';
}
}
function filterByTag(tag, clickedButton) {
const allButtons = document.querySelectorAll('.tag-button');
allButtons.forEach(btn => btn.classList.remove('active'));
clickedButton.classList.add('active');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
allTermItems.forEach(item => item.style.display = 'none');
const filteredItems = allTermItems.filter(item => {
const termText = item.querySelector('.text-xl').textContent.toLowerCase();
const readingElement = item.querySelector('.text-sm');
const readingText = readingElement ? readingElement.textContent.toLowerCase() : '';
const descriptionText = item.querySelector('p').textContent.toLowerCase();
const tagsContainer = item.querySelector('.flex-wrap.gap-1.ml-2');
let tagMatch = false;
if (tag === 'all') {
tagMatch = true;
} else if (tagsContainer) {
const tagElements = tagsContainer.querySelectorAll('span');
const tagTexts = Array.from(tagElements).map(span => span.textContent);
tagMatch = tagTexts.includes(tag);
}
let searchMatch = false;
if (searchTerm === '') {
searchMatch = true;
} else {
searchMatch = termText.includes(searchTerm) || readingText.includes(searchTerm) || descriptionText.includes(searchTerm);
}
return tagMatch && searchMatch;
});
filteredItems.forEach(item => item.style.display = 'block');
updateHeadingsVisibility();
}
function filterBySearch() {
const allButtons = document.querySelectorAll('.tag-button');
allButtons.forEach(btn => btn.classList.remove('active'));
document.querySelector('.tag-button').classList.add('active');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
allTermItems.forEach(item => item.style.display = 'none');
const filteredItems = allTermItems.filter(item => {
const termText = item.querySelector('.text-xl').textContent.toLowerCase();
const readingElement = item.querySelector('.text-sm');
const readingText = readingElement ? readingElement.textContent.toLowerCase() : '';
const descriptionText = item.querySelector('p').textContent.toLowerCase();
return termText.includes(searchTerm) || readingText.includes(searchTerm) || descriptionText.includes(searchTerm);
});
filteredItems.forEach(item => item.style.display = 'block');
updateHeadingsVisibility();
}
function updateHeadingsVisibility() {
const headings = document.querySelectorAll('#glossary-container h2');
headings.forEach(heading => {
let hasVisibleTerm = false;
let nextSibling = heading.nextElementSibling;
while (nextSibling && nextSibling.tagName.toLowerCase() !== 'h2') {
if (nextSibling.style.display !== 'none') {
hasVisibleTerm = true;
break;
}
nextSibling = nextSibling.nextElementSibling;
}
heading.style.display = hasVisibleTerm ? 'block' : 'none';
});
}
function scrollToCategory(category) {
document.getElementById('searchInput').value = '';
filterBySearch();
const element = document.getElementById(category);
if (element) {
const navHeight = document.querySelector('.fixed-nav').offsetHeight;
const offsetPosition = element.offsetTop - navHeight;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
document.getElementById('searchInput').addEventListener('input', filterBySearch);
</script>
</body>
</html>
Step 3: Webアプリとしてデプロイ
- 「デプロイ」→「新しいデプロイ」 をクリック
- 種類: 「ウェブアプリ」を選択
- 実行者: 「自分」
- アクセス権限: 「組織内の全員」を選択
- 「デプロイ」 をクリック
- WebアプリURLをコピー
Step 4: Google Sitesに埋め込み
4.1 Google Sitesでページ作成
- Google Sitesで企業サイトを開く
- 「用語集」ページを作成
4.2 iframe埋め込み
- 「挿入」→「埋め込み」→「URL」
- Apps ScriptのWebアプリURLを貼り付け
- 幅: 100%、高さ: 800px に設定
<iframe src="https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec"
width="100%"
height="800"
frameborder="0">
</iframe>
アプローチB: 日本語専用版の実装
特徴
- 対象: ひらがな・カタカナ・漢字で始まる用語のみ
- ナビゲーション: あ行〜わ行 + その他
- デザイン: エメラルドグリーン系(日本的な色合い)
- 最適化: 日本語フォント、行間、文字サイズ
Google Apps Script(日本語専用版)
Code.gs
// 日本語用語集専用版
function doGet(e) {
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID_HERE';
const SHEET_NAME = 'シート1';
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
return HtmlService.createHtmlOutput('シートが見つかりませんでした。');
}
const data = sheet.getDataRange().getValues();
const termsData = data.slice(1);
// 日本語用語のみを抽出
const japaneseTerms = termsData.filter(row => {
const term = row[0] || '';
return isJapaneseTerm(term);
});
// 日本語の自然なソート
japaneseTerms.sort((a, b) => {
const termA = a[0] || '';
const termB = b[0] || '';
return termA.localeCompare(termB, 'ja', { numeric: true });
});
const tagsSet = new Set();
const termsByCategory = {};
japaneseTerms.forEach(row => {
const term = row[0] || '';
const reading = row[1] || '';
const description = row[2] || '';
const tagsString = row[3] || '';
const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
tags.forEach(tag => tagsSet.add(tag));
const category = getJapaneseCategory(term);
if (!termsByCategory[category]) {
termsByCategory[category] = [];
}
termsByCategory[category].push({ term, reading, description, tags });
});
const template = HtmlService.createTemplateFromFile('index');
template.termsData = termsByCategory;
template.tags = Array.from(tagsSet).sort();
return template.evaluate()
.setTitle('日本語用語集')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
} catch (err) {
return HtmlService.createHtmlOutput(`エラーが発生しました: ${err.message}`);
}
}
// 日本語用語かどうかを判定
function isJapaneseTerm(term) {
if (!term || term.length === 0) return false;
const firstChar = term.charAt(0);
// アルファベットで始まる場合は除外
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z')) {
return false;
}
// 数字で始まる場合も除外
if (firstChar >= '0' && firstChar <= '9') {
return false;
}
return true;
}
// 日本語用語を行別に分類
function getJapaneseCategory(term) {
if (!term || term.length === 0) return 'その他';
const firstChar = term.charAt(0);
// ひらがなの分類
if (firstChar >= 'あ' && firstChar <= 'ん') {
if (firstChar >= 'あ' && firstChar <= 'お') return 'あ行';
if (firstChar >= 'か' && firstChar <= 'ご') return 'か行';
if (firstChar >= 'さ' && firstChar <= 'ぞ') return 'さ行';
if (firstChar >= 'た' && firstChar <= 'ど') return 'た行';
if (firstChar >= 'な' && firstChar <= 'の') return 'な行';
if (firstChar >= 'は' && firstChar <= 'ぽ') return 'は行';
if (firstChar >= 'ま' && firstChar <= 'も') return 'ま行';
if (firstChar >= 'や' && firstChar <= 'よ') return 'や行';
if (firstChar >= 'ら' && firstChar <= 'ろ') return 'ら行';
if (firstChar >= 'わ' && firstChar <= 'ん') return 'わ行';
}
// カタカナの分類
if (firstChar >= 'ア' && firstChar <= 'ン') {
if (firstChar >= 'ア' && firstChar <= 'オ') return 'あ行';
if (firstChar >= 'カ' && firstChar <= 'ゴ') return 'か行';
if (firstChar >= 'サ' && firstChar <= 'ゾ') return 'さ行';
if (firstChar >= 'タ' && firstChar <= 'ド') return 'た行';
if (firstChar >= 'ナ' && firstChar <= 'ノ') return 'な行';
if (firstChar >= 'ハ' && firstChar <= 'ポ') return 'は行';
if (firstChar >= 'マ' && firstChar <= 'モ') return 'ま行';
if (firstChar >= 'ヤ' && firstChar <= 'ヨ') return 'や行';
if (firstChar >= 'ラ' && firstChar <= 'ロ') return 'ら行';
if (firstChar >= 'ワ' && firstChar <= 'ン') return 'わ行';
}
return 'その他';
}
index.html(日本語専用版)
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans CJK JP', 'Meiryo', sans-serif;
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-item.open .accordion-content {
max-height: 500px;
}
.tag-button {
background-color: #e5e7eb;
color: #374151;
font-weight: 600;
font-size: 0.875rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
transition-property: background-color, color;
transition-duration: 0.2s;
}
.tag-button:hover {
background-color: #d1d5db;
}
.tag-button.active {
background-color: #10b981;
color: white;
}
.fixed-nav {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.glossary-container {
padding-bottom: 50vh;
}
.japanese-nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
}
.japanese-nav a {
padding: 0.75rem 1.5rem;
background-color: #dcfce7;
border-radius: 1rem;
font-weight: 700;
color: #166534;
text-decoration: none;
transition: all 0.2s;
font-size: 1.1rem;
}
.japanese-nav a:hover {
background-color: #10b981;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
</style>
</head>
<body class="bg-gradient-to-br from-green-50 to-emerald-100 p-4 sm:p-8">
<div class="max-w-4xl mx-auto bg-white rounded-xl shadow-lg p-6 sm:p-10">
<h1 class="text-3xl sm:text-4xl font-bold text-center text-emerald-800 mb-2">日本語用語集</h1>
<p class="text-center text-gray-600 mb-8">ひらがな・カタカナ・漢字の用語を検索</p>
<div class="fixed-nav flex flex-col gap-4 py-4 px-6 mx-6 mb-6">
<!-- 検索ボックス -->
<div class="relative mb-4">
<input type="text" id="searchInput" placeholder="日本語用語を検索..."
class="w-full px-4 py-2 border-2 border-emerald-200 rounded-full focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 pl-10">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<!-- タグフィルター -->
<div class="flex flex-wrap gap-2">
<span class="text-gray-600 mr-2">タグ:</span>
<div class="flex flex-wrap gap-2">
<button onclick="filterByTag('all', this)" class="tag-button active">すべて</button>
<? for (let i = 0; i < tags.length; i++) { ?>
<button onclick="filterByTag('<?= tags[i] ?>', this)" class="tag-button"><?= tags[i] ?></button>
<? } ?>
</div>
</div>
<!-- 日本語専用ナビゲーション -->
<div class="japanese-nav">
<a href="#" onclick="scrollToCategory('あ行')">あ行</a>
<a href="#" onclick="scrollToCategory('か行')">か行</a>
<a href="#" onclick="scrollToCategory('さ行')">さ行</a>
<a href="#" onclick="scrollToCategory('た行')">た行</a>
<a href="#" onclick="scrollToCategory('な行')">な行</a>
<a href="#" onclick="scrollToCategory('は行')">は行</a>
<a href="#" onclick="scrollToCategory('ま行')">ま行</a>
<a href="#" onclick="scrollToCategory('や行')">や行</a>
<a href="#" onclick="scrollToCategory('ら行')">ら行</a>
<a href="#" onclick="scrollToCategory('わ行')">わ行</a>
<a href="#" onclick="scrollToCategory('その他')">その他</a>
</div>
</div>
<!-- 用語リスト -->
<div id="glossary-container" class="glossary-container">
<?
const categoryOrder = ['あ行', 'か行', 'さ行', 'た行', 'な行', 'は行', 'ま行', 'や行', 'ら行', 'わ行', 'その他'];
categoryOrder.forEach(category => {
if (termsData[category]) {
const terms = termsData[category];
?>
<h2 id="<?= category ?>" class="text-2xl sm:text-3xl font-bold text-emerald-600 mt-8 mb-4 pt-16 -mt-16"><?= category ?></h2>
<? terms.forEach(term => { ?>
<div class="accordion-item p-4 border border-emerald-200 rounded-lg mb-2 cursor-pointer hover:bg-emerald-50 transition-colors" onclick="toggleAccordion(this)">
<div class="flex justify-between items-start mb-2">
<div class="flex flex-col">
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-xl sm:text-2xl font-semibold text-gray-800"><?= term.term ?></span>
<? if (term.reading) { ?>
<span class="text-sm text-gray-500">(<?= term.reading ?>)</span>
<? } ?>
</div>
</div>
<? if (term.tags && term.tags.length > 0) { ?>
<div class="flex flex-wrap gap-1 ml-2">
<? term.tags.forEach(tag => { ?>
<span class="bg-emerald-100 text-emerald-800 text-xs font-medium px-2.5 py-0.5 rounded-full"><?= tag ?></span>
<? }); ?>
</div>
<? } ?>
</div>
<div class="accordion-content mt-2">
<p class="text-gray-700 leading-relaxed"><?= term.description ?></p>
</div>
</div>
<? }); ?>
<? }
}); ?>
</div>
</div>
<script>
let allTermItems = [];
document.addEventListener('DOMContentLoaded', () => {
allTermItems = Array.from(document.querySelectorAll('.accordion-item'));
});
function toggleAccordion(element) {
element.classList.toggle('open');
const content = element.querySelector('.accordion-content');
if (element.classList.contains('open')) {
content.style.maxHeight = content.scrollHeight + 'px';
} else {
content.style.maxHeight = '0';
}
}
function filterByTag(tag, clickedButton) {
const allButtons = document.querySelectorAll('.tag-button');
allButtons.forEach(btn => btn.classList.remove('active'));
clickedButton.classList.add('active');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
allTermItems.forEach(item => item.style.display = 'none');
const filteredItems = allTermItems.filter(item => {
const termText = item.querySelector('.text-xl').textContent.toLowerCase();
const readingElement = item.querySelector('.text-sm');
const readingText = readingElement ? readingElement.textContent.toLowerCase() : '';
const descriptionText = item.querySelector('p').textContent.toLowerCase();
const tagsContainer = item.querySelector('.flex-wrap.gap-1.ml-2');
let tagMatch = false;
if (tag === 'all') {
tagMatch = true;
} else if (tagsContainer) {
const tagElements = tagsContainer.querySelectorAll('span');
const tagTexts = Array.from(tagElements).map(span => span.textContent);
tagMatch = tagTexts.includes(tag);
}
let searchMatch = false;
if (searchTerm === '') {
searchMatch = true;
} else {
searchMatch = termText.includes(searchTerm) || readingText.includes(searchTerm) || descriptionText.includes(searchTerm);
}
return tagMatch && searchMatch;
});
filteredItems.forEach(item => item.style.display = 'block');
updateHeadingsVisibility();
}
function filterBySearch() {
const allButtons = document.querySelectorAll('.tag-button');
allButtons.forEach(btn => btn.classList.remove('active'));
document.querySelector('.tag-button').classList.add('active');
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
allTermItems.forEach(item => item.style.display = 'none');
const filteredItems = allTermItems.filter(item => {
const termText = item.querySelector('.text-xl').textContent.toLowerCase();
const readingElement = item.querySelector('.text-sm');
const readingText = readingElement ? readingElement.textContent.toLowerCase() : '';
const descriptionText = item.querySelector('p').textContent.toLowerCase();
return termText.includes(searchTerm) || readingText.includes(searchTerm) || descriptionText.includes(searchTerm);
});
filteredItems.forEach(item => item.style.display = 'block');
updateHeadingsVisibility();
}
function updateHeadingsVisibility() {
const headings = document.querySelectorAll('#glossary-container h2');
headings.forEach(heading => {
let hasVisibleTerm = false;
let nextSibling = heading.nextElementSibling;
while (nextSibling && nextSibling.tagName.toLowerCase() !== 'h2') {
if (nextSibling.style.display !== 'none') {
hasVisibleTerm = true;
break;
}
nextSibling = nextSibling.nextElementSibling;
}
heading.style.display = hasVisibleTerm ? 'block' : 'none';
});
}
function scrollToCategory(category) {
document.getElementById('searchInput').value = '';
filterBySearch();
const element = document.getElementById(category);
if (element) {
const navHeight = document.querySelector('.fixed-nav').offsetHeight;
const offsetPosition = element.offsetTop - navHeight;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
document.getElementById('searchInput').addEventListener('input', filterBySearch);
</script>
</body>
</html>
アプローチC: 英語専用版の実装
特徴
- 対象: アルファベット・数字で始まる用語のみ
- ナビゲーション: A〜Z + 0-9 + Other
- デザイン: ブルー系(国際的でプロフェッショナル)
- 最適化: 英語フォント、アルファベット順ソート
Google Apps Script(英語専用版)
Code.gs
// 英語用語集専用版
function doGet(e) {
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID_HERE';
const SHEET_NAME = 'シート1';
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
return HtmlService.createHtmlOutput('シートが見つかりませんでした。');
}
const data = sheet.getDataRange().getValues();
const termsData = data.slice(1);
// 英語用語のみを抽出
const englishTerms = termsData.filter(row => {
const term = row[0] || '';
return isEnglishTerm(term);
});
// アルファベット順でソート
englishTerms.sort((a, b) => {
const termA = a[0] || '';
const termB = b[0] || '';
return termA.localeCompare(termB, 'en', { numeric: true });
});
const tagsSet = new Set();
const termsByCategory = {};
englishTerms.forEach(row => {
const term = row[0] || '';
const reading = row[1] || '';
const description = row[2] || '';
const tagsString = row[3] || '';
const tags = tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
tags.forEach(tag => tagsSet.add(tag));
const category = getEnglishCategory(term);
if (!termsByCategory[category]) {
termsByCategory[category] = [];
}
termsByCategory[category].push({ term, reading, description, tags });
});
const template = HtmlService.createTemplateFromFile('index');
template.termsData = termsByCategory;
template.tags = Array.from(tagsSet).sort();
return template.evaluate()
.setTitle('英語用語集')
.setSandboxMode(HtmlService.SandboxMode.IFRAME);
} catch (err) {
return HtmlService.createHtmlOutput(`エラーが発生しました: ${err.message}`);
}
}
// 英語用語かどうかを判定
function isEnglishTerm(term) {
if (!term || term.length === 0) return false;
const firstChar = term.charAt(0);
// アルファベットで始まる場合は英語用語
if ((firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z')) {
return true;
}
// 数字で始まる場合も英語セクションに含める
if (firstChar >= '0' && firstChar <= '9') {
return true;
}
return false;
}
// 英語用語をアルファベット順に分類
function getEnglishCategory(term) {
if (!term || term.length === 0) return 'その他';
const firstChar = term.charAt(0).toUpperCase();
// 数字の判定
if (firstChar >= '0' && firstChar <= '9') {
return '数字';
}
// アルファベットの判定
if (firstChar >= 'A' && firstChar <= 'Z') {
return firstChar;
}
return 'その他';
}
index.html(英語専用版)
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Inter', 'Helvetica', 'Arial', sans-serif;
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.accordion-item.open .accordion-content {
max-height: 500px;
}
.tag-button {
background-color: #e5e7eb;
color: #374151;
font-weight: 600;
font-size: 0.875rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
transition-property: background-color, color;
transition-duration: 0.2s;
}
.tag-button:hover {
background-color: #d1d5db;
}
.tag-button.active {
background-color: #3b82f6;
color: white;
}
.fixed-nav {
position: sticky;
top: 0;
background-color: white;
z-index: 10;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.glossary-container {
padding-bottom: 50vh;
}
.english-nav {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.english-nav a {
padding: 0.5rem 0.75rem;
background-color: #dbeafe;
border-radius: 0.5rem;
font-weight: 700;
color: #1e40af;
text-decoration: none;
transition: all 0.2s;
font-size: 1rem;
min-width: 2.5rem;
text-align: center;
}
.english-nav a:hover {
background-color: #3b82f6;
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.english-nav a.special {
background-color: #fef3c7;
color: #92400e;
}
.english-nav a.special:hover {
background-color: #f59e0b;
color: white;
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 p-4 sm:p-8">
<div class="max-w-4xl mx-auto bg-white rounded-xl shadow-lg p-6 sm:p-10">
<h1 class="text-3xl sm:text-4xl font-bold text-center text-blue-800 mb-2">English Glossary</h1>
<p class="text-center text-gray-600 mb-8">Search for alphabetic terms and acronyms</p>
<div class="fixed-nav flex flex-col gap-4 py-4 px-6 mx-6 mb-6">
<!-- 検索ボックス -->
<div class="relative mb-4">
<input type="text" id="searchInput" placeholder="Search English terms..."
class="w-full px-4 py-2 border-2 border-blue-200 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 pl-10">
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<!-- タグフィルター -->
<div class="flex flex-wrap gap-2">
<span class="text-gray-600 mr-2">Tags:</span>
<div class="flex flex-wrap gap-2">
<button onclick="filterByTag('all', this)" class="tag-button active">All</button>
<? for (let i = 0; i < tags.length; i++) { ?>
<button onclick="filterByTag('<?= tags[i] ?>', this)" class="tag-button"><?= tags[i] ?></button>
<? } ?>
</div>
</div>
<!-- アルファベット専用ナビゲーション -->
<div class="english-nav">
<? const alphabetNav = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); ?>
<? alphabetNav.forEach(letter => { ?>
<a href="#" onclick="scrollToCategory('<?= letter ?>')"><?= letter ?></a>
<? }); ?>
<a href="#" onclick="scrollToCategory('数字')" class="special">0-9</a>
<a href="#" onclick="scrollToCategory('その他')" class="special">Other</a>
</div>
</div>
<!-- 用語リスト -->
<div id="glossary-container" class="glossary-container">
<?
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const categoryOrder = [...alphabet, '数字', 'その他'];
categoryOrder.forEach(category => {
if (termsData[category]) {
const terms = termsData[category];
?>
<h2 id="<?= category ?>" class="text-2xl sm:text-3xl font-bold text-blue-600 mt-8 mb-4 pt-16 -mt-16"><?= category ?></h2>
<? terms.forEach(term => { ?>
<div class="accordion-item p-4 border border-blue-200 rounded-lg mb-2 cursor-pointer hover:bg-blue-50 transition-colors" onclick="toggleAccordion(this)">
<div class="flex justify-between items-start mb-2">
<div class="flex flex-col">
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-xl sm:text-2xl font-semibold text-gray-800"><?= term.term ?></span>
<? if (term.reading) { ?>
<span class="text-sm text-gray-500">(<?= term.reading ?>)</span>
<? } ?>
</div>
</div>
<? if (term.tags && term.tags.length > 0) { ?>
<div class="flex flex-wrap gap-1 ml-2">
<? term.tags.forEach(tag => { ?>
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full"><?= tag ?></span>
<? }); ?>
</div>
<? } ?>
</div>
<div class="accordion-content mt-2">
<p class="text-gray-700 leading-relaxed"><?= term.description ?></p>
</div>
</div>
<? }); ?>
<? }
}); ?>
</div>
</div>
<script>
// [JavaScriptコードは日本語版と同様のため省略]
</script>
</body>
</html>
💡 2つの専用版を作る場合の手順
- script.google.com で「日本語用語集」プロジェクトを作成
- 日本語専用のコードを設定・デプロイ
- 別プロジェクトとして「English Glossary」を作成
- 英語専用のコードを設定・デプロイ
- それぞれのURLをGoogle Sitesに埋め込み
🖥️ Google Sitesでの表示パターン
パターン1: 1つのページに統合版
<iframe src="https://script.google.com/macros/s/UNIFIED_APP_ID/exec"
width="100%" height="800" frameborder="0">
</iframe>
パターン2: 2つのページに分離
<!-- /glossary-japanese ページ -->
<iframe src="https://script.google.com/macros/s/JAPANESE_APP_ID/exec"
width="100%" height="800" frameborder="0">
</iframe>
<!-- /glossary-english ページ -->
<iframe src="https://script.google.com/macros/s/ENGLISH_APP_ID/exec"
width="100%" height="800" frameborder="0">
</iframe>
パターン3: 1つのページに2つ並列表示
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 400px;">
<h3>🇯🇵 日本語用語集</h3>
<iframe src="https://script.google.com/macros/s/JAPANESE_APP_ID/exec"
width="100%" height="600" frameborder="0">
</iframe>
</div>
<div style="flex: 1; min-width: 400px;">
<h3>🔤 English Glossary</h3>
<iframe src="https://script.google.com/macros/s/ENGLISH_APP_ID/exec"
width="100%" height="600" frameborder="0">
</iframe>
</div>
</div>
企業での活用シナリオ
🏢 IT企業での想定活用例
想定される課題
- PDF用語集:月1回更新、検索困難
- 社内WikiやConfluence:アクセスが煩雑
- 新人教育:用語理解に時間がかかる
- 部署間の用語認識差
期待される改善
- スプレッドシート更新: 各部署が随時用語追加可能
- 企業サイトに統合: 社外からもアクセス可能
- 検索効率化: 用語検索時間の短縮
- 新人研修短縮: 用語習得の効率化
🏥 医療機関での想定活用例
カスタマイズポイント
// 診療科別タグ自動付与
function addDepartmentTags(term, description) {
const departments = {
'内科': ['血圧', '糖尿病', '高血圧'],
'外科': ['手術', '麻酔', '切開'],
'小児科': ['予防接種', '発育', '成長']
};
for (const [dept, keywords] of Object.entries(departments)) {
if (keywords.some(keyword => description.includes(keyword))) {
return dept;
}
}
return '共通';
}
セキュリティ設定
// 病院内IPからのみアクセス許可
function checkHospitalIP() {
const allowedIPs = ['192.168.1.0/24', '10.0.0.0/16'];
// IP制限ロジック
}
🏭 製造業での活用例
品質管理用語の特殊表示
<!-- 重要度による色分け -->
<span class="<?= term.importance === 'critical' ? 'bg-red-500 text-white' : 'bg-gray-100' ?>">
<?= term.term ?>
</span>
運用とメンテナンス
📋 日常的なメンテナンス
1. 用語の追加・編集
手順: スプレッドシートを開く → 新しい行に入力 → 保存
結果: 即座にGoogle Sitesに反映
2. 複数人での編集管理
権限設定:
- 編集者: 各部署の用語集担当者
- 閲覧者: 全社員
- オーナー: IT部門
3. 編集履歴の確認
Google スプレッドシート → 表示 → 変更履歴
→ いつ・誰が・何を変更したかを確認可能
🔧 高度なカスタマイズ
企業ブランドカラーの適用
:root {
--company-primary: #your-brand-color;
--company-secondary: #your-secondary-color;
}
.tag-button.active {
background-color: var(--company-primary);
}
自動バックアップ機能
function createWeeklyBackup() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const backup = ss.copy(`用語集バックアップ_${new Date().toISOString().split('T')[0]}`);
// 専用フォルダに移動
const folder = DriveApp.getFolderById('BACKUP_FOLDER_ID');
DriveApp.getFileById(backup.getId()).moveTo(folder);
}
// 毎週日曜日に自動実行
ScriptApp.newTrigger('createWeeklyBackup')
.timeBased()
.everyWeeks(1)
.onWeekDay(ScriptApp.WeekDay.SUNDAY)
.create();
よくある質問と解決方法
Q1: 「エラーが発生しました」と表示される
A1: よくある原因と解決策
- スプレッドシートIDが間違っている → URLを再確認
- シート名が「シート1」と一致しない → Code.gsの
SHEET_NAME
を修正 - Apps Scriptの実行権限がない → 初回実行時に権限を許可
Q2: Google Sitesで表示されない
A2: 確認ポイント
- WebアプリのURLが正しいか確認
- アクセス権限が「組織内の全員」になっているか確認
- iframeのサイズ設定を確認
Q3: スプレッドシートを更新してもサイトに反映されない
A3: 対処方法
- ブラウザのキャッシュをクリア(Ctrl+F5)
- Apps Scriptで新しいバージョンをデプロイ
- しばらく待ってから再度確認
Q4: 社外からアクセスできない
A4: アクセス権限の設定
// 組織内限定の場合
function checkDomain() {
const user = Session.getActiveUser().getEmail();
if (!user.endsWith('@yourcompany.com')) {
return HtmlService.createHtmlOutput('アクセス権限がありません');
}
}
期待できる効果
📊 導入により期待できる改善
定量的な改善の可能性
- 用語検索時間の短縮
- 新人研修期間の短縮
- 用語関連問い合わせの削減
- 用語追加・更新頻度の向上
定性的な改善の可能性
- 新人の学習効率向上
- 部署間のコミュニケーション改善
- 顧客対応時の用語説明の統一
- 社内外への情報発信時の一貫性向上
まとめ
🎯 このシステムの本当の価値
技術的価値
- Google Workspaceの既存リソースを最大活用
- コスト0円で企業レベルの用語集システムを構築
- スプレッドシート→Apps Script→Google Sitesのシームレス連携
業務的価値
- 用語管理業務の劇的な効率化
- 新人教育・研修コストの削減
- 組織内コミュニケーションの円滑化
戦略的価値
- 企業の知識資産の体系化・活用
- 情報アクセシビリティの向上
- デジタル化推進の具体的な成果
🚀 導入を推奨する企業
- Google Workspaceを使用している企業 → 即座に導入可能
- 専門用語が多い業界 → IT、医療、金融、製造業など
- 新人教育を効率化したい企業 → オンボーディング期間短縮
- 部署間連携を改善したい企業 → 用語の統一で意思疎通向上
📈 期待できる投資対効果
初期コスト: 0円(Googleの無料サービス)
開発工数: 1-2日(技術者1名の場合)
運用コスト: 0円(継続的に無料)
期待される効果:
- 用語検索時間短縮による生産性向上
- 新人研修期間短縮によるコスト削減
- 問い合わせ対応工数削減
- 情報共有品質向上による業務効率化
💡 最後に
Google Workspaceの標準機能だけで、企業レベルの高機能用語集システムが構築できます。
このシステムは単なる用語集を超えて、企業の知識共有プラットフォームとして機能します。スプレッドシートでのメンテナンス性の良さと、Google Sitesでの高いアクセシビリティを両立した実用的なソリューションです。
ぜひあなたの企業でも、この 「スプレッドシート×Apps Script×Google Sites」 の組み合わせで、用語集システムを構築してみてください!
完全版コード
Google Apps Script(Code.gs)
[上記のCode.jsコード]
HTMLテンプレート(index.html)
[上記のindex.htmlコード]
設定手順チェックリスト
- スプレッドシート作成・データ入力
- Apps Scriptプロジェクト作成
- Code.gs・index.html設定
- スプレッドシートID更新
- Webアプリデプロイ・URL取得
- Google SitesにiFrame埋め込み
- 動作確認・権限設定
参考リンク
Keywords: Google Sites, スプレッドシート, Apps Script, 企業用語集, Webアプリ, iframe埋め込み, 社内ツール, Google Workspace