---
title: "ビジュアル編集を有効にする"
slug: "enable-visual-editing"
abstract: "フロントエンドアプリのSitecoreAIページビルダーで、フレームワーク固有のSitecore SDKを使わずにWYSIWYG編集を有効にする方法。"
---
# ビジュアル編集を有効にする

> !注**このドキュメントの改善にご協力ください**フレームワークに依存しないドキュメントは現在開発中です。コンテンツ改善の提案があれば、このページの下部で **フィードバック** を共有してください。

このウォークスルーでは、SitecoreAIの重要な開発タスクであるSitecoreAIページビルダーでコンテンツ作成者がWYSIWYG編集を有効にする方法を説明します。ウォークスルーの終わりには、ページビルダーがフロントエンドアプリをiframeに表示し、コンテンツ作成者がキャンバス上で直接ページの内容を編集できるようにします。

このウォークスルーは、Astro(v7)およびGo(v1)のフロントエンドアプリケーションのコード例を提供し、以下の方法を説明します:

:::攻略

1.  [Sitecoreの環境変数を設定しましょう](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#configure-your-sitecore-environment-variables)
2.  [GraphQLを使ったレイアウト編集データの取得](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#retrieve-editing-layout-data-using-graphql)
3.  [HTMLにメタデータラッパーを追加](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#add-metadata-wrappers-to-the-html)
4.  [作成 /api/editing/config](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#create-apieditingconfig)
5.  [作成 /api/editing/render](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#create-apieditingrender)
6.  [新しいエンドポイントを登録する](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#register-the-new-endpoints)
7.  [ページビルダーをConnectに localhost](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#connect-the-page-builder-to-localhost)
8.  [視覚編集のテスト](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/enable-visual-editing.html#test-visual-editing):::

> !注**始める前に**
> 
> -   アプリのウォークスルー [でRender SitecoreAIのコンテンツを](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/content-rendering/render-sitecoreai-content-in-your-app.html) 完成させてください。このウォークスルーは、あなたがそこで作成したアプリを直接基に構築します。
> -   [ビジュアル編集の仕組](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/editing-architecture.html)みを見直しましょう。
> -   コードエディタでフロントエンドプロジェクトを開き、ページビルダーでサイトを開いてください。

## Sitecoreの環境変数を設定しましょう

以前設定したコンテンツレンダリング用の識別子に加えて、フロントエンドアプリがページビルダーと安全に通信するためには、さらに多くの識別子が必要です。Nodeベースのアプリケーションでは、これらは通常 .envファイルに保存されますが、構築する言語やフレームワークに適した方法を使うべきです。

> !重要APIキー、編集シークレット、コンテキストIDなどの秘密は、決してハードコーディングされたりブラウザに露出させたりしてはなりません。代わりに環境変数やシークレットマネージャーに格納するのがベストプラクティスです。

名称

概要

例

SITECORE\_EDITING\_SECRET

Sitecore編集の秘密。  
  
ページビルダーがフロントエンドアプリからデータを取得するために必要です。  
  
SitecoreAI Deploy > **Projects** > プロジェクト> **Authoring environments** > > **Developer settings** > **Environment variables >** コードブロック内の SITECORE\_EDITING\_SECRET 値 Developer settings Environment variables値を見つけてください。  
  
SITECORE\_EDITING\_SECRETが見えない場合は、「**コンテキスト**\>**プレビュー**」を選択してください。

6lrpCn...

また、ライブコンテキストIDではなくプレビューコンテキストIDを使うようにしてください。ビジュアル編集は常に下書き(未公開)コンテンツにアクセスし、プレビューコンテキストIDを使ってアクセスします。

## GraphQLを使ったレイアウト編集データの取得

/api/editing/renderエンドポイントは通常のページレンダリングで使われるクエリとは異なり、[編集GraphQLクエリ](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/editing-architecture.html#the-editing-graphql-query)を使用します。

フレームワークを選択し、フロントエンドアプリでこのクエリを作成する手順に従ってください:

::::タブズ:::tab{title="Astro"}

> !注両方の編集エンドポイントは同じCORSロジックを共有しています。エンドポイントを作成する前に、ALLOWED\_ORIGINSとgetCorsHeadersを共有モジュール(例: src/services/editingCors.js)に抽出し、両方のエンドポイントファイルにインポートすることを検討してください。以下のコード例は、明確さのために完全な実装をインラインに含めています。

-   src/services/sitecoreClient.jsでは、既存のfetchLayoutData関数の後に次の関数を加えます。
    
    // src/services/sitecoreClient.js - add after fetchLayoutData
    
    export async function fetchEditingData(
     site,
     itemId,
     language,
     { version, layoutKind = "final", editMode = "false" } = {},
    ) {
     const endpoint = import.meta.env.SITECORE\_EDGE\_PLATFORM\_URL;
     const contextId = import.meta.env.SITECORE\_EDGE\_CONTEXT\_ID;
    
     const query = \`
     query EditingQuery(
     $siteName: String!
     $itemId: String!
     $language: String!
     $version: String
     $pageSize: Int = 1000
     $after: String
     ) {
     item(path: $itemId, language: $language, version: $version) {
     rendered
     }
     site {
     siteInfo(site: $siteName) {
     dictionary(language: $language, first: $pageSize, after: $after) {
     results { key value }
     pageInfo { endCursor hasNext }
     }
     }
     }
     }
     \`;
    
     const response = await fetch(endpoint, {
     method: "POST",
     headers: {
     "Content-Type": "application/json",
     "x-sitecore-contextid": contextId,
     // sc\_editMode instructs Edge to return draft content and field metadata.
     // sc\_layoutKind selects between final and shared layout variants.
     sc\_editMode: editMode,
     sc\_layoutKind: layoutKind,
     },
     body: JSON.stringify({
     query,
     variables: { siteName: site, itemId, language, version },
     }),
     });
    
     const result = await response.json();
     if (result.errors) {
     throw new Error(JSON.stringify(result.errors));
     }
    
     const item = result?.data?.item;
     if (!item) return null; // item not found
    
     // Parse the rendered layout data (same structure as the layout query).
     const rendered = item.rendered;
     const layoutData =
     typeof rendered === "string" ? JSON.parse(rendered) : rendered;
    
     // Collect the first page of dictionary entries.
     // TODO: For sites with large dictionaries, implement pagination using
     // result.data.site.siteInfo.dictionary.pageInfo.hasNext and endCursor.
     const dictionary = {};
     for (const { key, value } of result?.data?.site?.siteInfo?.dictionary
     ?.results ?? ) {
     dictionarykey = value;
     }
    
     return { layoutData, dictionary };
    }
    

::::::tab{title="Go"}

-   クエリを作成するには、editing.goを作成し、以下のコードを貼り付けます。
    
    // editing.go
    package main
    
    import (
     "bytes"
     "encoding/json"
     "fmt"
     "net/http"
     "os"
    )
    
    const editingQuery = \`
    query EditingQuery(
     $siteName: String!
     $itemId: String!
     $language: String!
     $version: String
     $pageSize: Int = 1000
     $after: String
    ) {
     item(path: $itemId, language: $language, version: $version) {
     rendered
     }
     site {
     siteInfo(site: $siteName) {
     dictionary(language: $language, first: $pageSize, after: $after) {
     results { key value }
     pageInfo { endCursor hasNext }
     }
     }
     }
    }\`
    
    // editingData holds the layout data and site dictionary returned by the
    // editing GraphQL query.
    type editingData struct {
     LayoutData mapstringany
     Dictionary mapstringstring
    }
    
    // fetchEditingData retrieves editing layout data and the site dictionary for
    // the given item using the Sitecore Edge GraphQL editing query.
    //
    // - itemId - the Sitecore item GUID received as sc\_itemid
    // - editMode - "true" to fetch draft content (edit canvas),
    // "false" for preview
    // - layoutKind - "final" or "shared"; defaults to "final"
    func fetchEditingData(site, itemId, language, version, layoutKind, editMode string) (\*editingData, error) {
     endpoint := os.Getenv("SITECORE\_EDGE\_PLATFORM\_URL")
     contextID := os.Getenv("SITECORE\_EDGE\_CONTEXT\_ID")
    
     payload, err := json.Marshal(mapstringany{
     "query": editingQuery,
     "variables": mapstringany{
     "siteName": site,
     "itemId": itemId,
     "language": language,
     "version": version,
     },
     })
     if err != nil {
     return nil, fmt.Errorf("marshal: %w", err)
     }
    
     req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload))
     if err != nil {
     return nil, fmt.Errorf("new request: %w", err)
     }
     req.Header.Set("Content-Type", "application/json")
     req.Header.Set("x-sitecore-contextid", contextID)
    
     // sc\_editMode instructs Edge to return draft content and field metadata.
     // sc\_layoutKind selects between final and shared layout variants.
     req.Header.Set("sc\_editMode", editMode) // "true" or "false"
     req.Header.Set("sc\_layoutKind", layoutKind) // "final" or "shared"
    
     resp, err := http.DefaultClient.Do(req)
     if err != nil {
     return nil, fmt.Errorf("edge editing request: %w", err)
     }
     defer resp.Body.Close()
    
     var gql struct {
     Data struct {
     Item \*struct {
     Rendered json.RawMessage \`json:"rendered"\`
     } \`json:"item"\`
     Site \*struct {
     SiteInfo \*struct {
     Dictionary \*struct {
     Results struct {
     Key string \`json:"key"\`
     Value string \`json:"value"\`
     } \`json:"results"\`
     PageInfo struct {
     EndCursor string \`json:"endCursor"\`
     HasNext bool \`json:"hasNext"\`
     } \`json:"pageInfo"\`
     } \`json:"dictionary"\`
     } \`json:"siteInfo"\`
     } \`json:"site"\`
     } \`json:"data"\`
     Errors struct {
     Message string \`json:"message"\`
     } \`json:"errors"\`
     }
    
     if err := json.NewDecoder(resp.Body).Decode(&gql); err != nil {
     return nil, fmt.Errorf("decode editing response: %w", err)
     }
     if len(gql.Errors) > 0 {
     return nil, fmt.Errorf("graphql editing: %s", gql.Errors0.Message)
     }
     if gql.Data.Item == nil {
     return nil, nil // item not found
     }
    
     // Parse the rendered layout data (same structure as the layout query).
     raw := gql.Data.Item.Rendered
     var layoutData mapstringany
     if json.Unmarshal(raw, &layoutData) != nil {
     var s string
     if json.Unmarshal(raw, &s) != nil {
     return nil, fmt.Errorf("cannot parse rendered field")
     }
     if err := json.Unmarshal(byte(s), &layoutData); err != nil {
     return nil, fmt.Errorf("cannot parse rendered string: %w", err)
     }
     }
    
     // Collect the first page of dictionary entries.
     // TODO: For sites with large dictionaries, implement pagination using
     // gql.Data.Site.SiteInfo.Dictionary.PageInfo.HasNext and EndCursor.
     dictionary := make(mapstringstring)
     if gql.Data.Site != nil &&
     gql.Data.Site.SiteInfo != nil &&
     gql.Data.Site.SiteInfo.Dictionary != nil {
     for \_, entry := range gql.Data.Site.SiteInfo.Dictionary.Results {
     dictionaryentry.Key = entry.Value
     }
     }
    
     return &editingData{LayoutData: layoutData, Dictionary: dictionary}, nil
    }
    

:::::::

これらのスクリプトは [辞書](https://doc.sitecore.com/sai/ja/developers/sitecoreai/introduction-to-framework-agnostic-sitecore-development/visual-editing/editing-architecture.html#the-editing-graphql-query) を取得しますが、コンポーネントに翻訳可能なUI文字列が含まれていないため、辞書は使用しません。

## HTMLにメタデータラッパーを追加

次に、ページビルダーが必要な <code> メタデータブロックを追加し、アプリがレンダリングするHTMLのプレースホルダー、コンポーネント、個々のフィールドを識別します。

メタデータラッパーを追加するには:

::::タブズ:::tab{title="Astro"}

-   src/services/editingRenderer.jsを作成し、以下のコードを貼り付けます:
    
    // src/services/editingRenderer.js
    
    const DEFAULT\_UID = "00000000-0000-0000-0000-000000000000";
    
    function escHtml(str) {
     return String(str ?? "")
     .replace(/&/g, "&amp;")
     .replace(/</g, "&lt;")
     .replace(/>/g, "&gt;")
     .replace(/"/g, "&quot;");
    }
    
    // --- Metadata code blocks ---
    
    function metaOpen(chrometype, id) {
     return \`<code type="text/sitecore" chrometype="${chrometype}" class="scpm" kind="open" id="${escHtml(id)}"></code>\`;
    }
    
    function metaClose(chrometype) {
     return \`<code type="text/sitecore" chrometype="${chrometype}" class="scpm" kind="close"></code>\`;
    }
    
    // metaField wraps rendered field HTML with field-chrome metadata code blocks.
    // The Page builder reads these to identify which fields a content author can
    // edit inline.
    //
    // fieldData must be the full field object (the object that contains both
    // "value" and, in edit mode, "metadata"). If no "metadata" key is present,
    // the rendered HTML is returned unchanged.
    export function metaField(fieldData, rendered) {
     const metadata = fieldData?.metadata;
     if (!metadata) return rendered;
     return (
     \`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">${JSON.stringify(metadata)}</code>\` +
     rendered +
     \`<code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>\`
     );
    }
    
    // --- Editing-aware component renderers ---
    
    function renderImageEditing(fields) {
     const f = fields?.Image;
     const img = f?.value ?? f?.jsonValue?.value;
     if (!img?.src) return metaField(f, "");
     const imgTag = \`<img src="${escHtml(img.src)}" alt="${escHtml(img.alt ?? "")}" width="${escHtml(img.width ?? "")}" height="${escHtml(img.height ?? "")}" />\`;
    
     // General Link field, with jsonValue as a fallback for direct item queries.
     const linkField = fields?.TargetUrl;
     const link = linkField?.value ?? linkField?.jsonValue?.value;
     const content = link?.href
     ? \`<a href="${escHtml(link.href)}"${link.target ? \` target="${escHtml(link.target)}"\` : ""}>${imgTag}</a>\`
     : imgTag;
    
     // Plain text caption field.
     const caption = fields?.ImageCaption?.value;
     const html = caption
     ? \`<figure>${content}<figcaption>${escHtml(caption)}</figcaption></figure>\`
     : \`<figure>${content}</figure>\`;
     return metaField(f, html);
    }
    
    function renderPromoEditing(fields) {
     for (const name of "PromoText", "PromoText2", "PromoText3") {
     const f = fields?.name;
     if (f?.value) return metaField(f, f.value);
     }
     return "";
    }
    
    function renderRichTextEditing(fields) {
     const f = fields?.Text;
     if (!f?.value) return metaField(f, "");
     return metaField(f, \`<div>${f.value}</div>\`);
    }
    
    function renderTitleEditing(fields, params) {
     const f = fields?.heading;
     if (!f?.value) return metaField(f, "");
     const classAttr = params?.cssClass
     ? \` class="${escHtml(params.cssClass)}"\`
     : "";
     return metaField(f, \`<h1${classAttr}>${escHtml(f.value)}</h1>\`);
    }
    
    // editingPassthroughComponents is the set of structural wrapper components
    // that have no fields of their own and only forward nested placeholders.
    // Components registered here in componentMap use a passthrough renderer
    // that calls Placeholder. In editing mode that would bypass
    // renderPlaceholderEditing, stripping all rendering and field chromes from
    // every component nested inside the wrapper.
    //
    // Add any new passthrough/container components here alongside adding them
    // to componentMap.
    export const editingPassthroughComponents = new Set(
     "Container",
     "ContainerFullBleed",
     "PartialDesignDynamicPlaceholder",
    );
    
    // editingComponentMap maps Sitecore component names to editing-aware renderer
    // functions. Each renderer wraps its field output with metaField so that the
    // Page builder can activate inline field editing on click.
    //
    // ColumnSplitter is handled separately in renderComponentEditing because it
    // renders nested placeholders and needs the component's own uid.
    const editingComponentMap = {
     Image: renderImageEditing,
     Promo: renderPromoEditing,
     RichText: renderRichTextEditing,
     Title: renderTitleEditing,
    };
    
    // renderColumnSplitterEditing mirrors ColumnSplitter.astro but calls
    // renderPlaceholderEditing so that nested column placeholders have metadata
    // code blocks and accept component drops in the Page builder canvas.
    function renderColumnSplitterEditing(fields, params, placeholders, uid) {
     const enabled = params?.EnabledPlaceholders
     ? params.EnabledPlaceholders.split(",").map((s) => s.trim())
     : null;
    
     const cols = Object.entries(placeholders)
     .map((key, items) => {
     const m = key.match(/^column-(\\d+)-/);
     if (!m) return null;
     const index = Number(m1);
     if (enabled && !enabled.includes(String(index))) return null;
     return { key, items, index };
     })
     .filter(Boolean)
     .sort((a, b) => a.index - b.index);
    
     let html = \`<div data-component="ColumnSplitter" class="${escHtml(params?.GridParameters ?? "")}">\`;
     for (const col of cols) {
     const colClass = params?.\`ColumnWidth${col.index}\` ?? "";
     html += \`<div data-column="${col.index}" class="${escHtml(colClass)}">\`;
     html += renderPlaceholderEditing(col.key, col.items, uid);
     html += "</div>";
     }
     html += "</div>";
     return html;
    }
    
    // renderPlaceholderEditing renders a placeholder wrapped in metadata code
    // blocks. parentUID is the uid of the route or component that owns this
    // placeholder.
    export function renderPlaceholderEditing(
     name,
     items,
     parentUID = DEFAULT\_UID,
    ) {
     const phId = \`${name}\_${parentUID}\`;
     return (
     metaOpen("placeholder", phId) +
     \`<div data-placeholder="${escHtml(name)}">\` +
     (items ?? ).map((item) => renderComponentEditing(item)).join("") +
     "</div>" +
     metaClose("placeholder")
     );
    }
    
    // renderComponentEditing renders a single component wrapped in metadata code
    // blocks. It delegates field rendering to editingComponentMap (which adds
    // field-chrome wrapping), with a fallback to renderMissingComponentEditing
    // for any components not listed in editingComponentMap.
    export function renderComponentEditing(comp) {
     const componentName = comp?.componentName ?? "";
     const uid = comp?.uid || DEFAULT\_UID;
     const fields = comp?.fields ?? {};
     const params = comp?.params ?? {};
     const rawPH = comp?.placeholders ?? {};
     const placeholders = Object.fromEntries(
     Object.entries(rawPH).map((k, v) => k, Array.isArray(v) ? v : ),
     );
    
     let inner;
     if (componentName === "ColumnSplitter") {
     // ColumnSplitter renders nested placeholders; it needs the component uid
     // to build correct placeholder IDs for drag-and-drop in the canvas.
     inner = renderColumnSplitterEditing(fields, params, placeholders, uid);
     } else if (editingPassthroughComponents.has(componentName)) {
     // Passthrough components have no fields of their own - they only forward
     // nested placeholders. Delegating to the normal component map would call
     // Placeholder instead of renderPlaceholderEditing, stripping all chromes
     // from every component nested inside the wrapper.
     inner = Object.entries(placeholders)
     .map((key, items) => renderPlaceholderEditing(key, items, uid))
     .join("");
     } else if (editingComponentMapcomponentName) {
     inner = editingComponentMapcomponentName(fields, params, placeholders);
     } else {
     inner = renderMissingComponentEditing(componentName, placeholders, uid);
     }
    
     return metaOpen("rendering", uid) + inner + metaClose("rendering");
    }
    
    // renderMissingComponentEditing renders unmapped components in editing mode,
    // preserving their nested placeholders with metadata wrappers.
    export function renderMissingComponentEditing(
     componentName,
     placeholders,
     uid = DEFAULT\_UID,
    ) {
     const inner = Object.entries(placeholders)
     .map((key, items) => renderPlaceholderEditing(key, items, uid))
     .join("");
     return \`<div data-missing-component="${escHtml(componentName)}">${inner}</div>\`;
    }
    
    // buildEditingHeadScripts extracts the three categories of editing scripts
    // from sitecore.context and returns them as a combined block of <script>
    // tags ready to embed in <head>:
    //
    // 1. clientScripts - JS bundle URLs that the Page builder provides. Loading
    // these activates the editing UI overlays inside the canvas iframe.
    // 2. hrz-canvas-state - JSON identifying the item, site, language, and mode
    // for the canvas. The Page builder reads this on load.
    // 3. hrz-canvas-verification-token - token that verifies the communication
    // channel between the iframe and the Page builder.
    export function buildEditingHeadScripts(ctx) {
     let html = "";
    
     // 1. Emit a <script src="..."> for each URL in clientScripts.
     for (const url of ctx?.clientScripts ?? ) {
     if (typeof url === "string") {
     html += \`<script type="text/javascript" src="${escHtml(url)}"></script>\`;
     }
     }
    
     const clientData = ctx?.clientData ?? {};
    
     // 2. Emit canvas state and verification token from clientData.
     if (clientData"hrz-canvas-state" !== undefined) {
     html += \`<script id="hrz-canvas-state" type="application/json">${JSON.stringify(clientData"hrz-canvas-state")}</script>\`;
     }
    
     if (typeof clientData"hrz-canvas-verification-token" === "string") {
     html += \`<script id="hrz-canvas-verification-token" type="application/json">${escHtml(clientData"hrz-canvas-verification-token")}</script>\`;
     }
    
     return html;
    }
    
    // renderLayoutEditing returns a complete HTML editing page string.
    //
    // headHtml is a string of <link> and <script> tags copied from your Astro
    // layout component's <head>. Passing these tags here ensures the editing
    // canvas loads with the same styles and scripts as the published site.
    // Astro API routes cannot call .astro components directly, so these tags
    // must be provided explicitly. Leave headHtml empty if your layout has no
    // external assets.
    export function renderLayoutEditing(layoutData, headHtml = "") {
     const sitecore = layoutData?.sitecore ?? {};
     const route = sitecore?.route;
     const ctx = sitecore?.context ?? {};
     const editingHeadScripts = buildEditingHeadScripts(ctx);
    
     if (!route) {
     return \`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Page not found</title>${headHtml}${editingHeadScripts}</head><body><main><p>Layout data is missing for this route.</p></main><script id="jss-hrz-editing" type="application/json">{}</script></body></html>\`;
     }
    
     const routeUID = route.uid ?? DEFAULT\_UID;
     const phs = route.placeholders ?? {};
     const title = escHtml(route.displayName || route.name || "");
    
     const header = renderPlaceholderEditing(
     "headless-header",
     phs"headless-header" ?? ,
     routeUID,
     );
     const main = renderPlaceholderEditing(
     "headless-main",
     phs"headless-main" ?? ,
     routeUID,
     );
     const footer = renderPlaceholderEditing(
     "headless-footer",
     phs"headless-footer" ?? ,
     routeUID,
     );
    
     return \`<!DOCTYPE html>
    
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>${title}</title>
     ${headHtml}
     ${editingHeadScripts}
    </head>
    <body>
     <header>${header}</header>
     <main>${main}</main>
     <footer>${footer}</footer>
     <script id="jss-hrz-editing" type="application/json">{}</script>
    </body>
    </html>\`;
    }
    
-   src/components/componentMap.jsでは、コンテンツレンダリングのウォークスルーに従って登録した他のパススルーコンポーネントに加えて、ContainerFullBleedを追加してください:
    
    // src/components/componentMap.js
    import PartialDesignDynamicPlaceholder from './PartialDesignDynamicPlaceholder.astro';
    
    export const componentMap = {
     // ...existing entries...
     'ContainerFullBleed': PartialDesignDynamicPlaceholder,
    };
    

::::::tab{title="Go"}

1.  editing\_rendering.goを作成し、以下のコードを貼り付けます:
    
    // editing\_rendering.go
    package main
    
    import (
     "encoding/json"
     "fmt"
     "html/template"
     "sort"
     "strconv"
     "strings"
    )
    
    // defaultPlaceholderUID is used when a route or component has no uid.
    const defaultPlaceholderUID = "00000000-0000-0000-0000-000000000000"
    
    // metaOpen returns an opening metadata code block for a placeholder or rendering.
    func metaOpen(chrometype, id string) template.HTML {
     return template.HTML(fmt.Sprintf(
     \`<code type="text/sitecore" chrometype="%s" class="scpm" kind="open" id="%s"></code>\`,
     template.HTMLEscapeString(chrometype),
     template.HTMLEscapeString(id),
     ))
    }
    
    // metaClose returns a closing metadata code block.
    func metaClose(chrometype string) template.HTML {
     return template.HTML(fmt.Sprintf(
     \`<code type="text/sitecore" chrometype="%s" class="scpm" kind="close"></code>\`,
     template.HTMLEscapeString(chrometype),
     ))
    }
    
    // metaField wraps rendered field HTML with field-chrome metadata code blocks.
    // The Page builder reads these to identify which fields a content author can
    // edit inline.
    //
    // fieldData must be the full field object (the mapstringany that contains
    // both "value" and, in edit mode, "metadata"). If no "metadata" key is
    // present the rendered HTML is returned unchanged.
    func metaField(fieldData mapstringany, rendered template.HTML) template.HTML {
     if fieldData == nil {
     return rendered
     }
     metadata, ok := fieldData"metadata".(mapstringany)
     if !ok || metadata == nil {
     return rendered
     }
     metaJSON, \_ := json.Marshal(metadata)
     return template.HTML(fmt.Sprintf(
     \`<code type="text/sitecore" chrometype="field" class="scpm" kind="open">%s</code>%s<code type="text/sitecore" chrometype="field" class="scpm" kind="close"></code>\`,
     string(metaJSON),
     string(rendered),
     ))
    }
    
    // editingPassthroughComponents is the set of structural wrapper components
    // that have no fields of their own and only forward nested placeholders.
    // Their renderers in componentRegistry call renderPlaceholder for nested slots.
    // In editing mode this must be overridden to call renderPlaceholderEditing
    // instead, so that nested components receive rendering chrome and are
    // selectable in the canvas. Add every such wrapper here alongside registering
    // it in componentRegistry.
    var editingPassthroughComponents = mapstringbool{
     "Container": true,
     "ContainerFullBleed": true,
     "PartialDesignDynamicPlaceholder": true,
    }
    
    // renderColumnSplitterEditing mirrors renderColumnSplitter in components.go
    // but calls renderPlaceholderEditing so that nested column placeholders have
    // metadata code blocks and accept component drops in the Page builder canvas.
    func renderColumnSplitterEditing(fields, params mapstringany, placeholders mapstringany, uid string) template.HTML {
     var enabled string
     if ep := ms(params, "EnabledPlaceholders"); ep != "" {
     for \_, p := range strings.Split(ep, ",") {
     enabled = append(enabled, strings.TrimSpace(p))
     }
     }
    
     type col struct {
     index int
     key string
     items any
     }
     var cols col
     for key, items := range placeholders {
     if !strings.HasPrefix(key, "column-") {
     continue
     }
     rest := keylen("column-"):
     dash := strings.Index(rest, "-")
     if dash < 0 {
     continue
     }
     idx, err := strconv.Atoi(rest:dash)
     if err != nil {
     continue
     }
     idxStr := strconv.Itoa(idx)
     if len(enabled) > 0 {
     found := false
     for \_, e := range enabled {
     if e == idxStr {
     found = true
     break
     }
     }
     if !found {
     continue
     }
     }
     cols = append(cols, col{index: idx, key: key, items: items})
     }
     sort.Slice(cols, func(i, j int) bool { return colsi.index < colsj.index })
    
     var sb strings.Builder
     sb.WriteString(fmt.Sprintf(\`<div data-component="ColumnSplitter" class="%s">\`, esc(ms(params, "GridParameters"))))
     for \_, c := range cols {
     colClass := ms(params, fmt.Sprintf("ColumnWidth%d", c.index))
     sb.WriteString(fmt.Sprintf(\`<div data-column="%d" class="%s">\`, c.index, esc(colClass)))
     sb.WriteString(string(renderPlaceholderEditing(c.key, c.items, uid)))
     sb.WriteString("</div>")
     }
     sb.WriteString("</div>")
     return template.HTML(sb.String())
    }
    
    // renderPlaceholderEditing renders a placeholder wrapped in metadata code
    // blocks. parentUID is the uid of the route or component that owns this
    // placeholder.
    func renderPlaceholderEditing(name string, items any, parentUID string) template.HTML {
     if parentUID == "" {
     parentUID = defaultPlaceholderUID
     }
     phID := name + "\_" + parentUID
    
     var sb strings.Builder
     sb.WriteString(string(metaOpen("placeholder", phID)))
     sb.WriteString(fmt.Sprintf(\`<div data-placeholder="%s">\`, template.HTMLEscapeString(name)))
     for \_, item := range items {
     comp, ok := item.(mapstringany)
     if !ok {
     continue
     }
     sb.WriteString(string(renderComponentEditing(comp)))
     }
     sb.WriteString("</div>")
     sb.WriteString(string(metaClose("placeholder")))
     return template.HTML(sb.String())
    }
    
    // renderComponentEditing renders a single component wrapped in metadata code
    // blocks. It delegates field rendering to componentRegistry, which calls
    // metaField on each field value. Because metaField is a no-op when the
    // "metadata" key is absent, the same renderers work in both normal and
    // editing mode.
    func renderComponentEditing(comp mapstringany) template.HTML {
     componentName, \_ := comp"componentName".(string)
     uid, \_ := comp"uid".(string)
     fields, \_ := comp"fields".(mapstringany)
     params, \_ := comp"params".(mapstringany)
     rawPH, \_ := comp"placeholders".(mapstringany)
    
     if fields == nil {
     fields = mapstringany{}
     }
     if params == nil {
     params = mapstringany{}
     }
     if uid == "" {
     uid = defaultPlaceholderUID
     }
    
     // Coerce nested placeholder arrays.
     placeholders := make(mapstringany, len(rawPH))
     for k, v := range rawPH {
     if arr, ok := v.(any); ok {
     placeholdersk = arr
     }
     }
    
     var inner template.HTML
     switch {
     case componentName == "ColumnSplitter":
     // ColumnSplitter renders nested placeholders; it needs the component uid
     // to build correct placeholder IDs for drag-and-drop in the canvas.
     inner = renderColumnSplitterEditing(fields, params, placeholders, uid)
     case editingPassthroughComponentscomponentName:
     // Passthrough components call renderPlaceholder for nested slots, which
     // would strip rendering chrome from nested components. Override that
     // here to call renderPlaceholderEditing so nested components remain
     // selectable in the canvas.
     var sb strings.Builder
     for key, items := range placeholders {
     sb.WriteString(string(renderPlaceholderEditing(key, items, uid)))
     }
     inner = template.HTML(sb.String())
     case componentRegistrycomponentName != nil:
     inner = componentRegistrycomponentName(fields, params, placeholders)
     default:
     inner = renderMissingComponentEditing(componentName, placeholders, uid)
     }
    
     var sb strings.Builder
     sb.WriteString(string(metaOpen("rendering", uid)))
     sb.WriteString(string(inner))
     sb.WriteString(string(metaClose("rendering")))
     return template.HTML(sb.String())
    }
    
    // renderMissingComponentEditing renders unmapped components in editing mode,
    // preserving their nested placeholders with metadata wrappers.
    func renderMissingComponentEditing(componentName string, placeholders mapstringany, uid string) template.HTML {
     var sb strings.Builder
     for key, items := range placeholders {
     sb.WriteString(string(renderPlaceholderEditing(key, items, uid)))
     }
     return template.HTML(fmt.Sprintf(
     \`<div data-missing-component="%s">%s</div>\`,
     template.HTMLEscapeString(componentName), sb.String(),
     ))
    }
    
2.  コンポーネントレンダラーをcomponents.goで更新し、各フィールド値でmetaFieldを呼び出します。 metaFieldがmetadataがないときには変更せずにHTMLを返すため、同じレンダラーが通常モードと編集モードの両方で動作します。
    
    // Updated renderers in components.go
    
    func renderTitle(fields, params mapstringany, \_ mapstringany) template.HTML {
     f, \_ := fields"heading".(mapstringany)
     heading, \_ := f"value".(string)
     if heading == "" {
     return metaField(f, "")
     }
     classAttr := ""
     if cssClass := ms(params, "cssClass"); cssClass != "" {
     classAttr = fmt.Sprintf(\` class="%s"\`, esc(cssClass))
     }
     return metaField(f, template.HTML(fmt.Sprintf("<h1%s>%s</h1>", classAttr, esc(heading))))
    }
    
    func renderRichText(fields, params mapstringany, \_ mapstringany) template.HTML {
     f, \_ := fields"Text".(mapstringany)
     html, \_ := f"value".(string)
     if html == "" {
     return metaField(f, "")
     }
     return metaField(f, template.HTML("<div>"+html+"</div>"))
    }
    
    func renderImage(fields, params mapstringany, \_ mapstringany) template.HTML {
     f, \_ := fields"Image".(mapstringany)
     img := imgValue(fields, "Image")
     if img == nil || ms(img, "src") == "" {
     return metaField(f, "")
     }
     imgTag := fmt.Sprintf(
     \`<img src="%s" alt="%s" width="%s" height="%s" />\`,
     esc(ms(img, "src")), esc(ms(img, "alt")),
     esc(ms(img, "width")), esc(ms(img, "height")),
     )
     lnk := lnkValue(fields, "TargetUrl")
     var content string
     if lnk != nil && ms(lnk, "href") != "" {
     targetAttr := ""
     if target := ms(lnk, "target"); target != "" {
     targetAttr = fmt.Sprintf(\` target="%s"\`, esc(target))
     }
     content = fmt.Sprintf(\`<a href="%s"%s>%s</a>\`, esc(ms(lnk, "href")), targetAttr, imgTag)
     } else {
     content = imgTag
     }
     caption := strField(fields, "ImageCaption")
     if caption != "" {
     return metaField(f, template.HTML(fmt.Sprintf("<figure>%s<figcaption>%s</figcaption></figure>", content, esc(caption))))
     }
     return metaField(f, template.HTML(fmt.Sprintf("<figure>%s</figure>", content)))
    }
    
    func renderPromo(fields, params mapstringany, \_ mapstringany) template.HTML {
     for \_, name := range string{"PromoText", "PromoText2", "PromoText3"} {
     f, \_ := fieldsname.(mapstringany)
     if html, \_ := f"value".(string); html != "" {
     return metaField(f, template.HTML(html))
     }
     }
     return ""
    }
    
    また、components.goのcomponentRegistry init()ブロックに同じパススルーレンダラーを使ってContainerFullBleedを追加してくださいPartialDesignDynamicPlaceholder:
    
    // In the init() block in components.go, add:
    "ContainerFullBleed": renderPartialDesignDynamicPlaceholder,
    

:::::::

## 作成 /api/editing/config

/api/editing/configエンドポイントは、アプリがどの編集モードを使っていて、どのコンポーネント名がサポートされているかをページビルダーに伝えます。ページビルダーは編集ホストに接続するたびにこのエンドポイントを呼び出します。例えば、コンテンツ作成者がエディターを開くときや、ローカルとホストの編集ホストを切り替えるときなどです。このエンドポイントが返すべきレスポンスペイロードについては[、設定レスポン](./editing-architecture.md#the-config-response)スを参照してください。

/api/editing/configエンドポイントを作成するには:

::::タブズ:::tab{title="Astro"}

-   src/pages/api/editing/config.jsを作成し、以下のコードを貼り付けます:
    
    // src/pages/api/editing/config.js
    
    export const prerender = false;
    
    import { componentMap } from "../../../components/componentMap.js";
    
    // editingAllowedOrigins contains the domains from which the Page builder
    // makes requests.
    const ALLOWED\_ORIGINS = 
     "https://pages.sitecorecloud.io",
     "https://app.sitecorecloud.io",
    ;
    
    function getCorsHeaders(origin) {
     if (ALLOWED\_ORIGINS.includes(origin)) {
     return {
     "Access-Control-Allow-Origin": origin,
     "Access-Control-Allow-Headers": "Authorization, Content-Type",
     "Access-Control-Allow-Methods": "GET, OPTIONS",
     // Required when an HTTPS page (pages.sitecorecloud.io) accesses
     // a private-network address (localhost). See the Private Network
     // Access spec: https://developer.chrome.com/blog/private-network-access-preflight
     "Access-Control-Allow-Private-Network": "true",
     };
     }
     return {};
    }
    
    // Handle CORS preflight.
    export function OPTIONS({ request }) {
     const origin = request.headers.get("Origin") ?? "";
     return new Response(null, { status: 204, headers: getCorsHeaders(origin) });
    }
    
    // Handle GET /api/editing/config.
    // The Page builder calls this endpoint to confirm metadata editing mode support.
    export function GET({ request }) {
     const origin = request.headers.get("Origin") ?? "";
     const url = new URL(request.url);
     const corsHeaders = getCorsHeaders(origin);
    
     const secret = import.meta.env.SITECORE\_EDITING\_SECRET;
     if (!secret || url.searchParams.get("secret") !== secret) {
     return new Response(
     JSON.stringify({ message: "Missing or invalid editing secret" }),
     {
     status: 401,
     headers: { "Content-Type": "application/json", ...corsHeaders },
     },
     );
     }
    
     // Build the list of registered component names from the component map.
     const components = Object.keys(componentMap);
    
     // Return the config JSON.
     // editMode must be "metadata" to use metadata editing mode.
     // components is the list of Sitecore component names your app can render.
     // packages is an optional map of package versions; leave empty for custom apps.
     return new Response(
     JSON.stringify({ editMode: "metadata", components, packages: {} }),
     {
     status: 200,
     headers: { "Content-Type": "application/json", ...corsHeaders },
     },
     );
    }
    

::::::tab{title="Go"}

-   api\_editing\_config.goを作成し、以下のコードを貼り付けます:
    
    // api\_editing\_config.go
    package main
    
    import (
     "encoding/json"
     "net/http"
     "os"
    )
    
    // editingAllowedOrigins contains the domains from which the Page builder
    // makes requests.
    var editingAllowedOrigins = string{
     "https://pages.sitecorecloud.io",
     "https://app.sitecorecloud.io",
    }
    
    // setCORSEditingHeaders sets the CORS headers required by the editing endpoints.
    func setCORSEditingHeaders(w http.ResponseWriter, r \*http.Request) {
     origin := r.Header.Get("Origin")
     for \_, allowed := range editingAllowedOrigins {
     if origin == allowed {
     w.Header().Set("Access-Control-Allow-Origin", origin)
     w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
     w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
     // Required when an HTTPS page (pages.sitecorecloud.io) accesses
     // a private-network address (localhost). See the Private Network
     // Access spec: https://developer.chrome.com/blog/private-network-access-preflight
     w.Header().Set("Access-Control-Allow-Private-Network", "true")
     return
     }
     }
    }
    
    // validateEditingSecret returns true when the request's secret query
    // parameter matches SITECORE\_EDITING\_SECRET.
    func validateEditingSecret(r \*http.Request) bool {
     expected := os.Getenv("SITECORE\_EDITING\_SECRET")
     if expected == "" {
     return false
     }
     return r.URL.Query().Get("secret") == expected
    }
    
    // handleEditingConfig handles GET /api/editing/config.
    // The Page builder calls this endpoint to confirm metadata editing mode support.
    func handleEditingConfig(w http.ResponseWriter, r \*http.Request) {
     setCORSEditingHeaders(w, r)
    
     // Handle CORS preflight.
     if r.Method == http.MethodOptions {
     w.WriteHeader(http.StatusNoContent)
     return
     }
    
     if r.Method != http.MethodGet {
     http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
     return
     }
    
     if !validateEditingSecret(r) {
     w.Header().Set("Content-Type", "application/json")
     http.Error(w, \`{"message":"Missing or invalid editing secret"}\`, http.StatusUnauthorized)
     return
     }
    
     // Build the list of registered component names from the component registry.
     components := make(string, 0, len(componentRegistry))
     for name := range componentRegistry {
     components = append(components, name)
     }
    
     // Return the config JSON.
     // editMode must be "metadata" to use metadata editing mode.
     // components is the list of Sitecore component names your app can render.
     // packages is an optional map of package versions; leave empty for custom apps.
     w.Header().Set("Content-Type", "application/json")
     json.NewEncoder(w).Encode(mapstringany{
     "editMode": "metadata",
     "components": components,
     "packages": mapstringstring{},
     })
    }
    

:::::::

## 作成 /api/editing/render

/api/editing/renderエンドポイントは編集統合の中核です。コンテンツ作成者がエディターでページを見ると、ページビルダーはiframeのsrcをこのエンドポイントに設定して編集キャンバスを読み込みます。その後、ブラウザはURL内の編集パラメータを含んでアプリにGETリクエストを送ります。アプリはSitecoreから編集レイアウトデータを取得し、メタデータコードブロックを埋め込んだまま完全なHTMLページをページビルダーに返します。ページビルダーはこのHTMLページをiframe内に読み込みます。

> !重要アプリがX-Frame-Options: SAMEORIGINやContent-Security-Policyヘッダーにframe-ancestors 'self'を設定していると、iframeはブロックされます。編集時のレンダリングエンドポイント応答用のヘッダーを削除するか、https://pages.sitecorecloud.ioを許容されるフレーミング原点として追加してください。

/api/editing/renderエンドポイントを作成するには:

::::タブズ:::tab{title="Astro"}

-   src/pages/api/editing/render.jsを作成し、以下のコードを貼り付けます:
    
    // src/pages/api/editing/render.js
    
    export const prerender = false;
    
    import { fetchEditingData } from "../../../services/sitecoreClient.js";
    import { renderLayoutEditing } from "../../../services/editingRenderer.js";
    
    const ALLOWED\_ORIGINS = 
     "https://pages.sitecorecloud.io",
     "https://app.sitecorecloud.io",
    ;
    
    function getCorsHeaders(origin) {
     if (ALLOWED\_ORIGINS.includes(origin)) {
     return {
     "Access-Control-Allow-Origin": origin,
     "Access-Control-Allow-Headers": "Authorization, Content-Type",
     "Access-Control-Allow-Methods": "GET, OPTIONS",
     // Required when an HTTPS page (pages.sitecorecloud.io) accesses
     // a private-network address (localhost).
     "Access-Control-Allow-Private-Network": "true",
     };
     }
     return {};
    }
    
    // Handle CORS preflight.
    export function OPTIONS({ request }) {
     const origin = request.headers.get("Origin") ?? "";
     return new Response(null, { status: 204, headers: getCorsHeaders(origin) });
    }
    
    // Handle GET /api/editing/render.
    // The Page builder calls this endpoint for every page view inside the editor.
    export async function GET({ request }) {
     const url = new URL(request.url);
     const q = url.searchParams;
     const secret = import.meta.env.SITECORE\_EDITING\_SECRET;
    
     if (!secret || q.get("secret") !== secret) {
     return new Response(
     JSON.stringify({ message: "Missing or invalid editing secret" }),
     { status: 401, headers: { "Content-Type": "application/json" } },
     );
     }
    
     const site = q.get("sc\_site") ?? "";
     const itemId = q.get("sc\_itemid") ?? "";
     const lang =
     q.get("sc\_lang") || import.meta.env.SITECORE\_SITE\_LANGUAGE\_CODE || "en";
     const mode = q.get("mode") ?? "preview";
     const version = q.get("sc\_version") ?? undefined;
     const layoutKind = q.get("sc\_layoutKind") || "final";
    
     // sc\_editMode: "true" returns draft content and embeds editing scripts in
     // sitecore.context; "false" returns published content only.
     const editMode = mode === "edit" ? "true" : "false";
    
     let data;
     try {
     data = await fetchEditingData(site, itemId, lang, {
     version,
     layoutKind,
     editMode,
     });
     } catch (err) {
     return new Response(String(err), { status: 500 });
     }
    
     if (!data) {
     return new Response("Item not found", { status: 404 });
     }
    
     // Pass the <link> and <script> tags your Astro layout component includes
     // in <head> so that the editing canvas loads with the correct styles and
     // scripts. Astro API routes cannot call .astro components directly, so
     // these tags must be provided here explicitly. Copy them from your layout
     // component (for example src/layouts/Layout.astro) and keep them in sync
     // when your layout changes. Leave headHtml empty if your layout has no
     // external assets.
     const headHtml = "";
     const html = renderLayoutEditing(data.layoutData, headHtml);
     const origin = request.headers.get("Origin") ?? "";
    
     return new Response(html, {
     status: 200,
     headers: {
     "Content-Type": "text/html; charset=utf-8",
     ...getCorsHeaders(origin),
     },
     });
    }
    

::::::tab{title="Go"}

編集キャンバスは公開サイトと同じスタイルとスクリプトでレンダリングされなければなりません。Goでは、編集レンダラーがlayout.goの既存のpageTmplページテンプレートを再利用することでこれを達成しており、別のHTMLドキュメント構造を定義するのではありません。通常のレンダリング用にCSSやJavaScriptを追加したものも、編集キャンバスに自動的に表示されますpageTmpl。

1.  編集サポート付きのpageVarsとpageTmplを拡張するための更新layout.go:
    
    // layout.go
    package main
    
    import (
     "html/template"
     "io"
    )
    
    var pageTmpl = template.Must(template.New("page").Parse(\`<!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{.Title}}</title>
     {{.EditingHeadScripts}}
    </head>
    <body>
     {{- if .HasRoute}}
     <header>{{.Header}}</header>
     <main>{{.Main}}</main>
     <footer>{{.Footer}}</footer>
     {{- else}}
     <main><p>Layout data is missing for this route.</p></main>
     {{- end}}
     {{- if .EditingHeadScripts}}
     <script id="jss-hrz-editing" type="application/json">{}</script>
     {{- end}}
    </body>
    </html>\`))
    
    type pageVars struct {
     Title string
     HasRoute bool
     Header template.HTML
     Main template.HTML
     Footer template.HTML
     EditingHeadScripts template.HTML
    }
    
    // renderLayout writes a complete HTML page to w using the provided layout data.
    // Fields of type template.HTML are rendered as-is by html/template (no escaping),
    // which is intentional. Our component renderers produce safe, trusted HTML.
    func renderLayout(w io.Writer, layoutData mapstringany) error {
     if layoutData == nil {
     return pageTmpl.Execute(w, pageVars{Title: "Page not found"})
     }
     sitecore, \_ := layoutData"sitecore".(mapstringany)
     route, \_ := sitecore"route".(mapstringany)
     if route == nil {
     return pageTmpl.Execute(w, pageVars{Title: "Page not found"})
     }
    
     phs, \_ := route"placeholders".(mapstringany)
     slot := func(key string) any {
     arr, \_ := phskey.(any)
     return arr
     }
    
     title, \_ := route"displayName".(string)
     if title == "" {
     title, \_ = route"name".(string)
     }
     return pageTmpl.Execute(w, pageVars{
     Title: title,
     HasRoute: true,
     Header: renderPlaceholder("headless-header", slot("headless-header")),
     Main: renderPlaceholder("headless-main", slot("headless-main")),
     Footer: renderPlaceholder("headless-footer", slot("headless-footer")),
     })
    }
    
    layout.goに加えられた2つの追加要素は以下の通りです:
    
    -   EditingHeadScripts template.HTMLpageVarsでは、通常のレンダリング用に空で、編集レンダラーで埋められます。
    -   {{.EditingHeadScripts}}<head>および<body>の末尾にある条件付きjss-hrz-editing標識。どちらもフィールドが空のときはノーオペレーションなので、通常のページレンダリングには影響がありません。
2.  api\_editing\_render.goを作成し、以下のコードを貼り付けます:
    
    // api\_editing\_render.go
    package main
    
    import (
     "encoding/json"
     "fmt"
     "html/template"
     "io"
     "net/http"
     "os"
     "strings"
    )
    
    // buildEditingHeadScripts extracts the three categories of editing scripts
    // from sitecore.context and returns them as a combined block of <script>
    // tags ready to embed in <head>:
    //
    // 1. clientScripts - JS bundle URLs that the Page builder provides. Loading
    // these activates the editing UI overlays inside the canvas iframe.
    // 2. hrz-canvas-state - JSON identifying the item, site, language, and mode
    // for the canvas. The Page builder reads this on load.
    // 3. hrz-canvas-verification-token - token that verifies the communication
    // channel between the iframe and the Page builder.
    func buildEditingHeadScripts(ctx mapstringany) template.HTML {
     var sb strings.Builder
    
     // 1. Emit a <script src="..."> for each URL in clientScripts.
     if rawScripts, ok := ctx"clientScripts".(any); ok {
     for \_, s := range rawScripts {
     if url, ok := s.(string); ok {
     sb.WriteString(fmt.Sprintf(
     \`<script type="text/javascript" src="%s"></script>\`,
     template.HTMLEscapeString(url),
     ))
     }
     }
     }
    
     // 2. Emit canvas state and verification token from clientData.
     clientData, \_ := ctx"clientData".(mapstringany)
    
     if cs, ok := clientData"hrz-canvas-state"; ok {
     if b, err := json.Marshal(cs); err == nil {
     sb.WriteString(fmt.Sprintf(
     \`<script id="hrz-canvas-state" type="application/json">%s</script>\`,
     string(b),
     ))
     }
     }
    
     if vt, ok := clientData"hrz-canvas-verification-token".(string); ok {
     sb.WriteString(fmt.Sprintf(
     \`<script id="hrz-canvas-verification-token" type="application/json">%s</script>\`,
     template.HTMLEscapeString(vt),
     ))
     }
    
     return template.HTML(sb.String())
    }
    
    // renderLayoutEditing writes a complete HTML editing page to w.
    // It reuses pageTmpl from layout.go so that any CSS and JavaScript added to
    // the page template for normal rendering automatically appears in the editing
    // canvas too.
    func renderLayoutEditing(w io.Writer, layoutData mapstringany) error {
     if layoutData == nil {
     return pageTmpl.Execute(w, pageVars{Title: "Page not found"})
     }
     sitecore, \_ := layoutData"sitecore".(mapstringany)
     route, \_ := sitecore"route".(mapstringany)
    
     // Extract editing scripts from sitecore.context before rendering.
     // These are only present when sc\_editMode is "true".
     ctx, \_ := sitecore"context".(mapstringany)
     if ctx == nil {
     ctx = mapstringany{}
     }
     editingHeadScripts := buildEditingHeadScripts(ctx)
    
     if route == nil {
     return pageTmpl.Execute(w, pageVars{
     Title: "Page not found",
     EditingHeadScripts: editingHeadScripts,
     })
     }
    
     // The route UID is used as the parentUID for top-level placeholder IDs.
     routeUID, \_ := route"uid".(string)
    
     phs, \_ := route"placeholders".(mapstringany)
     slot := func(key string) any {
     arr, \_ := phskey.(any)
     return arr
     }
    
     title, \_ := route"displayName".(string)
     if title == "" {
     title, \_ = route"name".(string)
     }
    
     return pageTmpl.Execute(w, pageVars{
     Title: title,
     HasRoute: true,
     Header: renderPlaceholderEditing("headless-header", slot("headless-header"), routeUID),
     Main: renderPlaceholderEditing("headless-main", slot("headless-main"), routeUID),
     Footer: renderPlaceholderEditing("headless-footer", slot("headless-footer"), routeUID),
     EditingHeadScripts: editingHeadScripts,
     })
    }
    
    // handleEditingRender handles GET /api/editing/render.
    // The Page builder calls this endpoint for every page view inside the editor.
    func handleEditingRender(w http.ResponseWriter, r \*http.Request) {
     if r.Method != http.MethodGet {
     http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
     return
     }
    
     if !validateEditingSecret(r) {
     w.Header().Set("Content-Type", "application/json")
     http.Error(w, \`{"message":"Missing or invalid editing secret"}\`, http.StatusUnauthorized)
     return
     }
    
     q := r.URL.Query()
    
     site := q.Get("sc\_site")
     itemId := q.Get("sc\_itemid")
     lang := q.Get("sc\_lang")
     mode := q.Get("mode") // "edit" or "preview"
     version := q.Get("sc\_version")
     layoutKind := q.Get("sc\_layoutKind")
    
     if lang == "" {
     lang = os.Getenv("SITECORE\_SITE\_LANGUAGE\_CODE")
     }
     if lang == "" {
     lang = "en"
     }
     if layoutKind == "" {
     layoutKind = "final"
     }
    
     // sc\_editMode header: "true" returns draft content and embeds editing
     // scripts in sitecore.context; "false" returns published content only.
     editMode := "false"
     if mode == "edit" {
     editMode = "true"
     }
    
     data, err := fetchEditingData(site, itemId, lang, version, layoutKind, editMode)
     if err != nil {
     http.Error(w, err.Error(), http.StatusInternalServerError)
     return
     }
     if data == nil {
     http.Error(w, "item not found", http.StatusNotFound)
     return
     }
    
     w.Header().Set("Content-Type", "text/html; charset=utf-8")
     if err := renderLayoutEditing(w, data.LayoutData); err != nil {
     // Headers already sent; cannot call http.Error.
     \_ = err
     }
    }
    

:::::::

## 新しいエンドポイントを登録する

エンドポイントを作成したら、HTTPサーバーを更新し、エンドポイントを登録してください。

新しいエンドポイントを登録するには:

::::タブズ:::tab{title="Astro"}

Astroはファイルベースのルーティングを採用しているため、前述の2つの手順でファイルを作成する/api/editing/render/api/editing/configだけで、エンドポイントが自動的に登録され、追加の登録は不要です。

::::::tab{title="Go"}

-   main.go更新して、キャッチオール/ハンドラの前の2つの新しい編集エンドポイントを登録します:
    
    // main.go
    package main
    
    import (
     "log"
     "net/http"
     "os"
     "strings"
     "github.com/joho/godotenv"
    )
    
    func main() {
     // Load .env file if present (ignored when not found, e.g. in production).
     \_ = godotenv.Load()
    
     site := os.Getenv("SITECORE\_SITE\_NAME")
     language := os.Getenv("SITECORE\_SITE\_LANGUAGE\_CODE")
     if language == "" {
     language = "en"
     }
    
     // Editing endpoints - registered before the catch-all.
     http.HandleFunc("/api/editing/config", handleEditingConfig)
     http.HandleFunc("/api/editing/render", handleEditingRender)
    
     // Catch-all page route.
     http.HandleFunc("/", func(w http.ResponseWriter, r \*http.Request) {
     routePath := r.URL.Path
     if routePath != "/" {
     routePath = strings.TrimRight(routePath, "/")
     }
    
     data, err := fetchLayoutData(site, language, routePath)
     if err != nil {
     http.Error(w, err.Error(), http.StatusInternalServerError)
     return
     }
     if data == nil {
     if routePath == "/404" {
     http.NotFound(w, r)
     return
     }
     http.Redirect(w, r, "/404", http.StatusFound)
     return
     }
    
     w.Header().Set("Content-Type", "text/html; charset=utf-8")
     if err := renderLayout(w, data); err != nil {
     http.Error(w, err.Error(), http.StatusInternalServerError)
     }
     })
    
     log.Println("Listening on http://localhost:3000")
     log.Fatal(http.ListenAndServe(":3000", nil))
    }
    

:::::::

## ページビルダーをConnectに localhost

あなたのアプリはこれでページビルダーのiframeに読み込む準備ができました。Page Builderはデプロイせずに直接ローカルサーバーに接続できます。これにより、他のSitecoreAIユーザーに影響を与えずにコンポーネントをテストできます。

ページビルダーをlocalhostに接続するには:

::::タブズ:::tab{title="Astro"}

1.  astro.config.mjsでは をsecurity.allowedDomainsを加え、vite.server.cors:
    
    // astro.config.mjs
    import { defineConfig } from "astro/config";
    import node from "@astrojs/node";
    
    export default defineConfig({
     output: "server",
     security: {
     // Allow the SitecoreAI Page builder origins through Astro's
     // cross-origin security middleware so requests from these domains
     // reach the editing API endpoints. Requires Astro >= 5.14.2.
     allowedDomains: 
     { hostname: "pages.sitecorecloud.io", protocol: "https" },
     { hostname: "app.sitecorecloud.io", protocol: "https" },
     ,
     },
     vite: {
     server: {
     // Disable Vite's built-in CORS middleware so that OPTIONS preflight
     // requests from pages.sitecorecloud.io (an HTTPS origin accessing
     // localhost, a private network address) are not intercepted by Vite
     // before reaching the Astro API route handlers. The editing endpoints
     // handle CORS themselves and include the Access-Control-Allow-Private-
     // Network header required by the Private Network Access spec.
     cors: false,
     },
     },
     adapter: node({ mode: "standalone" }),
    });
    
    -   security.allowedDomainsがなければ、Astroはエンドポイントコードが実行される前に、エンドポイントが設定したCORSヘッダーに関係なく、pages.sitecorecloud.ioからのすべてのリクエストに対して返403 Forbidden返します。
    -   vite.server.cors: falseがなければ、Viteの内蔵CORSミドルウェアはpages.sitecorecloud.ioからのすべてのOPTIONSプリフライトを傍受し、Access-Control-Allow-Origin: \*で応答しますが、Access-Control-Allow-Private-Network: trueヘッダーは一切追加しません。HTTPSページがプライベートネットワークアドレス(localhost)にアクセスすると、ブラウザは実際のリクエストを送信する前にプリフライト応答でこのヘッダーを求めます。ブラウザはリクエストをブロックし、Astro OPTIONSハンドラーは一度も動作しません。
2.  開発サーバーを起動:
    
    npm run dev
    
3.  ページビルダーであなたのサイトを開いてください。
    
4.  ページビルダーのキャンバスのすぐ上のリボン、**バージョン** セレクターの右側に「 **デフォルト編集ホスト**」をクリックし、「 **ローカルホスト**」を選択し、アプリのlocalhostアドレス(例: http://localhost:4321)を入力してください。
    
5.  **「保存**」をクリックします。
    
6.  ページビルダーがキャンバス上でアプリを読み込むか確認してください。
    
7.  ビジュアル編集のテスト。
    

::::::tab{title="Go"}

> !注**プライベートネットワークアクセスプリフライト**pages.sitecorecloud.ioのページビルダー(HTTPS発信者)があなたのlocalhostサーバー(プライベートネットワークアドレス)に接続すると、ブラウザは実際のGET /api/editing/config前にプライベートネットワークアクセスのプリフライトOPTIONSリクエストを送信します。/api/editing/configエンドポイントはこのプリフライトを処理し、標準的なCORSヘッダーと並んでAccess-Control-Allow-Private-Network: trueヘッダーで応答します。/api/editing/renderエンドポイントはサブリソースフェッチではなくiframeナビゲーションとして読み込まれているため、ブラウザはそれに対してプライベートネットワークアクセスのプリフライトを送信しません。ブラウザコンソールで空白のキャンバスやCORSエラーが見つかった場合は、**ネットワーク**タブで/api/editing/configのOPTIONSリクエストに失敗しているか確認し、サーバーが稼働してヘッダーを返しているかを確認してください。

1.  開発サーバーを起動:
    
    go run .
    
2.  ページビルダーであなたのサイトを開いてください。
    
3.  ページビルダーのキャンバスのすぐ上のリボン、**バージョン** セレクターの右側に「 **デフォルト編集ホスト**」をクリックし、「 **ローカルホスト**」を選択し、アプリのlocalhostアドレス(例: http://localhost:3000)を入力してください。
    
4.  **「保存**」をクリックします。
    
5.  ページビルダーがキャンバス上でアプリを読み込むか確認してください。
    
6.  ビジュアル編集のテスト。
    

:::::::

## テストビジュアル編集

ページビルダーがキャンバスにアプリを表示した後、視覚的な編集をテストして、一般的な編集操作がうまく機能するか確認してください。

ビジュアル編集のテスト:

1.  開発サーバーを起動または再起動してください。
2.  [ページビルダー](https://pages.sitecorecloud.io/editor)でサイトを開き、[localhostに接続](#connect-the-page-builder-to-localhost)してください。
3.  ページビルダーがキャンバス上でアプリを読み込むか確認してください。
4.  ウェブブラウザの開発者コンソールの **ネットワーク** タブで、 /api/editing/configのリクエストを見つけ、リクエストとレスポンスの詳細を読みます。成功した /api/editing/config 応答は {"editMode":"metadata","components":...}を返します。
5.  フィールドに変更しましょう。例えば、テキストを編集してから、ページを更新した後にlocalhostでその変更が反映されているか確認します。
6.  コンポーネントをキャンバス内の別の場所にドラッグし、ページを更新した後にlocalhostで変更が現れるか確認してください。

## 次のステップ { .notintoc }

Localhostでフロントエンドアプリを設定し、ページビルダーで表示・編集可能にしました。

次に、次のことができます:

-   複数のページタイプでテストして、すべてのコンポーネント実装が正しくマッピングされているか確認し、キャンバス上でメタデータでレンダリングしてください。
-   [アプリをデプロイして、ページビルダーを接続](./visual-editing.md#deploying-your-app) してください。