当サイトを最適な状態で閲覧していただくにはブラウザのJavaScriptを有効にしてご利用下さい。
JavaScriptを無効のままご覧いただいた場合には一部機能がご利用頂けない場合や正しい情報を取得できない場合がございます。
承知しました
本サイトではWebサイトのエクスペリエンスを向上させるために、Cookieを使用しています。Cookieはブラウザの設定から無効にできます。本サイトで使用するCookieについては、プライバシーポリシーをご確認ください。

Blog

ブログ

統合

KenticoKontentでのAzureFunctionWebhookの使用

By Bryan Soltis  

開発者はすべてを自動化したいと考えています。コードを移動する場合でも、関数をテストする場合でも、マシンを奴隷にして汚い仕事をすることは、コンピューターに対する権限を行使し、コーディングの寿命を短縮するための最良の方法です。

注:「 AzureCognitiveSearchをKenticoKontentと統合する方法」で、このより完全で最新の例を作成しました。

この記事では、 KenticoKontentのWebhookサポートを使用してアプリケーションを合理化する方法の例を紹介します。

プログラミングに関して言えば、できる限り最適化することがすべてです。多分それはパフォーマンスの改善のためです。たぶんそれはあなたが6年前に何をしたかを理解できるようにするためです。動機に関係なく、機能を可能な限り最良の方法で実装する方法を常に考えておく必要があります。そしてそれが世界にウェブフックがある理由です。

これらのプログラマティックセンチネルを立ち上げることで、開発者はアーキテクチャの自動化を簡単に活用できます。 Kentico KontentのようなヘッドレスCMSの場合、この機能はさらに重要です。コンテンツは中央の場所で管理されるため、コンテンツがいつ変更されるかを知り、他のシステムを更新するのは少し難しい場合があります。 Kentico KontentのWebhookサポートにより、心配は終わりです。

私を信じないの?この機能を使用して、WebhookとKenticoKontentを使用してAzureSearchインデックスの更新を自動化する方法を紹介します。

Azure関数を作成する

プロセスの最初のステップは、新しいAzure関数を作成することでした。統合はWebhook-a-fiedになるため、技術的には、KenticoKontentが呼び出す新しい関数を自分のサイトに作成することができたはずです。しかし、その楽しみはどこにありますか?!?このジョブには、新しいAzure Generic WebhookFunctionを使用することを選択しました。

Azure Functionユーティリティで、新しい関数を作成しました。 C#とGenericWebhookフレーバーを選択しました。この関数は、KenticoKontentが投稿するHttpRequestMessageを受け入れるようにすでに配線されています。

Kentico Kontent

これがデフォルトの関数コードで、 HttpRequestMessageを受け入れ、いくつかの基本的な検証を行います。

 #r 'Newtonsoft.Json'using System;using System.Net;using Newtonsoft.Json;public static async Task Run(HttpRequestMessage req, TraceWriter log){ log.Info($'Webhook was triggered!'); string jsonContent = await req.Content.ReadAsStringAsync(); dynamic data = JsonConvert.DeserializeObject(jsonContent); if (data.first == null || data.last == null) { return req.CreateResponse(HttpStatusCode.BadRequest, new { error = 'Please pass first/last properties in the input object' }); } return req.CreateResponse(HttpStatusCode.OK, new { greeting = $'Hello {data.first} {data.last}!' });}

次に、私はKentico Kontent配達Azureの検索NuGetパッケージを持ち込むにproject.jsonファイルを作成しました。

Kentico Kontent

project.jsonファイルに変更を加えると、NuGetパッケージの復元が実行されます。

Kentico Kontent

ハッシュジェネレーターを追加する

すべてのKenticoKontent通知には、ヘッダーにシステム生成のハッシュ署名が含まれています。これは、リクエストがKenticoKontentからのものであることを検証するのに役立ちます。 Azure関数で、通知を検証するためのハッシュを生成する新しい関数を作成しました。

 private static string GenerateHash(string message, string secret){ secret = secret ?? ''; var encoding = new System.Text.UTF8Encoding(); byte[] keyByte = encoding.GetBytes(secret); byte[] messageBytes = encoding.GetBytes(message); using (var hmacsha256 = new HMACSHA256(keyByte)) { byte[] hashmessage = hmacsha256.ComputeHash(messageBytes); return Convert.ToBase64String(hashmessage); }}

