genko.meDocs

導入

Goでgenko.meの配信APIに接続し、net/httpで記事一覧と記事詳細を取得して構造体へデコードする手順。

このページでは、Goからgenko.meのblogAPIに接続し、記事一覧と記事詳細を取得します。使うのは標準ライブラリだけです。

用意するもの

  • genko.meのアカウントとワークスペース。APIの作成とAPIキーの発行にはオーナー権限が必要です。
  • Go 1.21以降。以下のコードはジェネリクス(1.18以降)とhttp.NewRequestWithContextを使います。
  • リスト形式のAPIが1つと、公開済みの記事が1件。まだない場合はクイックスタートの手順2・3で作れます。

以降のyour-workspaceは自分のワークスペースIDに、blogは自分のエンドポイントに置き換えます。

インストール

ありません。 go getするものはなく、以下のコードをプロジェクトに置くだけです。

APIキーとワークスペースIDを設定する

管理画面のURLがhttps://genko.me/w/your-workspaceなら、ワークスペースIDはyour-workspaceです。

APIキーは「設定・プラン」の「APIキー」タブ(/w/your-workspace/settings/api-keys)で作成します。「選択したAPIのみ」でblogだけを許可しておくと、キーが漏れたときの範囲を限定できます。作成直後に表示される値は後から再表示できません。

キーは環境変数から読み、起動時に存在を確かめます。

go
apiKey := os.Getenv("GENKO_API_KEY")if apiKey == "" {    log.Fatal("GENKO_API_KEYが設定されていません。")}

os.Getenvは未設定でも空文字を返すだけなので、この確認がないと401を受け取ってから気づくことになります。キーをソースに直接書かないでください。

クライアントを書く

配信APIへのGETを1か所にまとめます。

