1759794748
2025-10-06 04:57:00
大規模なコンテンツ サイトでは、数千ページにわたる可視性と制御を維持するという大きな課題に直面することがよくあります。コンテンツ管理者は、公開ステータスを追跡し、期限切れまたは未公開のコンテンツを特定し、コンテンツ全体の品質を確保することに苦労しています。自動化がなければ、監査は時間がかかり、エラーが発生しやすく、非効率になり、ガバナンス、コンプライアンス、ユーザー エクスペリエンスにリスクが生じます。
エンタープライズ規模でのコンテンツ管理は次の理由から困難です。
-
限られた可視性 – すべてのサイト コンテンツを統合して表示することはできません。
-
出版の混乱 – 何が公開されているのか、何が未公開なのか、または保留中ののかを追跡するのは困難です。
-
ライフサイクルの問題 – 古いコンテンツまたは期限切れのコンテンツを特定するには、手動チェックが必要です。
-
変更の追跡 – 数千ページにわたる編集を監視するには、リソースが大量に消費されます。
-
データのエクスポート – 手動 CSV エクスポートは時間がかかり、エラーが発生しやすく、拡張性がありません。
-
高い労力 – 監査には、IT と管理の時間がかなりかかります。
使用例:
1. スケジュールされたレポートのエクスポート:
コンテンツの監査とレビューのために、選択したページおよびコンテンツ データ (タイトル、URL、作成者、最終更新日など) を構造化された CSV または Excel 形式に自動的にエクスポートするスケジュールされたジョブを統合することで、レポート機能を拡張します。
2. メタデータの検証と更新
コンテンツの正確性、SEO コンプライアンス、ガバナンス基準の順守を維持するために、公開されたページの欠落または不完全なメタデータ (タイトル、説明、キーワードなど) を特定して更新します。
3. 期限切れまたはアーカイブされたコンテンツ
アクティブではなくなった、または関連性のなくなったアーカイブされたページまたは期限切れのページを検出してリストし、コンテンツ チームがクリーンアップしてサイトのパフォーマンスとユーザー エクスペリエンスを向上できるようにします。
4. 長年にわたるドラフトページ
長期間未公開のまま放置されていた下書きページを強調表示します。これは、コンテンツ管理者が古い下書きを確認、更新、または削除して、コンテンツの鮮度を確保するのに役立ちます。
5. SEO に適した簡略化された URL
単純なアドレス (SEO に適した URL) への更新を自動化して、命名規則との一貫性を確保し、検索の可視性を向上させます。
6. 承認済みだが未公開のページ
著者または編集者によって承認されているがまだ公開されていないページを特定し、管理者がそれらを効率的に確認して公開できるようにします。
解決
カスタム ページレポートジョブ 大規模なコンテンツ サイトのページ監査を自動化します。 CSV レポートを生成し、管理者やコンテンツ管理者にコンテンツ構造、公開ステータス、更新の概要を提供します。
これらの問題は、次のような自動化されたジョブ プロセスで解決されます。
- すべてのコンテンツ タイプとステータスを自動的にスキャンして識別します。
- 一貫性を保つために標準化された CSV レポートを生成します。
- ページングと非同期実行を使用して、大規模なデータセットを効率的に処理します。
- コンテンツのステータスと更新に関するほぼリアルタイムの洞察を提供します。
- ロールベースの権限でアクセスを保護します。
using EPiServer.Logging;
using EPiServer.PlugIn;
using EPiServer.Scheduler;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using Microsoft.Extensions.Options;
using Nito.AsyncEx;
using MyCompany.Web.Features.Reports.ExistingPages.Models;
using MyCompany.Web.Infrastructure.Cms;
using MyCompany.Web.Infrastructure.Cms.Settings;
using static MyCompany.Web.Features.Core.Constants.CommonConstants;
namespace MyCompany.Web.Features.Jobs
{
[ScheduledPlugIn(
DisplayName = "Page Report Job ",
Description = "Exports existing pages to CSV for administrators and content managers",
IntervalType = ScheduledIntervalType.None,
SortIndex = -1004 + 'D')]
public class PageReportJob : ScheduledJobBase
{
private readonly ILogger _logger;
private readonly IMediaFileService _mediaFileService;
private readonly IContentRepository _contentRepository;
private readonly IContentReportQueryService _contentReportQueryService;
private readonly IContentVersionRepository _contentVersionRepository;
private readonly ISettingsService _settingsService;
private readonly ExistingPagesReportOptions _options;
private bool _stopSignaled;
public PageReportJob (
IMediaFileService mediaFileService,
IContentRepository contentRepository,
IContentReportQueryService contentReportQueryService,
IContentVersionRepository contentVersionRepository,
ISettingsService settingsService,
IOptions options)
{
_mediaFileService = mediaFileService;
_contentRepository = contentRepository;
_contentReportQueryService = contentReportQueryService;
_contentVersionRepository = contentVersionRepository;
_settingsService = settingsService;
_options = options?.Value ?? new ExistingPagesReportOptions();
_logger = LogManager.GetLogger();
IsStoppable = true;
}
public override void Stop() => _stopSignaled = true;
public override string Execute()
{
try
{
return AsyncContext.Run(GenerateReports);
}
catch (Exception ex)
{
_logger.Error(ex.StackTrace);
return "The job stopped due to an exception.";
}
}
private async Task GenerateReports()
{
var reportQueryTypes = Enum.GetValues(typeof(ContentReportQueryType))
.Cast()
.Select(v => v.ToString())
.ToList();
// Adding a custom query type
reportQueryTypes.Add("NotPublishedPages");
foreach (var reportQueryType in reportQueryTypes)
{
if (_stopSignaled) break;
await CreateReport(reportQueryType);
}
return "Exported all pages successfully.";
}
private async Task CreateReport(string reportQueryType)
{
try
{
_logger.Information($"Starting report creation for type '{reportQueryType}'.");
var csvFolderPath = _mediaFileService.GetOrCreateFolder(
ExistingPageReportTypes.FolderName,
SiteDefinition.Current.SiteAssetsRoot);
var pageCollection = await SetPagesByTypeOfQuery(reportQueryType);
var pages = pageCollection
.Select(x => ExistingPageViewModel.Make(
x,
_contentVersionRepository.List(x.ContentLink).Where(v => v.LanguageBranch == x.Language?.Name)))
.Where(x => !string.IsNullOrWhiteSpace(x.Url))
.ToList();
var fileName = $"{ExistingPageReportTypes.PrefixFileName}_{reportQueryType.ToLower()}";
var media = _mediaFileService.SaveFile(
fileName,
ExistingPageReportTypes.FileExtension,
csvFolderPath,
new byte[0]);
_mediaFileService.WriteCsv(media, pages);
_contentRepository.Save(media, SaveAction.Publish, AccessLevel.NoAccess);
_logger.Information($"Report '{fileName}' created and published with {pages.Count} rows.");
}
catch (Exception ex)
{
_logger.Error($"Error in CreateReport for type '{reportQueryType}': {ex}");
throw;
}
}
private async Task SetPagesByTypeOfQuery(string typeOfQuery)
{
var retryCounter = 0;
do
{
try
{
return await ReadPageDataCollection(typeOfQuery);
}
catch (Exception ex)
{
retryCounter++;
_logger.Warning($"Error in SetPagesByTypeOfQuery (attempt {retryCounter}) for query '{typeOfQuery}': {ex}");
}
}
while (retryCounter ReadPageDataCollection(string typeOfQuery)
{
const int MaximumRows = 50;
int receivedRecords, pageNumber = 0;
var queryType = string.Equals("NotPublishedPages", typeOfQuery, StringComparison.OrdinalIgnoreCase)
? "ReadyToPublish"
: typeOfQuery;
var typeEnum = GetType(queryType);
var startDate = (typeEnum == ContentReportQueryType.Changed && _options?.ChangedWindowDays is int days && days > 0)
? DateTime.Now.AddDays(-days)
: new DateTime(1900, 1, 1);
if (typeEnum == ContentReportQueryType.Changed)
{
_logger.Information($"Using ChangedWindowDays of {_options.ChangedWindowDays}, start date {startDate:yyyy-MM-dd}");
}
var query = new ContentReportQuery
{
Root = ContentReference.StartPage,
StartDate = startDate,
EndDate = DateTime.Now,
PageSize = 1,
PageNumber = pageNumber,
TypeOfQuery = typeEnum,
IsReadyToPublish = string.Equals("ReadyToPublish", typeOfQuery, StringComparison.OrdinalIgnoreCase)
};
var totalRecords = await Task.Run(() =>
{
_contentReportQueryService.Get(query, out int outRows);
return outRows;
});
var pageDataCollection = new PageDataCollection(totalRecords);
do
{
query.PageNumber = pageNumber;
query.PageSize = MaximumRows;
var enumerable = await Task.Run(() => _contentReportQueryService.Get(query, out int _));
foreach (ContentReference item in enumerable)
{
if (_contentRepository.Get(item) is PageData page)
{
pageDataCollection.Add(page);
}
}
pageNumber++;
receivedRecords = MaximumRows * pageNumber + 1;
}
while (totalRecords > receivedRecords);
_logger.Information($"Retrieved {pageDataCollection.Count} pages for query type '{typeOfQuery}'.");
var filtered = ApplyGuards(pageDataCollection, typeEnum, typeOfQuery);
return new PageDataCollection(filtered);
}
private IEnumerable ApplyGuards(IEnumerable items, ContentReportQueryType type, string typeOfQuery)
{
var now = DateTime.Now;
return type switch
{
ContentReportQueryType.Published => items.Where(p =>
p.CheckPublishedStatus(PagePublishedStatus.Published) &&
p.StartPublish now)),
ContentReportQueryType.ReadyToPublish when
string.Equals(typeOfQuery, "NotPublishedPages", StringComparison.OrdinalIgnoreCase) =>
items.Where(p => (!p.StopPublish.HasValue || p.StopPublish > now) && (!p.StartPublish.HasValue || p.StartPublish > now)),
ContentReportQueryType.ReadyToPublish => items,
ContentReportQueryType.Expired => items.Where(p => p.StopPublish.HasValue && p.StopPublish.Value items,
ContentReportQueryType.SimpleAddress => items.Where(p => !string.IsNullOrWhiteSpace(p.URLSegment)),
_ => items
};
}
private ContentReportQueryType GetType(string type) =>
type switch
{
"Published" => ContentReportQueryType.Published,
"ReadyToPublish" => ContentReportQueryType.ReadyToPublish,
"Expired" => ContentReportQueryType.Expired,
"Changed" => ContentReportQueryType.Changed,
"SimpleAddress" => ContentReportQueryType.SimpleAddress,
_ => ContentReportQueryType.Published
};
}
}
システムは、公開済み、未公開、公開準備完了、期限切れ、変更済み、簡易アドレス ページの 6 種類のページ レポートを自動的に生成します。
🧾 例: ページ監査レポート (CSV 出力)
結論:
の ページ監査ジョブ は、コンテンツの監査とレポートのための堅牢なエンタープライズ グレードのソリューションを表します。スケジュールされた自動化、Web インターフェイスへのアクセシビリティ、および包括的なレポート機能の組み合わせにより、Optimizely コンテンツ管理システムのコンテンツ品質とガバナンス基準を維持するための不可欠なツールとなっています。
この投稿がページ監査プロセスの強化と合理化に役立つことを願っています。
乾杯! 🥂
2025 年 10 月 6 日
#Optimizely #における大規模コンテンツ #サイトの自動ページ監査