導入
PHPでgenko.meの配信APIにcurl_*関数で接続し、記事一覧と記事詳細を取得してHTMLに表示するまでの手順。
このページでは、PHPからgenko.meのblogAPIに接続し、記事一覧と記事詳細を取得します。使うのは標準のcURL拡張とjson拡張だけです。
用意するもの
- genko.meのアカウントとワークスペース。APIの作成とAPIキーの発行にはオーナー権限が必要です。
- PHP 8.1以降。以下のコードはコンストラクタプロモーションと
readonlyプロパティを使います。cURL拡張(ext-curl)とjson拡張(ext-json)が有効であること。php -m | grep -E 'curl|json'で確認できます。 - リスト形式のAPIが1つと、公開済みの記事が1件。まだない場合はクイックスタートの手順2・3で作れます。
以降のyour-workspaceは自分のワークスペースIDに、blogは自分のエンドポイントに置き換えます。
インストール
ありません。 Composerで追加するパッケージはなく、以下のコードをプロジェクトに置くだけです。
APIキーとワークスペースIDを設定する
管理画面のURLがhttps://genko.me/w/your-workspaceなら、ワークスペースIDはyour-workspaceです。
APIキーは「設定・プラン」の「APIキー」タブ(/w/your-workspace/settings/api-keys)で作成します。「選択したAPIのみ」でblogだけを許可しておくと、キーが漏れたときの範囲を限定できます。作成直後に表示される値は後から再表示できません。
キーは環境変数から読みます。ApacheならSetEnv、php-fpmならenv[GENKO_API_KEY]、DockerやPaaSならその環境変数の仕組みを使ってください。
$apiKey = getenv('GENKO_API_KEY');キーをPHPファイルに直接書かないでください。 どうしても設定ファイルに置く場合は、ドキュメントルートの外に置き、Gitの追跡対象から外します。.envをドキュメントルート直下に置くと、設定によっては素のテキストとして配信されます。
クライアントを書く
配信APIへのGETを1か所にまとめます。
<?php
declare(strict_types=1);
const GENKO_BASE_URL = 'https://api.genko.me';const GENKO_WORKSPACE = 'your-workspace';
final class GenkoApiError extends RuntimeException{}
/** * 配信APIをGETして、デコード済みの連想配列を返す。 * * @param array<string, string|int> $queries * @return array<string, mixed> */function genko_get(string $path, array $queries = []): array{ $url = GENKO_BASE_URL . '/v1/' . GENKO_WORKSPACE . $path; if ($queries !== []) { $url .= '?' . http_build_query($queries); }
$curl = curl_init($url); curl_setopt_array($curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['X-API-KEY: ' . getenv('GENKO_API_KEY')], ]);
$body = curl_exec($curl); if ($body === false) { throw new GenkoApiError('配信APIへ接続できませんでした: ' . curl_error($curl)); }
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if ($status >= 400) { throw new GenkoApiError($decoded['message'] ?? '原因不明のエラーです。', $status); }
return $decoded;}http_build_query()を使うと、filtersの角括弧や日本語の値がURLエンコードされます。クエリ文字列を自分で連結しないでください。
curl_close()はPHP 8.0以降で意味がなくなり、8.5では非推奨になったため呼んでいません。$curlはスコープを抜けると解放されます。
記事一覧を出す
<?php
require __DIR__ . '/../src/Genko.php';
$list = genko_get('/blog', [ 'limit' => 10, 'orders' => '-publishedAt', 'fields' => 'title,publishedAt',]);
?><h1>ブログ(全<?= (int) $list['totalCount'] ?>件)</h1><ul><?php foreach ($list['contents'] as $post): ?> <li> <a href="/blog.php?id=<?= rawurlencode($post['id']) ?>"> <?= htmlspecialchars($post['title'], ENT_QUOTES, 'UTF-8') ?> </a> </li><?php endforeach; ?></ul>レスポンスはcontents・totalCount・offset・limitを持つ連想配列です。該当する記事がない場合もエラーにはならず、contentsが空配列になります。
テキストフィールドは必ずhtmlspecialchars()を通してください。 配信APIはテキストをエスケープせずに返します。
型を付けて受け取る
連想配列のまま扱うとフィールドIDの打ち間違いに気づけません。スキーマに合わせたクラスを1つ書いておくと、IDEの補完も効きます。
<?php
declare(strict_types=1);
final class Post{ public function __construct( public readonly string $id, public readonly string $title, public readonly string $body, public readonly ?string $category, /** @var list<string> */ public readonly array $tags, public readonly ?DateTimeImmutable $publishedAt, ) { }
/** @param array<string, mixed> $entry */ public static function fromEntry(array $entry): self { return new self( id: $entry['id'], title: $entry['title'] ?? '', body: $entry['body'] ?? '', // 任意項目や、あとから追加したフィールドは古い記事に無いことがある。 category: $entry['category'] ?? null, tags: $entry['tags'] ?? [], publishedAt: isset($entry['publishedAt']) ? new DateTimeImmutable($entry['publishedAt']) : null, ); }}fieldsで項目を絞ると、指定しなかったフィールドはレスポンスに含まれません。??での既定値は省略しないでください。
$posts = array_map(Post::fromEntry(...), $list['contents']);記事詳細を出す
一覧で得たidをパスの末尾に付けます。
<?php
require __DIR__ . '/../src/Genko.php';require __DIR__ . '/../src/Post.php';
$id = $_GET['id'] ?? '';
try { $post = Post::fromEntry(genko_get('/blog/' . rawurlencode($id)));} catch (GenkoApiError $error) { if ($error->getCode() === 404) { http_response_code(404); exit('記事が見つかりません。'); } throw $error;}
?><article> <h1><?= htmlspecialchars($post->title, ENT_QUOTES, 'UTF-8') ?></h1> <?php if ($post->publishedAt !== null): ?> <time datetime="<?= $post->publishedAt->format(DateTimeInterface::ATOM) ?>"> <?= $post->publishedAt->setTimezone(new DateTimeZone('Asia/Tokyo'))->format('Y年n月j日') ?> </time> <?php endif; ?> <?= $post->body ?></article>一覧と違い、詳細のレスポンスはcontentsで包まれず、コンテンツの連想配列がそのまま返ります。非公開・削除済み・存在しないIDはどれも404です。
日時はISO 8601のUTC文字列で届きます。表示前にAsia/Tokyoへ変換してください。
リッチテキスト(body)はHTML文字列なので、htmlspecialchars()を通さずに出力します。管理画面で入力された内容がそのまま出るため、信頼できる書き手だけに編集権限を渡してください。 外部から投稿を受け付ける構成では、HTML Purifierなどでサニタイズしてから出力します。
絞り込む
filtersはフィールドIDと演算子を組み合わせた文字列です。
$tech = genko_get('/blog', [ 'filters' => 'category[equals]tech', 'orders' => '-publishedAt', 'limit' => 10,]);
$recent = genko_get('/blog', [ 'filters' => 'publishedAt[greater_than]' . (new DateTimeImmutable('-30 days'))->format('Y-m-d\TH:i:s.v\Z'),]);演算子の一覧と、[and]/[or]での連結はクエリ仕様を参照してください。
エラーの扱い
| ステータス | 主な原因 |
|---|---|
400 | クエリの形式・値・演算子が不正 |
401 | APIキーがない、無効、期限切れ |
403 | キーとワークスペースが一致しない |
404 | ワークスペース・API・コンテンツがない、またはキーの対象外 |
429 | 同じAPIキーで1分300リクエストを超えた |
上のクライアントはGenkoApiErrorのコードにHTTPステータスを入れています。$error->getCode()で分岐してください。詳しくはエラーと制限にまとめています。
次に読む
ページ送り、保存済み下書きのプレビュー、Webhookの署名検証は実践に続きます。
最終更新: 2026年9月17日