Technical reference
Site Archive captures point-in-time HTML snapshots of published assets. When an asset (or any asset it references or embeds) publishes, the plugin queues a snapshot, renders the asset deterministically, stores the HTML, captures the underlying field values and a diff against the previous snapshot, and records the result as a searchable archive entry. Projects extend or override behavior through small extension points and Settings-based configuration, with no required project glue beyond marking which asset types should be archived.
Dependencies
The plugin has no required dependency on other Brightspot plugins. It uses Brightspot Go libraries (task host, content publish utilities, state walker, site helpers, hide-urls widget) and one third-party cron description library (net.redhogs.cronparser:cron-parser-core), all pulled in transitively via the plugin's own dependencies. It targets Brightspot 4.8.13.1 and Java 11.
Installation
- Maven
- Gradle
- Gradle (Kotlin DSL)
<!-- Requires Brightspot 4.8 or later. -->
<dependency>
<groupId>com.brightspot.sitearchive</groupId>
<artifactId>site-archive</artifactId>
<version>1.3.0</version>
</dependency>
// Requires Brightspot 4.8 or later.
implementation 'com.brightspot.sitearchive:site-archive:1.3.0'
// Requires Brightspot 4.8 or later.
implementation("com.brightspot.sitearchive:site-archive:1.3.0")
Marking an asset type as archivable
Pick the asset type whose publish events should generate snapshots, typically a top-level page type. Implement Archivable and return the styleguide view class that should render its snapshot HTML. The view class is usually the same one the live page already uses.
1public class Article extends Page implements Archivable {23@Override4public Class<?> getArchiveRenderViewClass() {5return ArticlePageView.class;6}78@Override9public int getMagnitude() {10return 1;11}12}
Archivable extends HasReferenceIndex, so getMagnitude() comes from that interface. Lower numbers rank higher in the asset hierarchy; 1 is fine for a top-level page. getArchiveRenderViewClass() returns a styleguide view interface, and the renderer hands this class to the view system to produce the HTML.
That is the entire code change needed on the project side. Every publish of an Article (or any asset it references) now flows through the plugin's background tasks and produces a SiteArchiveEntry.
Storage configuration
Each SiteArchiveEntry carries a rendered HTML representation as a StorageItem. Configure where that HTML lives by declaring a dari/storage/site-archive block, using the same shape as any other Brightspot named storage.
1# Local development (filesystem)2dari/storage/site-archive/class=com.psddev.dari.util.LocalStorageItem3dari/storage/site-archive/rootPath=/servers/tomcat/storage/4dari/storage/site-archive/baseUrl=http://localhost/storage5dari/storage/site-archive/originBaseUrl=http://localhost/storage
1# Production (AWS S3)2dari/storage/site-archive/class=com.psddev.dari.aws.S3StorageItem3dari/storage/site-archive/bucket=my-project-archive4dari/storage/site-archive/baseUrl=https://my-project-archive.s3.amazonaws.com5dari/storage/site-archive/secureBaseUrl=https://my-project-archive.s3.amazonaws.com6dari/storage/site-archive/originBaseUrl=https://my-project-archive.s3.amazonaws.com
Any Dari storage back end (LocalStorageItem, S3StorageItem, GoogleCloudStorageItem2, and so on) works. If dari/storage/site-archive is not configured, the plugin falls back to the project's default storage and applies the site-archive/ path prefix so the data stays grouped.
Finally, enable the feature; see Configuring Site Archive.
Lifecycle
- Publish:
SiteArchiveCascadingModification#afterSave()runs on every save. If the saved asset is not embedded, is a publish, and is not excluded (see Cascade exclusions), the plugin queries everyArchivableasset that references the saved asset and enqueues aTrackRelationshipsrow for each. - Reference index refresh:
RelationshipTrackingTaskruns once per minute on the task host and processes that queue. For each row it walks the asset's state graph, updates its reference index, and, if the feature is enabled and the asset actually changed, enqueues aCreateSiteArchiveEntryrow. - Archive creation:
SiteArchiveEntryCreationTaskruns on a configurable cron schedule (default every 12 hours) and processes up to 1,000CreateSiteArchiveEntryrows per run. For each one it loads the asset, renders it deterministically viaSiteArchiveRenderer, writes the HTML to storage, captures field values and a diff, and saves theSiteArchiveEntry. A render that fails is retried on a later run; see Retries on render failure. - Cleanup:
CreateSiteArchiveEntryCleanupTaskdeletes all queue rows older than seven days regardless of status, andTrackRelationshipsCleanupTaskdeletes errored rows older than two weeks.
API reference
Archivable
Marker interface for asset types that should produce archive snapshots. Extends HasReferenceIndex so that the asset's embedded references are tracked automatically.
| Method | Returns | Description |
|---|---|---|
getArchiveRenderViewClass() | Class<?> | The styleguide view interface the renderer uses to produce the snapshot HTML. |
getMagnitude() | int | From HasReferenceIndex. Lower numbers rank higher in the asset hierarchy. |
SiteArchiveEntry
The snapshot record. Stores the source asset reference, the rendered HTML (as a StorageItem), the field values at snapshot time, the trigger events that produced the snapshot, and the field diff against the previous snapshot. Globally visible: one editor's archive entries are visible to every editor.
ArchivableModification
Adds helper fields to every Archivable.
| Method | Returns | Description |
|---|---|---|
getLastArchiveCreateDate() | Date | Indexed; the date of the most recent snapshot. Usable in queries and exports. |
getArchiveTriggeringReferences() | List<Recordable> | A UI-only display of which referenced assets would cascade an archive. |
CreateSiteArchiveEntry
The queue record that represents the intent to archive an asset. SiteArchiveEntryCreationTask picks these up periodically and produces the corresponding SiteArchiveEntry.
| Method | Returns | Description |
|---|---|---|
getAttemptCount() | int | Number of archive-creation attempts made for this record so far. Resets to 0 when the asset is republished. |
getLastError() | String | The failure from the most recent attempt that will be retried. Cleared once the record either succeeds or gives up. |
getErrors() | List<String> | Failures from attempts that used up all retries. A non-empty list marks the record as given up on. |
isComplete() | boolean | Whether the record is finished with the queue and can be deleted. Returns true only when getProcessedAt() is set and getErrors() is empty. A record awaiting a retry has processedAt unset and returns false here. |
Retries on render failure
A render failure does not immediately give up on an asset's snapshot. CreateSiteArchiveEntry retries up to three attempts total (one initial attempt plus two retries) before it records the failure in getErrors() for investigation. This absorbs transient failures, such as a page that fails to render while an instance is still warming up after a deploy, without permanently costing that asset its snapshot.
Each retry is scheduled on a later run of SiteArchiveEntryCreationTask rather than retried immediately, so a retry runs against a different state of the deployment than the attempt that failed. Republishing the asset resets getAttemptCount() to zero, since a new version of the asset is a fresh chance to render.
Tasks
| Class | Description |
|---|---|
SiteArchiveEntryCreationTask | Drains the CreateSiteArchiveEntry queue on the schedule configured in SiteArchiveSettings. Runs only on the designated task host. |
CreateSiteArchiveEntryCleanupTask | Runs daily; deletes all queue rows older than seven days by queuedAt, regardless of completion or error state. A slow-draining queue can lose unprocessed rows to this task; see Backfilling existing assets. |
RelationshipTrackingTask | Runs once per minute; refreshes the reference index for assets queued by SiteArchiveCascadingModification and queues CreateSiteArchiveEntry rows for changed assets. |
Backfilling existing assets
Assets published before the feature was enabled never enter the archive queue, because queue rows are only created on publish. To backfill them, run one of these from the Code tool (/_debug/code):
1return com.brightspot.sitearchive.task.SiteArchiveBackfillTask.queueNeverArchived(); // only content with no archive entries yet
1return com.brightspot.sitearchive.task.SiteArchiveBackfillTask.queueAll(); // all archivable content, even if already archived
Each call returns a status message saying what was started, or why it was refused. The backfill requires the feature to be enabled in CMS settings, and only one backfill runs at a time per host.
The backfill only enqueues CreateSiteArchiveEntry rows; SiteArchiveEntryCreationTask creates the entries on its normal schedule. The running backfill shows progress on the Background Tasks page of the host that served the Code tool request, and can be stopped there. In a multi-node cluster that page is node-specific, so stop a backfill on the node running it. Because the creation task drains at most 1,000 rows per run and unprocessed rows are deleted after seven days, temporarily tighten the cron expression (for example, every 15 minutes) for large backfills, then restore it once the queue drains.
Configuration
All overrides live in Brightspot Settings and are read at run time. Names are stable.
| Setting | Default | Effect |
|---|---|---|
siteArchive/storageName | site-archive | Storage name used in StorageItem.Static.createIn(...) for archive HTML. Override to reuse an existing storage block. |
siteArchive/pathPrefix | site-archive/ | Path prefix prepended to every archive HTML object key. Override to keep data under a different folder. |
siteArchive/disableDefaultRoutes | false | When true, the plugin's default CMS servlets at /cms/site-archive/* return 404, and SiteArchiveTool registers no navigation area. Use this together with project-supplied subclasses that route a different URL to the same logic. See URL and permission overrides. |
siteArchive/filtersUrlPath | /site-archive/filters | Path used by managed-record edit links on SiteArchiveEntryQueryFilter. Set this when overriding the filter servlet URL so saved-filter edit links point at the project's path. |
siteArchive/renderTimeoutMs | 30000 | How long the deterministic renderer waits for a single asset to render before timing out. |
The cron schedule and the master enable switch live on the SiteArchiveSettings CMS modification (see Configuring Site Archive), not as Settings properties.
Extension points
Cascade exclusions
Two SPIs, depending on whether the project owns the class.
For project-owned types, implement ExcludedFromSiteArchiveCascade. A single marker with no methods. The plugin already excludes a small set of CMS infrastructure types (ObjectType, Singleton, Site, ToolEntity, ToolUser, Theme, StyleData, JarBundle). Use the marker on project types such as a global navigation, a search-index page, or a welcome page: anything referenced from many archive entries whose churn is not editorially meaningful.
1public class GlobalNavigation extends Record implements ExcludedFromSiteArchiveCascade {2// ...3}
For library types the project cannot modify, register a SiteArchiveCascadeExclusionProvider. Returns a set of Class<? extends Recordable>. Auto-discovered. Use this when the type to exclude lives in a library, such as dimensions or authentication.
1public class ProjectCascadeExclusions implements SiteArchiveCascadeExclusionProvider {23@Override4public Set<Class<? extends Recordable>> getExcludedTypes() {5return Set.of(6Dimension.class,7DimensionValue.class,8AuthenticationManager.class,9AuthorizationManager.class);10}11}
Both SPIs match by isInstantiableTo, so subtypes are also excluded. Provider results are memoized for the JVM lifetime; restart the application to pick up provider changes.
HTML injection
Implement SiteArchiveHtmlInjector to add arbitrary HTML to every (or some) archive snapshot. The plugin parses the rendered HTML once with Jsoup, asks each registered injector for its HTML, and inserts it at the requested anchor. A failure in one injector, whether from a selector miss or an exception, is logged but never aborts the snapshot.
1public class ArchivedBannerInjector implements SiteArchiveHtmlInjector {23@Override4public InjectionPoint getInjectionPoint() {5return InjectionPoint.bodyStart();6}78@Override9public int getPosition() {10return 0;11}1213@Override14public String renderHtml(Archivable content, Site site) {15return "<div class=\"archive-banner\">Snapshot archived for compliance review.</div>";16}17}
Anchors
| Factory | Behavior |
|---|---|
InjectionPoint.headEnd() | Insert just before the closing </head>. |
InjectionPoint.bodyStart() | Insert immediately after the opening <body>. |
InjectionPoint.bodyEnd() | Insert just before the closing </body>. |
InjectionPoint.before(selector) | Insert immediately before the first element matching the Jsoup CSS selector. |
InjectionPoint.after(selector) | Insert immediately after the first match. |
InjectionPoint.prepend(selector) | Insert as the first child of the first match. |
InjectionPoint.append(selector) | Insert as the last child of the first match. |
Filtering
Override shouldInject(Archivable content, Site site) to limit when an injector runs. Defaults to true. A RuntimeException thrown from this method is logged and treated as false for that injector, so a broken filter skips only its own fragment rather than the whole snapshot.
1@Override2public boolean shouldInject(Archivable content, Site site) {3return content instanceof Article;4}
Ordering
getPosition() orders multiple injectors that target the same anchor. Lower values insert first.
Detecting an archive render
SiteArchiveRenderer#IS_ARCHIVE_VIEW_PARAM is set on the deterministic web request. Use it in any view model that should behave differently inside an archive snapshot, for example to hide navigation chrome.
1@WebParameter(value = SiteArchiveRenderer.IS_ARCHIVE_VIEW_PARAM)2private boolean isArchiveView;
Request customization
The plugin's renderer runs in a mock web request context. It intentionally does not depend on fragment rendering, dimensions, authentication, or any other project-specific render-time infrastructure. Projects that need to mutate the request before the view model renders, for example to disable fragment caching so cached HTML does not appear in the snapshot, or to initialize a dimension web request that other view models depend on, implement SiteArchiveRenderRequestCustomizer. Auto-discovered.
1public class ProjectArchiveRenderCustomizer implements SiteArchiveRenderRequestCustomizer {23@Override4public void beforeRender(WebRequest webRequest, Archivable content) {5// Disable fragment rendering so the snapshot reflects current state rather than cached fragments.6webRequest.as(FragmentRenderRequest.class).setFragmentRenderingDisabled(true);78// Initialize the dimension web request with a neutral provider so other view models that depend on9// DimensionWebRequest do not throw.10webRequest.as(DimensionWebRequest.class).init(new ProjectArchiveDimensionProvider());11}12}
Customizers run after the plugin's own request setup and before the view model renders. Both ReflectiveOperationException on instantiation and RuntimeException from beforeRender() are caught, logged with the full stack trace under the logger com.brightspot.sitearchive.render.SiteArchiveRenderer, and the remaining customizers still run. The snapshot continues unless the render itself fails downstream; one broken customizer does not take down archiving.
If a project's live render path depends on fragment rendering or dimension initialization, register a customizer. Without one, the snapshot may pull cached fragments or fail when view models look up an uninitialized dimension context.
SPI lifecycle
All three SPIs (SiteArchiveHtmlInjector, SiteArchiveCascadeExclusionProvider, SiteArchiveRenderRequestCustomizer) are auto-discovered by ClassFinder and instantiated via clazz.getDeclaredConstructor().newInstance().
| Requirement | What it means |
|---|---|
| Public no-arg constructor | An implementation that uses constructor injection, or has a non-public constructor, is logged at WARN and skipped silently. If a registered SPI does not seem to run, check this first. |
| Per-render instantiation | SiteArchiveHtmlInjector and SiteArchiveRenderRequestCustomizer are constructed once per snapshot. Do not cache state in fields between calls. |
| JVM-scoped memoization | SiteArchiveCascadeExclusionProvider results are collected and cached on first lookup. Restart the application to pick up changes to a provider's return value. |
| Non-deterministic discovery order | Across multiple implementations of the same SPI, discovery order is not guaranteed. Within SiteArchiveHtmlInjector, getPosition() orders implementations that target the same anchor. Across different anchors, and for the other two SPIs, ship a single implementation when ordering matters. |
URL and permission overrides
The CMS routes the plugin registers are:
| URL | Permission ID |
|---|---|
/cms/site-archive/search | area/siteArchive/search |
/cms/site-archive/filters | area/siteArchive/filters |
/cms/site-archive/filters/filterResults | area/siteArchive/filters |
/cms/site-archive/filters-search-page | area/siteArchive/filters |
/cms/site-archive/previewDates | None |
@RoutingFilter.Path annotation values must be compile-time constants, so URL paths cannot be moved purely through configuration. To use a different URL or permission scheme, for example to preserve URLs from an existing implementation, do the following:
- Set
siteArchive/disableDefaultRoutes=trueso the plugin's default servlets return404andSiteArchiveToolregisters no navigation entry. - Set
siteArchive/filtersUrlPath=/project/path/to/filtersso the Edit link on savedSiteArchiveEntryQueryFilterrecords points at the project's filter servlet rather than the plugin default. - Ship project-supplied servlet subclasses with the desired
@RoutingFilter.PathandgetPermissionId().
1@RoutingFilter.Path(application = "cms", value = "archive/search")2public class ProjectArchiveSearchPageArea extends SiteArchiveSearchPageArea {34@Override5protected String getPermissionId() {6return "area/archive/search";7}89@Override10public String getInternalName() {11return "archive/search";12}1314@Override15public String getHierarchy() {16return "archive/search";17}1819@Override20public String getUrl() {21return RoutingFilter.Static.getApplicationPath("cms") + "/archive/search";22}23}
A project Tool registering the new navigation area is also required, equivalent to SiteArchiveTool but with a different INTERNAL_NAME and HIERARCHY.
The five servlets that may need subclasses when overriding URLs:
SiteArchiveSearchPageAreaSiteArchiveEntryQueryFilterSearchPageAreaSiteArchiveEntryQueryFilterServletSiteArchiveEntryQueryFilterResultsSiteArchivePreviewDatesServlet