Большие сайты содержания часто сталкиваются с значительными проблемами в поддержании видимости и контроля на тысячах страниц. Менеджеры контента изо всех сил пытаются отслеживать статус публикации, определить устаревший или неопубликованный контент и обеспечить общее качество контента. Без автоматизации аудит становится трудоемким, подверженным ошибкам и неэффективным, создавая риски в управлении, соблюдении и пользовательском опыте.
Управление контентом в масштабе предприятия сложно, потому что:
-
Ограниченная видимость – Нет консолидированного представления всего контента сайта.
-
Публикация путаницы – Трудно отслеживать то, что опубликовано, неопубликовано или в ожидании.
-
Проблемы жизненного цикла – Выявление устаревшего или истекшего контента требует ручной проверки.
-
Изменить отслеживание -Мониторинг изменений на тысячах страниц является ресурсным.
-
Экспорт данных -Ручной экспорт CSV медленные, подверженные ошибкам и не масштабируемые.
-
Высокие усилия – Аудит потребляет существенное время и административное время.
Примеры использования:
1. Запланированный отчет Экспорт:
Расширить функцию отчетов, интегрируя запланированное задание, которое автоматически экспортирует выбранную страницу и данные контента (например, заголовок, URL, автор, последняя модифицированная дата) в структурированный формат CSV или Excel для аудита и проверки контента.
2. Проверка и обновления метаданных
Определить и обновить отсутствующие или неполные метаданные (например, заголовок, описание или ключевые слова) для опубликованных страниц, чтобы поддерживать точность контента, соответствие SEO и соблюдение стандартов управления.
3. Срок действия истечения или архивного контента
Обнаружение и списка архивных или истекших страниц, которые больше не являются активными или актуальными, позволяя контент -командам очищать и улучшать производительность сайта и пользовательский опыт.
4. Давние страницы проекта
Выделите черновые страницы, которые сидели неопубликованы в течение длительного периода. Это помогает менеджерам контента просмотреть, обновлять или удалять устаревшие черновики, чтобы обеспечить свежесть контента.
5. SEO-удобные упрощенные URL-адреса
Автоматизируйте обновления для простых адресов (удобные для SEO URL-адреса), чтобы обеспечить согласованность с соглашениями об именах и улучшить видимость поиска.
6. утвержденные, но неопубликованные страницы
Определите страницы, которые были одобрены авторами или редакторами, но еще не опубликованы, что позволяет администраторам пересматривать и публиковать их эффективно.
Решение
Обычай Pagereportsjob Автоматизирует аудит страницы для больших сайтов контента. Он генерирует отчеты 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 < 3);
_logger.Error($"Failed to get pages by type of query '{typeOfQuery}' after 3 attempts.");
return null;
}
private async Task 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 &&
(!p.StopPublish.HasValue || p.StopPublish > 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 <= now),
ContentReportQueryType.Changed => 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
};
}
}
Система автоматически генерирует шесть типов отчетов о страницах: опубликовано, не опубликовано, готова к публикации, истек, изменена и простые адресные страницы.
🧾 Пример: отчет по аудиту страницы (вывод CSV)
Заключение:
А PAGESAUDITJOB представляет собой надежное, предпринимательское решение для аудита контента и отчетности. Его сочетание запланированной автоматизации, доступности веб -интерфейса и комплексных возможностей отчетности делает его важным инструментом для поддержания качества контента и стандартов управления в системе управления контентом.
Надеюсь, этот пост поможет вам укрепить и упростить процесс аудита страницы.
Ваше здоровье! 🥂
06 октября 2025 года
По теме

