1766162630
2025-12-17 12:42:00
問題
ニッチな問題ですが、プレゼンテーション レイヤーが Netlify でホストされるヘッドレス ソリューションを構築しています。通常の配信サイクルで作業をテストする必要がある場合、一意の URL でデプロイメント プレビューが生成されます。フロントエンド デプロイメントの一部として、新しい Web フックが Optimizely Content Graph に登録されるため、コンテンツが公開されるときにフロントエンド キャッシュを無効にできます。ここでの問題は、非常に多くのデプロイメントが発生すると、Web フックのリストが非常に長くなる可能性があることです。 Webhook 登録をスキップするようにデプロイメント プレビューを構成することもできましたが、その場合、それらの環境ではコンテンツがキャッシュされることになり、リアルタイムのコンテンツ更新の適切なテストができなくなります。
解決策
テスト ワークフローを犠牲にする代わりに、Optimizely のスケジュールされたジョブ機能を使用して自動クリーンアップ ソリューションを構築しました。このジョブは、登録されているすべての Webhook を定期的にスキャンし、本番環境に属さないものを削除して、リストをクリーンで管理しやすい状態に保ちます。
実装
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using RestSharp;
using System.Text;
using System.Text.Json;
namespace Client.Cms.Infrastructure.Jobs
{
[ScheduledPlugIn(DisplayName = "Clear Graph Webhooks", GUID = "D4F5A6B7-C8D9-4E0F-AB12-3456789ABCDE")]
public class ClearGraphWebhooksJob : ScheduledJobBase
{
private readonly IConfiguration _configuration;
private readonly IContentLoader _contentLoader;
private bool _stopSignaled;
public ClearGraphWebhooksJob(IConfiguration configuration, IContentLoader contentLoader)
{
_configuration = configuration;
_contentLoader = contentLoader;
IsStoppable = true;
}
public override string Execute()
{
try
{
// Fetch Content Graph Gateway Address, AppKey and Secret from appSettings
var gatewayAddress = _configuration["Optimizely:ContentGraph:GatewayAddress"];
var appKey = _configuration["Optimizely:ContentGraph:AppKey"];
var appSecret = _configuration["Optimizely:ContentGraph:Secret"];
if (string.IsNullOrEmpty(gatewayAddress) || string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(appSecret))
{
return "Failed: Missing configuration values for ContentGraph";
}
var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{appKey}:{appSecret}"));
var baseUrl = $"{gatewayAddress}/api/webhooks";
var client = new RestClient(new RestClientOptions(baseUrl));
var request = new RestRequest();
request.AddHeader("Accept", "application/json");
request.AddHeader("Authorization", $"Basic {auth}");
var response = client.GetAsync(request).GetAwaiter().GetResult();
if (!response.IsSuccessful)
{
OnStatusChanged($"Failed to fetch webhooks: {response.StatusCode} {response.ErrorMessage}");
return $"Failed: {response.StatusCode} - {response.ErrorMessage}";
}
var json = response.Content;
var webhooks = JsonSerializer.Deserialize>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (webhooks == null || webhooks.Count == 0)
{
OnStatusChanged("No webhooks found or failed to deserialize response.");
return "Success: No webhooks to process";
}
int deletedCount = 0;
int skippedCount = 0;
foreach (var webhook in webhooks)
{
if (_stopSignaled)
{
return $"Stopped: Processed {deletedCount} deletions, {skippedCount} skipped";
}
var urls = GetUrls().ToList();
if (urls.Any(url => webhook.Request.Url.Contains(url)))
{
OnStatusChanged($"Skipping webhook {webhook.Id} with URL {webhook.Request.Url}");
skippedCount++;
continue;
}
DeleteWebhook(webhook.Id.ToString(), auth, baseUrl);
deletedCount++;
}
return $"Success: Deleted {deletedCount} webhooks, skipped {skippedCount}";
}
catch (Exception ex)
{
OnStatusChanged($"Error: {ex.Message}");
return $"Failed: {ex.Message}";
}
}
public override void Stop()
{
_stopSignaled = true;
base.Stop();
}
private void DeleteWebhook(string id, string auth, string baseUrl)
{
var client = new RestClient(new RestClientOptions($"{baseUrl}/{id}"));
var request = new RestRequest();
request.AddHeader("Authorization", $"Basic {auth}");
var response = client.DeleteAsync(request).GetAwaiter().GetResult();
if (!response.IsSuccessful)
{
OnStatusChanged($"Failed to delete webhook {id}: {response.StatusCode} {response.ErrorMessage}");
}
else
{
OnStatusChanged($"Deleted webhook {id}");
}
}
// Fetch URLs from Site Config Block
private IEnumerable GetUrls()
{
var globalSettingsBlock = _contentLoader
.GetChildren(ContentReference.GlobalBlockFolder)
.FirstOrDefault();
return globalSettingsBlock?.FrontendUrls ?? Enumerable.Empty;
}
}
public class WebhookRequest
{
public string Url { get; set; }
public string Method { get; set; }
public Dictionary Headers { get; set; }
}
public class WebhookItem
{
public Guid Id { get; set; }
public WebhookRequest Request { get; set; }
public string Preset { get; set; }
}
}
仕組み
- 認証: ジョブは、構成の AppKey と Secret を使用して Content Graph で認証します。
- Webhook の取得: Content Graph API を介して、登録されているすべての Webhook を取得します。
- URL によるフィルター: ジョブは、グローバル設定ブロックに保存されている実稼働 URL の構成可能なリストと照合して各 Webhook URL をチェックします。
- スマート削除: 運用 URL に一致する Webhook は保持されます。他はすべて削除されます
- 進行状況の追跡: ジョブは進行状況を報告し、削除とスキップの概要を提供します。
主な特長
- コンテンツ主導型構成: 実稼働 URL は Optimizely ブロックに保存され、コードを変更せずにコンテンツ エディターで編集できるようになります。
- Stoppable: ジョブは IsStoppable = true を実装し、正常にキャンセルするための停止信号を適切に処理します。
- 包括的なログ: OnStatusChanged() を使用して、スケジュールされたジョブ インターフェイスに表示される詳細な進行状況の更新を提供します。
- エラー処理: 堅牢な try-catch ブロックと検証により、意味のあるエラー メッセージを表示してジョブが正常に失敗することが保証されます。
利点
- デプロイプレビューでは Webhook を登録し、リアルタイムのコンテンツ更新をテストできます
- Webhook リストは自動メンテナンスによりクリーンな状態を維持します
- 手動介入は必要ありません
- コンテンツエディターは、CMS インターフェイスを通じてどの URL を保存するかを制御します
結論
Optimizely のスケジュールされたジョブ フレームワークを活用することで、すべての環境で完全な機能を維持しながら、クリーンな Webhook レジストリを維持する自動化ソリューションを作成しました。このアプローチは、Webhook を手動で管理したり、デプロイメント プレビューでのテスト機能を危険にさらしたりするよりもはるかに保守しやすいです。
スケジュールされたジョブは、毎日、毎週、または展開頻度に応じた任意の間隔で実行できるため、コンテンツ グラフの Webhook リストが無駄がなく管理しやすい状態に保たれます。
2025 年 12 月 17 日
#PaaS #CMS #でのコンテンツ #グラフ #Webhook #のクリーンアップ #スケジュールされたジョブ