ビジュアル編集を有効にする

日本語翻訳に関する免責事項

このページの翻訳はAIによって自動的に行われました。可能な限り正確な翻訳を心掛けていますが、原文と異なる表現や解釈が含まれる場合があります。正確で公式な情報については、必ず英語の原文をご参照ください。

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

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

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

  1. Sitecoreの環境変数を設定しましょう
  2. GraphQLを使ったレイアウト編集データの取得
  3. HTMLにメタデータラッパーを追加
  4. 作成 /api/editing/config
  5. 作成 /api/editing/render
  6. 新しいエンドポイントを登録する
  7. ページビルダーをConnectに localhost
  8. 視覚編集のテスト:::

!注始める前に

  • アプリのウォークスルー でRender SitecoreAIのコンテンツを 完成させてください。このウォークスルーは、あなたがそこで作成したアプリを直接基に構築します。
  • ビジュアル編集の仕組みを見直しましょう。
  • コードエディタでフロントエンドプロジェクトを開き、ページビルダーでサイトを開いてください。

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クエリを使用します。

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

::::タブズ:::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 }; }

  • クエリを作成するには、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 }

これらのスクリプトは 辞書 を取得しますが、コンポーネントに翻訳可能なUI文字列が含まれていないため、辞書は使用しません。

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

次に、ページビルダーが必要な メタデータブロックを追加し、アプリがレンダリングする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, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """); }

    // --- Metadata code blocks ---

    function metaOpen(chrometype, id) { return ``; }

    function metaClose(chrometype) { return ``; }

    // 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 ( `${JSON.stringify(metadata)}` + rendered + `` ); }

    // --- 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}` : imgTag;

    // Plain text caption field. const caption = fields?.ImageCaption?.value; const html = caption ? `

    ${content}
    ${escHtml(caption)}
    ` : `
    ${content}
    `; 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, `

    ${f.value}
    `); }

    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)}`); }

    // 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 += `

    `; html += renderPlaceholderEditing(col.key, col.items, uid); html += "
    "; } html += ""; 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) + `

    ` + (items ?? ).map((item) => renderComponentEditing(item)).join("") + "
    " + 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 `

    ${inner}
    `; }

    // buildEditingHeadScripts extracts the three categories of editing scripts // from sitecore.context and returns them as a combined block of

この記事を改善するための提案がある場合は、 お知らせください!