Skip to content
bitzorcas
中EN

Guide

Website content lifecycle, cache, and SEO

Content state machine, HTML sanitization, slug concurrency, public reads, cache invalidation, feeds, sitemaps, and scheduled-publishing gap.

Last updated

Website Content is a global public-content aggregate without TenantId. Management APIs create and transition it; public APIs read Published only. A publication integration event invalidates tagged output cache across processes.

1. State machine

Schedule future timePublish nowmanual Publish onlyUnpublishSchedulePublishArchiveArchiveArchive

Draft

Scheduled

Published

Unpublished

Archived

Published and Archived are not editable. Published must be unpublished before archive. Publish replaces PublishedAt with the current clock value.

2. What Scheduled means

Schedule validates a future instant and writes Scheduled plus PublishedAt. No job scans due content, and public queries do not promote it on read.

Scheduled therefore means “plan recorded,” not guaranteed automatic publication. GA needs an owner job with a trusted clock, lease, the same publish use case, and the publication event.

3. Create and update

Create trims/lowercases Slug, prechecks collision, sanitizes Body, creates the aggregate, and applies the remaining fields. UX_WebsiteContent_Slug is the concurrent final guard; known violations map to Website.Content.SlugExists.

Slug is immutable after creation. It accepts 1..160 lowercase ASCII letters/digits and nonconsecutive hyphens, never leading/trailing.

Create a draft
// ① The endpoint accepts no ContentId; the handler canonicalizes Slug.
var command = new CreateContentCommand(
title, slug, body, summary, category, tags, author,
coverImage, seoTitle, seoDescription, seoKeywords,
canonicalUrl, openGraphImage, sortWeight);
// ② The handler sanitizes Body before aggregate validation.
Result<ContentManagementDetails> created =
await mediator.Send(command, cancellationToken);
// ③ A precheck never replaces the database unique constraint.
return created;

4. HTML allowlist

The sanitizer tokenizes character by character rather than parsing HTML with regex. It preserves selected typography, links, images, and tables; only href/src/alt/title remain, and URL attributes allow local absolute paths or HTTP(S).

Script, style, iframe, object, embed, form, SVG, MathML, and template content is removed. Events, style, data/javascript schemes, and protocol-relative URLs are rejected.

It is not a full browser parser. GA still needs an OWASP corpus, malformed nesting, entities, Unicode, browser-differential tests, and CSP defense in depth.

5. Field boundaries

Body is 500,000 characters; title 200, summary 500, category 80, author 120; at most 20 tags of 50 characters. Tags are trimmed and deduplicated case-insensitively.

CoverImage, CanonicalUrl, and OpenGraphImage receive length/trim only—no scheme, same-origin, ownership, or SSRF policy. Add an Application URL policy.

6. Management authorization

The management group requires authentication. Create/update/schedule/publish/unpublish/archive require website.content.manage; detail requires website.content.read. Body limits and read/command timeouts are explicit.

There is no approval, revision history, preview token, or optimistic concurrency version. Concurrent updates are last-write-wins.

7. Public reads and feed

Public detail, list, and feed stores select Published only. The feed returns 20 recent entries and host code builds Atom XML. Link uses CanonicalUrl or /content/{slug}.

CanonicalUrl can currently be external. That is content metadata, not a safe-navigation guarantee.

8. Output cache

Public content, feed, and sitemap share WebsitePublicContent output policy/tag. Publish/Unpublish save and then emit ContentPublicationChangedIntegrationEvent; API consumers evict the tag.

If event publication fails after state commit, the client may see failure while visibility changed; retrying Publish then conflicts. GA must prove transactional outbox semantics and stale-window SLO under broker delay.

9. Sitemap

Only Published entries appear. A shard holds 50,000 URLs and an index 50,000 shards. Base URL must be HTTP(S) and is reduced to authority; external Canonical falls back to the local path; XML text is escaped.

Offset paging can drift under concurrent publication at large scale. Test stable ordering/snapshot semantics, not only the 50,001 boundary.

10. Publish example

Publish and propagate visibility
// ① Validate Draft/Scheduled/Unpublished → Published and persist.
Result<Content> result = await ContentCommandSupport.MutateAndGetAsync(
store, contentId, content => content.Publish(clock.UtcNow), cancellationToken);
if (result.IsFailure) return Result.Failure(result.Error);
// ② Emit the stable fact consumed by cache and sitemap paths.
Content content = result.Value!;
await events.PublishAsync(new ContentPublicationChangedIntegrationEvent(
content.Id, content.Slug, true, clock.UtcNow), cancellationToken);
// ③ GA requires proof that state and event cannot diverge permanently.
return Result.Success();

11. Test matrix

Cover every transition, past schedule, concurrent Slug, unrelated unique errors, large fields/tags, sanitizer bypasses, Published-only reads, publication event, cross-instance cache, Atom XML, sitemap shard/external canonical/concurrent drift, dual ORM, and API permissions.

Keep a current contract proving due Scheduled content remains invisible. Replace it only when an automatic-publish job is implemented and tested.

12. Source checks

Terminal window
# ① State transitions and published predicates.
rg -n "Schedule\(|Publish\(|Unpublish\(|Archive\(|PublishStatus\.Published" src/Platform/Website -g '*.cs'
# ② Cache tag and publication-event consumers.
rg -n "WebsitePublicContent|ContentPublicationChanged" src/Hosts src/Platform/Website -g '*.cs'

13. Publication review checklist

  • Does Slug require localization, redirect, and history?
  • Which node, timezone, and compensation own Scheduled execution?
  • Does auto-publish reuse the same aggregate method/event?
  • Does editing need ETag or row version?
  • Must an edit to Published create a revision?
  • Does sanitizer upgrade rewrite historic body content?
  • Do canonical/cover/OpenGraph URLs pass one policy?
  • Are feed ID, author, and absolute links configurable?
  • How is a lost cache event repaired?
  • Is sitemap ordering stable on both ORMs?
  • Does large sitemap generation need snapshot/keyset paging?
  • How do archive/delete and search-engine removal coordinate?
  • Are approval, audit, and publisher identity sufficient?

Back to Website

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%