JSON投稿を確認する

プロセスの次のステップは、リクエストを検証することでした。リクエストからX-KC-Signatureヘッダーを読み取りました。

 // Get the signature for validation IEnumerable headerValues = req.Headers.GetValues('X-KC-Signature'); var sig = headerValues.FirstOrDefault();

次に、 HttpRequestMessage.Contentを読み込みます。

 var content = req.Content; string jsonContent = content.ReadAsStringAsync().Result;

次に、 GenerateHash関数を呼び出して、リクエストを検証しました。 KenticoKontentインターフェースからWebHookSecret値を使用したことに注意してください。この値は、KenticoKontent内でWebhookが有効になっている場合に作成されます。今のところ、プレースホルダーとしてApplicationSetting値を使用しました。次に、生成されたハッシュをX-KC-Signatureヘッダー値と比較しました。

 // Generate a hash using the content and the webhook secret var hash = GenerateHash(jsonContent, ConfigurationManager.AppSettings['KenticoKontentWebhookSecret']); // Verify the notification is valid if(sig != hash) { return req.CreateResponse(HttpStatusCode.Unauthorized, new { error = 'Unauthorized!' }); }

次に、HttpRequestMessageでJSONデータを読み込む必要がありました。既存の関数コードのいくつかを活用し、 JsonSerializerSettings値をいくつか追加しました。

 var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Ignore }; dynamic data = JsonConvert.DeserializeObject(jsonContent, settings); if (data == null) { return req.CreateResponse(HttpStatusCode.BadRequest, new { error = 'Please pass data properties in the input object' }); }

次に、KenticoKontentでどのような操作が完了したかを確認しました。公開/非公開アクションのみを処理したかったので、アクションを決定するための呼び出しを作成しました。

 // Determine the operation // Only process if it is publish or unpublish string strOperation = data.message.operation.ToString().ToLower(); switch(strOperation) { case 'publish': blnValid = true; blnPublish = true; break; case 'unpublish': blnValid = true; blnPublish = false; break; }

次に、アイテムリストをループして、影響を受けるコンテンツアイテムを取得しました。

 // Make sure it's a valid operation if(blnValid) { List lstCodeNames = new List(); foreach(var item in data.data.items) { lstCodeNames.Add(item.codename.ToString()); }…

更新されたコンテンツアイテムのリストを取得したら、それらを処理する準備が整いました。

アイテムの詳細を取得する