genko/client.go
package genko
import (	"context"	"encoding/json"	"fmt"	"net/http"	"net/url")
const baseURL = "https://api.genko.me"
// Client は配信APIへの、1ワークスペース分の接続。type Client struct {	Workspace string	APIKey    string	HTTP      *http.Client}
// APIError は配信APIが返したエラー応答。type APIError struct {	Status  int	Message string `json:"message"`}
func (e *APIError) Error() string {	return fmt.Sprintf("genko: 配信APIが%dを返しました: %s", e.Status, e.Message)}
// List はリスト形式APIの一覧レスポンス。type List[T any] struct {	Contents   []T `json:"contents"`	TotalCount int `json:"totalCount"`	Offset     int `json:"offset"`	Limit      int `json:"limit"`}
func (c *Client) get(ctx context.Context, path string, query url.Values, dest any) error {	endpoint := baseURL + "/v1/" + c.Workspace + path	if len(query) > 0 {		endpoint += "?" + query.Encode()	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)	if err != nil {		return err	}	req.Header.Set("X-API-KEY", c.APIKey)
	res, err := c.HTTP.Do(req)	if err != nil {		return err	}	defer res.Body.Close()
	if res.StatusCode >= 400 {		apiErr := &APIError{Status: res.StatusCode}		// message が読めなくてもステータスは返す。		_ = json.NewDecoder(res.Body).Decode(apiErr)		return apiErr	}
	return json.NewDecoder(res.Body).Decode(dest)}
// GetList は一覧を取得する。Goのメソッドは型パラメータを持てないので関数にする。func GetList[T any](ctx context.Context, c *Client, endpoint string, query url.Values) (*List[T], error) {	var list List[T]	if err := c.get(ctx, "/"+endpoint, query, &list); err != nil {		return nil, err	}	return &list, nil}
// GetEntry はリスト形式APIの1件を取得する。func GetEntry[T any](ctx context.Context, c *Client, endpoint, id string, query url.Values) (*T, error) {	var entry T	if err := c.get(ctx, "/"+endpoint+"/"+url.PathEscape(id), query, &entry); err != nil {		return nil, err	}	return &entry, nil}

url.Values.Encode()を使うと、filtersの角括弧や日本語の値がURLエンコードされます。クエリ文字列を自分で連結しないでください。

スキーマを構造体にする

管理画面のフィールドIDをjsonタグに書きます。

genko/post.go
package genko
import "time"
// Post はブログAPIのフィールド。管理画面のフィールドIDをjsonタグに書く。type Post struct {	ID    string `json:"id"`	Title string `json:"title"`	// リッチテキストはHTML文字列。	Body     string   `json:"body"`	Category string   `json:"category"`	Tags     []string `json:"tags"`	// 画像フィールドの配信値は url と alt だけ。寸法は付かない。	Thumbnail *Image `json:"thumbnail"`	// fields で外すと空になる。ゼロ値かどうかで判断する。	PublishedAt time.Time `json:"publishedAt"`	UpdatedAt   time.Time `json:"updatedAt"`}
type Image struct {	URL string `json:"url"`	Alt string `json:"alt"`}

日時はISO 8601の文字列なので、time.Timeがそのままデコードします。UTCで届くため、表示前にtime.LocalやAsia/Tokyoへ変換してください。

任意項目や、あとから追加したフィールドは、古い記事に値がないことがあります。値がない場合はゼロ値になります。fieldsで項目を絞った場合も同じです。単一参照は*Imageのようにポインタで受け、nilを扱えるようにします。

記事一覧を出す

main.go
package main
import (	"context"	"fmt"	"log"	"net/http"	"net/url"	"os"	"time"
	"example.com/yourapp/genko")
func main() {	apiKey := os.Getenv("GENKO_API_KEY")	if apiKey == "" {		log.Fatal("GENKO_API_KEYが設定されていません。")	}
	client := &genko.Client{		Workspace: "your-workspace",		APIKey:    apiKey,		HTTP:      &http.Client{Timeout: 10 * time.Second},	}
	list, err := genko.GetList[genko.Post](context.Background(), client, "blog", url.Values{		"limit":  {"10"},		"orders": {"-publishedAt"},		"fields": {"title,publishedAt"},	})	if err != nil {		log.Fatal(err)	}
	fmt.Printf("全%d件\n", list.TotalCount)	for _, post := range list.Contents {		fmt.Printf("%s %s %s\n", post.ID, post.Title, post.PublishedAt.Format(time.DateOnly))	}}

レスポンスはContents・TotalCount・Offset・Limitを持ちます。該当する記事がない場合もエラーにはならず、Contentsが空スライスになります。

html/templateに渡す場合、テキストフィールドは自動でエスケープされます。リッチテキスト(Body)はHTMLとして出したいので、template.HTML(post.Body)に変換します。この変換はエスケープを無効にします。 管理画面で入力された内容がそのまま出るため、信頼できる書き手だけに編集権限を渡してください。

記事詳細を出す

一覧で得たIDをパスの末尾に付けます。

go
post, err := genko.GetEntry[genko.Post](ctx, client, "blog", id, nil)
var apiErr *genko.APIErrorif errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {    http.NotFound(w, r)    return}if err != nil {    http.Error(w, "記事を取得できませんでした", http.StatusInternalServerError)    log.Printf("genko: %v", err)    return}

一覧と違い、詳細のレスポンスはcontentsで包まれず、コンテンツのオブジェクトがそのまま返ります。非公開・削除済み・存在しないIDはどれも404です。

errors.Asで*APIErrorを取り出し、Statusで分岐します。ネットワーク障害など、HTTPレスポンス自体を受け取れなかった失敗は*APIErrorになりません。

エラーの中身をそのままブラウザへ返さないでください。 URLやキーがメッセージに含まれる可能性があります。ログには残し、画面には一般的な文言を出します。

絞り込む

filtersはフィールドIDと演算子を組み合わせた文字列です。

go
tech, err := genko.GetList[genko.Post](ctx, client, "blog", url.Values{    "filters": {"category[equals]tech"},    "orders":  {"-publishedAt"},    "limit":   {"10"},})
since := time.Now().AddDate(0, 0, -30).UTC().Format("2006-01-02T15:04:05.000Z")recent, err := genko.GetList[genko.Post](ctx, client, "blog", url.Values{    "filters": {"publishedAt[greater_than]" + since},})

演算子の一覧と、[and]/[or]での連結はクエリ仕様を参照してください。

エラーの扱い

ステータス主な原因
400クエリの形式・値・演算子が不正
401APIキーがない、無効、期限切れ
403キーとワークスペースが一致しない
404ワークスペース・API・コンテンツがない、またはキーの対象外
429同じAPIキーで1分300リクエストを超えた

詳しくはエラーと制限にまとめています。

次に読む

ページ送り、保存済み下書きのプレビュー、Webhookの署名検証は実践に続きます。

最終更新: 2026年9月17日

目次