更新されたコンテンツアイテムごとに、検索インデックスを更新するために詳細を取得する必要がありました。新しいUpdateIndex関数を作成し、 DeliveryClientを作成しました。

 public async static Task UpdateIndex(List lstCodeNames, TraceWriter log){ List lstActions = new List(); DeliveryClient client = new DeliveryClient(ConfigurationManager.AppSettings['SoltiswebProjectID'], ConfigurationManager.AppSettings['SoltiswebPreviewAPIKey']); // Loop through each updated content item foreach(string codename in lstCodeNames) { …

クライアント作成の一部としてPreviewAPIを指定していることに注意してください。 Webhookは公開イベントと非公開イベントに対して呼び出されるため、コンテンツアイテムの詳細を常に取得できる必要があります。

次に、Delivery APIを呼び出して、コンテンツアイテムのコード名を指定して詳細を取得しました。

 DeliveryItemResponse response = await client.GetItemAsync(codename);

呼び出しで結果が返された場合は、レコードの新しいAzure SearchIndexアクションを作成しました。公開されたアイテムの場合、これは検索インデックスを新しいデータで更新することを意味しました。未公開のアイテムの場合、これはインデックスからレコードを削除することを意味しました。

 if(response != null) { var item = response.Item; var doc = new Document(); log.Info(item.GetString('name')); doc.Add('CodeName', item.System.Id); doc.Add('Type', item.System.Type); doc.Add('Name', item.GetString('name')); doc.Add('PageAlias', item.GetString('pagealias')); doc.Add('Location', item.GetString('eventlocation')); doc.Add('Date', item.GetDateTime('date')); if(blnPublish) { lstActions.Add(IndexAction.MergeOrUpload(doc)); } else { lstActions.Add(IndexAction.Delete(doc)); } }

この機能は、リスト内のコンテンツアイテムごとに呼び出され、アイテムごとにAzure SearchIndexアクションが作成されるようにします。これらのアクションは、AzureSearchサービスに送信するアクションのリストに追加されました。

注意

このブログでは、更新されたアイテムごとに汎用オブジェクトタイプを使用しています。もう1つのオプションは、 Kentico Kontent Code Generatorプロジェクトを活用して、コンテンツアイテムタイプごとに厳密に型指定されたクラスを作成することです。 Azure関数を使用しているため、コードを最小化し、ジェネリック型を使用することにしました。実装によっては、コードジェネレーターを使用してその機能を利用することを検討する必要があります。

AzureSearchを更新する

次のステップは、アクションでAzureSearchインデックスを更新することでした。インデックス用に新しいSearchServiceClientISearchIndexClientを作成しました。次に、SearchIndexActionsのリストをISearchIndexClientに渡しました。また、Azure Function内にいくつかのメッセージングを追加して、追加/更新または削除されたドキュメントの数を教えてくれました。

 if(lstActions.Count > 0) { // Get the search client SearchServiceClient serviceClient = new SearchServiceClient(ConfigurationManager.AppSettings['AzureSearchServiceName'], new SearchCredentials(ConfigurationManager.AppSettings['AzureSearchAPIKey'])); ISearchIndexClient indexClient = serviceClient.Indexes.GetClient(ConfigurationManager.AppSettings['AzureSearchIndexName']); indexClient.Documents.Index(new IndexBatch(lstActions)); if(blnPublish) { log.Info(lstActions.Count.ToString() + ' documents added/updated!'); } else { log.Info(lstActions.Count.ToString() + ' documents deleted!'); } } else { log.Info('No document updated!'); }

Webhookを有効にする

プロセスの最後のステップは、KenticoKontentに私の新しいWebhookについて伝えることでした。 Azure Functionで、関数のURLをコピーしました。

Kentico Kontent

KenticoKontentの[プロジェクト設定]-> [Webhook]セクションで、コピーしたURLを使用して、AzureFunction用の新しいWebhookを追加しました。また、シークレット値をコピーして、Azureアプリケーション設定を更新しました。

Kentico Kontent

テスト

すべての配管が整ったので、テストする準備ができました。まず、Azure Search Indexにクエリを実行して、イベントがインデックスの一部ではないことを確認しました。

Kentico Kontent

次に、Kentico Kontentプロジェクトにアクセスして、新しいSpeakingEngagementを作成しました。

Kentico Kontent

イベントを作成したら、それを公開してWebhookを実行しました。

Kentico Kontent

Azure Functionで、Webhookが実行され、アイテムが更新されたことを確認しました。

Kentico Kontent

次に、インデックスを照会して、コンテンツアイテムが追加されたことを確認しました。

Kentico Kontent

次に、KenticoKontentでコンテンツアイテムを非公開にしました。

Kentico Kontent

Azure Functionで、アイテムが処理されたことを確認しました。

Kentico Kontent

最後に、Azure Search Indexにクエリを実行して、アイテムが削除されたことを確認しました。

Kentico Kontent

これが私のライブサイトの検索に表示される公開されたイベントです。

Kentico Kontent

前進する

ご覧のとおり、Kentico KontentのWebhookサポートは、プラットフォーム内で非常に役立つ機能です。この機能を活用することで、開発者は他のシステム内のコンテンツ更新プロセスを簡単に自動化できます。これにより、パフォーマンスが向上し、コードが少なくなり、アプリケーションがより合理化されます。 Kentico Kontent Webhookの完全なドキュメントをチェックして、プロジェクトで利用できる可能性を確認することをお勧めします。がんばろう!

Kentico Kontent Webhookの詳細については、こちらをご覧ください

完全なAzure関数コードは次のとおりです。

#r 'Newtonsoft.Json'using System;using System.Net;using System.Text;using Newtonsoft.Json;using Kentico.Kontent.Delivery;using Microsoft.Azure.Search;using Microsoft.Azure.Search.Models;using System.Configuration;using System.Security.Cryptography;private static bool blnValid = false;private static bool blnPublish = false;public static async Task Run(HttpRequestMessage req, TraceWriter log){    System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;    // Get the signature for validation    IEnumerable headerValues = req.Headers.GetValues('X-KC-Signature');    var sig = headerValues.FirstOrDefault();    // Get the content    var content = req.Content;    string jsonContent = content.ReadAsStringAsync().Result;    // Generate a hash using the content and the webhook secret    var hash = GenerateHash(jsonContent, ConfigurationManager.AppSettings['KenticoKontentWebhookSecret']);    // Verify the notification is valid    if(sig != hash)    {        return req.CreateResponse(HttpStatusCode.Unauthorized, new        {            error = 'Unauthorized!'        });    }        var settings = new JsonSerializerSettings                    {                        NullValueHandling = NullValueHandling.Ignore,                        MissingMemberHandling = MissingMemberHandling.Ignore                    };    dynamic data = JsonConvert.DeserializeObject(jsonContent, settings);    if (data == null)    {        return req.CreateResponse(HttpStatusCode.BadRequest, new        {            error = 'Please pass data properties in the input object'        });    }    // Determine the operation    // Only process if it is publish or unpublish    string strOperation = data.message.operation.ToString().ToLower();    switch(strOperation)    {        case 'publish':            blnValid = true;            blnPublish = true;            break;        case 'unpublish':            blnValid = true;            blnPublish = false;            break;    }    // Make sure it's a valid operation    if(blnValid)    {        List lstCodeNames = new List();          foreach(var item in data.data.items)        {            lstCodeNames.Add(item.codename.ToString());        }        // Update the search index        if(lstCodeNames.Count > 0)        {            await UpdateIndex(lstCodeNames, log);        }        return req.CreateResponse(HttpStatusCode.OK, new        {            greeting = $'Success!'        });    }    else    {        return req.CreateResponse(HttpStatusCode.NotImplemented, new        {            greeting = $'Not Supported!'        });    }}public async static Task UpdateIndex(List lstCodeNames, TraceWriter log){    List lstActions = new List();    DeliveryClient client = new DeliveryClient(ConfigurationManager.AppSettings['SoltiswebProjectID'], ConfigurationManager.AppSettings['SoltiswebPreviewAPIKey']);    // Loop through each updated content item    foreach(string codename in lstCodeNames)    {        log.Info($'Processing ' + codename);                // Get the details from Kentico Kontent        DeliveryItemResponse response = await client.GetItemAsync(codename);        if(response != null)        {            var item = response.Item;            var doc = new Document();                        log.Info(item.GetString('name'));            doc.Add('CodeName', item.System.Id);            doc.Add('Type', item.System.Type);                        doc.Add('Name', item.GetString('name'));            doc.Add('PageAlias', item.GetString('pagealias'));            doc.Add('Location', item.GetString('eventlocation'));            doc.Add('Date', item.GetDateTime('date'));            // Determine the index action            if(blnPublish)            {                lstActions.Add(IndexAction.MergeOrUpload(doc));            }            else            {                lstActions.Add(IndexAction.Delete(doc));            }        }        else        {            log.Info($'Item not found!');        }    }    try    {        if(lstActions.Count > 0)        {            // Get the search client            SearchServiceClient serviceClient = new SearchServiceClient(ConfigurationManager.AppSettings['AzureSearchServiceName'], new SearchCredentials(ConfigurationManager.AppSettings['AzureSearchAPIKey']));            ISearchIndexClient indexClient = serviceClient.Indexes.GetClient(ConfigurationManager.AppSettings['AzureSearchIndexName']);            indexClient.Documents.Index(new IndexBatch(lstActions));            if(blnPublish)            {                log.Info(lstActions.Count.ToString() + ' documents added/updated!');            }            else            {                                log.Info(lstActions.Count.ToString() + ' documents deleted!');            }        }        else        {            log.Info('No document updated!');        }    }    catch (IndexBatchException e)    {        log.Info(e.Message);    }}private static string GenerateHash(string message, string secret){    secret = secret ?? '';    var encoding = new System.Text.UTF8Encoding();    byte[] keyByte = encoding.GetBytes(secret);    byte[] messageBytes = encoding.GetBytes(message);    using (var hmacsha256 = new HMACSHA256(keyByte))    {        byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);        return Convert.ToBase64String(hashmessage);    }}
                    
                
                    

                

Headless CMSの導入をお考えでしょうか?

クラウドとマルチデバイスに最適化されたKentico Kontentをお試しください