The Server Managing Public Copy and Data
The server checks publication status, publication time, and revision before returning material. Connect queries and publication so that the body, metadata, and search material use the same published revision.
Store and query public copy in servers, databases, and a CMS
Suppose the application deadline changes from November 5 to 6, but only the body changes while the search summary still says 5. Search and answer tools receive different deadlines depending on what they read. Establishing one published revision and generating body, summary, and dates together reduces these conflicts. The CMS’s storage and publication structure determines the facts delivered externally.
A server means a program that receives network requests and responds, as well as the equipment running it. A web server delivers files or forwards requests to an application; an application server runs service logic such as database queries and HTML generation. Managed hosting may provide both roles together.
A DB (Database) stores articles, publication states, dates, and relationships. A CMS (Content Management System) provides management features to edit and publish this material. A database schema defines fields, data types, and relationships. SQL queries and modifies relational databases; an ORM connects code objects with database records. Whatever the tool, include publication state and timing in query conditions.
A database index is a data structure for finding rows quickly; its role differs from a search-engine index. Choose indexes using actual execution plans for queries by slug or publication state and date. Repeating unnecessary full-table queries on every page increases SSR response time and database load.
A transaction groups database changes so they succeed or roll back together. A revision is a version of the copy. Switching body, title, and dates together when changing the published revision reduces mismatches between screen content and metadata. Record delivery to an external search repository as a separate job and retry failures.
Authentication verifies identity; authorization checks which material that person can use. A session maintains login state, and a cookie stores a value the browser can send back to the server. Public articles should be readable without login; draft and administrator queries require server-side authentication and authorization. If the same URL returns user-specific content, prevent it from mixing in shared caches.
Define publication eligibility in one place
Apply publication conditions in the server query layer. The TypeScript below imports isPublic from identity-time.mjs in Documents and time. Save that file as lib/identity-time.mjs. Date.parse may normalize February 30 to another date, so successful numeric conversion alone does not validate a calendar date.
import { isPublic } from './identity-time.mjs'
type Post = {
status: 'draft' | 'published' | 'withdrawn'
publishedAt: string | null // Known first publication: Date or DateTime
publicFrom: string | null // Exact time public access begins on this server
deletedAt: string | null
}
export function publicPosts(posts: Post[], now = Date.now()) {
return posts.filter(post => isPublic(post, now))
}
type Post defines input fields and types. publishedAt is the original publication date; only the date may be known, or it may be unknown. publicFrom is the exact time this server starts public access. Separating them preserves a historical publication date when importing material today. isPublic validates date formats, then checks published status, absence of deletion, and whether publicFrom has been reached. Invalid input is reported as an error; valid drafts are excluded from lists.
For example, define the conditions in src/lib/public-content.ts and use equivalent conditions for lists, details, sitemaps, and public APIs. Apply them in SQL too when filtering in the database. Do not send all records to the client for filtering. For republishing, withdrawal, or scheduled publication, group cache updates so lists and details change together after transitions.
Determine public eligibility first, then apply search-inclusion policy. On a site where public articles are searchable, lists, details, and sitemaps should read the same published revision. Check login state and access permissions on the server for administrator views and drafts.
Shared publication conditions align addresses announced to search engines with articles that actually open. If a list considers an article public and links it, but the detail page treats it as private and returns 404, the discovered link does not lead to a readable document. Adding scheduled articles to a sitemap before publication has the same problem. isPublic makes each route answer using the same time and status criteria.
Generate body and derived data together from the published revision
Separate working copy from currently published copy in a CMS-backed site. Public detail pages, metadata, and sitemaps read the published revision; administrator previews read the working draft. Draft changes must not leak into the public screen on each request to the same URL.
For example, content_id identifies the document and revision its content version. Generate the body, title, summary, sources, sharing image, and search data from the revision to publish, then switch the active published version. Updates within one database can be transactional. For external search storage, record a synchronization job with publication and separately store the applied revision, failures, and retry state.
Send the published body and sources to internal search or the site chatbot. Search the same sentence immediately after publication to check that new content returns. Withdrawn articles must also disappear from search results and material available to the chatbot.
On withdrawal, change availability of the detail text, lists, sitemap, related articles, internal search, and chatbot together, then invalidate relevant caches. On restoration, check that the previous revision’s sources and media can still be public. Restoring an old revision must not expose rejected or blocked original material again.
Store a request ID and expected revision to distinguish retries of the same publication. If publish-42 changes revision 7 to 8 but response delivery fails, return the already-created revision 8 when publish-42 is retried. If another request has already modified revision 8, an attempt to save with expected revision 7 returns a conflict. This prevents network retries from publishing twice or overwriting another edit.
If internal RAG keeps retrieving the previous revision, the chatbot may answer with an old deadline even after the screen is corrected. Storing the publication revision with search material makes the retrieved copy traceable. Compare the retrieved chunk’s revision with the public page’s revision and fix synchronization if they differ. Check material stored by external search and AI through those services’ recrawling and processing results.
Handle failures between publication and external updates
Switching the published database version and updating an external CDN occur in separate systems. If only the database changes and the update request fails, some paths retain old content. The publication transaction can also store a job containing document ID, version, and target paths, with a handler recording success and retries.
When the same publication job returns, use document ID and expected version to check whether it was already applied. Edits switch to a new published revision; withdrawal stops returning the document through every query path. With caching, also inspect paths that return stored responses before querying the origin.
Pass the public copy obtained in one request to the body and metadata queried together. Calling a function with the same name twice still allows the published version to change between calls if each reads the database. The next implementation example reuses query results within a request and chooses long-term caching separately.
Preserve both document revisions and the period in which facts apply
The content ID identifies the document; revision identifies an edited version; publishedAt is the first publication time; contentModifiedAt is the substantive modification time. Resolve the active revision before passing the same data to body, metadata, and lists. Do not overwrite a published revision’s modification date with the working draft’s updatedAt.
Correcting a typo in a 2025 record in 2026 increases its revision but leaves its validity period in 2025. Documents and time models this distinction by storing publication state and validity period separately.
Run publication, editing, withdrawal, and restoration in a small CMS
Save lifecycle.mjs and lifecycle.test.mjs beside identity-time.mjs and run node --test lifecycle.test.mjs. Data stays in memory and a local HTTP server returns JSON. /list returns the list, /sitemap the addresses for the sitemap, and /guides/opening-hours the detail data.
// Node.js 22. Standalone in-memory CMS exercise; data disappears on shutdown.
import { isPublic, describeDocument } from './identity-time.mjs';
export function createLibrary() {
let draft = { id: 'hours-current', path: '/guides/opening-hours', revision: 0,
title: 'Library opening hours', text: 'Sundays 10–17. Closed on the first Sunday and public holidays.',
status: 'draft', deletedAt: null, publishedAt: null, publicFrom: null,
contentModifiedAt: null, eventDate: null,
validFrom: '2026-10-01T00:00:00+09:00', validUntil: null };
let active = structuredClone(draft);
const history = [], cache = new Map();
function saveDraft(text) { draft = { ...draft, text }; }
function change(action, at, { purge = true, revision } = {}) {
if (!['publish', 'withdraw', 'restore'].includes(action)) throw new Error('Invalid action');
const selected = action === 'restore' ? history.find(x => x.revision === revision) : draft;
if (!selected) throw new Error('Unknown revision');
const next = action === 'withdraw' ? { ...active, status: 'withdrawn' }
: { ...selected, status: 'published',
publishedAt: active.publishedAt ?? at, publicFrom: at,
contentModifiedAt: active.publishedAt === null ? null : at };
next.revision = active.revision + 1;
describeDocument(next, at); // Switch in one operation after validating all input.
active = next; history.push(structuredClone(next));
draft = structuredClone(next);
if (purge) cache.clear();
return next.revision;
}
function read(path, at, { bypass = false } = {}) {
// Withdrawal in this model rechecks the current state on detail requests.
if (path === active.path && !isPublic(active, Date.parse(at)))
return { status: 404, body: null };
if (!bypass && cache.has(path)) return structuredClone(cache.get(path));
const info = describeDocument(active, at);
const body = path === '/list' ? (info ? [{ id: active.id, title: active.title, revision: active.revision }] : [])
: path === '/sitemap' ? (info ? [info.canonical] : [])
: path === active.path && info ? { ...active, ...info } : null;
const result = { status: body === null ? 404 : 200, body };
if (!bypass) cache.set(path, result);
return structuredClone(result);
}
return { saveDraft, change, read, clearCache: () => cache.clear() };
}
saveDraft changes only the working draft. change selects publication, withdrawal, or restoration and assigns fully date-validated data to active in one operation. history preserves copies of previous published revisions. Restoration publishes old content as a new revision while keeping the first publication date. The restored public content records the current modification time.
read builds details, lists, and sitemap from the same active object. Detail requests recheck current publication eligibility before reading the cache. By default, change clears every exercise cache. Real databases and CDNs do not change atomically in one memory space, so production needs failure records and retries for cache updates.
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { createLibrary } from './lifecycle.mjs';
const at = '2026-10-24T09:00:00+09:00';
const later = '2026-10-24T10:00:00+09:00';
test('Compare draft→publish→edit→withdraw→restore through HTTP queries', async () => {
const cms = createLibrary(); let clock = at;
const server = createServer((req, res) => {
const result = cms.read(req.url, clock);
res.writeHead(result.status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result.body));
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
const rows = [];
async function inspect(state, revision) {
const responses = await Promise.all(['/guides/opening-hours', '/list', '/sitemap'].map(async path => {
const r = await fetch(origin + path); return { path, status: r.status, body: await r.json() };
}));
const [detail, list, sitemap] = responses;
assert.equal(detail.status, revision === null ? 404 : 200);
assert.equal(list.body.length, revision === null ? 0 : 1);
assert.equal(sitemap.body.length, revision === null ? 0 : 1);
if (revision !== null) {
assert.equal(detail.body.revision, revision); assert.equal(list.body[0].revision, revision);
assert.equal(sitemap.body[0], detail.body.canonical);
}
rows.push({ state, responses }); return detail.body;
}
try {
await inspect('draft', null);
cms.change('publish', at); const first = await inspect('published', 1);
cms.saveDraft('Sundays 10–16. Closed on the first Sunday and public holidays.');
assert.equal((await inspect('draft edit remains private', 1)).text, first.text);
clock = later; cms.change('publish', later); const revised = await inspect('revised', 2);
assert.match(revised.text, /10–16/); assert.equal(revised.publishedAt, at);
cms.change('withdraw', later); await inspect('withdrawn', null);
cms.change('restore', later, { revision: 1 }); const restored = await inspect('restored', 4);
assert.equal(restored.text, first.text); assert.equal(restored.publishedAt, at);
assert.equal(restored.contentModifiedAt, later);
console.log(JSON.stringify(rows));
} finally { await new Promise(resolve => server.close(resolve)); }
});
test('Reproduce edit and withdrawal failures without invalidation, then clear all caches', () => {
const cms = createLibrary(); cms.change('publish', at);
for (const path of ['/guides/opening-hours', '/list', '/sitemap']) cms.read(path, at);
cms.saveDraft('New opening hours'); cms.change('publish', later, { purge: false });
assert.notEqual(cms.read('/guides/opening-hours', later).body.text,
cms.read('/guides/opening-hours', later, { bypass: true }).body.text);
cms.change('withdraw', later, { purge: false });
assert.equal(cms.read('/guides/opening-hours', later).status, 404);
assert.equal(cms.read('/list', later).body.length, 1); // Incorrectly retained list
assert.equal(cms.read('/sitemap', later).body.length, 1);
cms.clearCache();
assert.deepEqual(cms.read('/list', later).body, []);
assert.deepEqual(cms.read('/sitemap', later).body, []);
});
The first test starts with an empty list and detail 404, then checks 200 after publication, private draft edits, a new published body, 404 and empty list and sitemap after withdrawal, and restoration of old content. The second injects failure with purge: false. It reproduces the detail being withdrawn while list and sitemap return stale caches. After clearCache, it verifies that both routes are empty to check the scope of the fix.
A list cache returning links after withdrawal represents search crawlers continuing to discover articles no longer served. Once caches are cleared and addresses disappear from list and sitemap, newly supplied discovery paths are corrected. Material already stored in external search must be checked in subsequent crawl records. Code-level publication decisions and external material updates happen in these separate places.
Reuse image conversions after checking public availability
A CMS photo ID is a key for finding its file path and publication status. A real CMS checks whether a currently public article references it, its publication time has arrived, and it has been withdrawn. If only the article is withdrawn while the original file address stays open, search, sharing, and direct requests can still reach the file. Both original and derivative routes must pass the same publication check.
The following media-store.mjs keeps one photo and its publication state in memory. After checking public availability, it finds conversions by source hash, width, and WebP settings. Concurrent requests for the same width await the same Promise in a Map. Publication state and generation number are checked again after conversion, blocking requests withdrawn while waiting.
import sharp from 'sharp';
import { createHash } from 'node:crypto';
// Single-image exercise store. Do not write originals or conversions to the public directory.
export async function createMediaStore(input) {
const source = Buffer.from(input);
if (source.length > 20 * 1024 * 1024) throw new Error('Image exceeds 20 MiB');
const options = { limitInputPixels: 40_000_000 };
const meta = await sharp(source, options).metadata();
if (!['jpeg', 'png', 'webp'].includes(meta.format) || (meta.pages ?? 1) !== 1)
throw new Error('Single JPEG, PNG or WebP required');
const hash = createHash('sha256').update(source).digest('hex');
const variants = new Map();
let published = true;
let generation = 0;
let transforms = 0;
const headers = { 'Cache-Control': 'private, no-store, max-age=0' };
const missing = () => ({ status: 404, headers, body: Buffer.from('Not found') });
return {
setPublished(value) {
if (typeof value !== 'boolean') throw new Error('Boolean required');
published = value; generation++;
},
stats: () => ({ transforms, variants: variants.size }),
async read(width = null) {
if (!published) return missing();
const observedGeneration = generation;
if (width === null) return {
status: 200, headers: { ...headers, 'Content-Type': 'image/' + meta.format }, body: source,
};
if (![320, 640, 1280].includes(width))
return { status: 400, headers, body: Buffer.from('Unsupported width') };
const key = `${hash}:${width}:webp-q78-v1`;
if (!variants.has(key)) {
transforms++;
const pending = sharp(source, options).rotate()
.resize({ width, withoutEnlargement: true }).webp({ quality: 78 })
.toBuffer({ resolveWithObject: true });
variants.set(key, pending);
pending.catch(() => variants.delete(key));
}
const result = await variants.get(key);
// Also stop requests withdrawn or republished while awaiting conversion.
if (!published || generation !== observedGeneration) return missing();
return { status: 200, headers: { ...headers, 'Content-Type': 'image/webp' },
body: result.data, width: result.info.width, height: result.info.height };
},
};
}
Buffer.from copies input bytes; a SHA-256 hash identifies the source content. rotate applies image orientation. resize’s withoutEnlargement prevents forced enlargement of small originals. toBuffer’s info gives actual output width and height. Use those values in srcset to avoid incorrectly labeling a small original as 1280w. sharp resizing
setPublished(false) is withdrawal in this exercise. Restarting the server removes originals and derivatives. When connecting a CMS, replace the published variable with a published-revision query and keep derived files in storage inaccessible directly from outside. Multiple servers must read the same publication state. Input-size, pixel-count, and format checks are included here; add time and concurrency limits in the environment running uploads and conversions.
This example also exposes the original address, so decide first whether to provide embedded location and capture information. If the service offers only metadata-stripped public derivatives, restrict the original route to administrators. Files already delivered and responses already in transit cannot be recalled by a later withdrawal. Distinguish access conditions for new requests from handling material already distributed.
Run this file with the server in the image exercise. Tests in the verification chapter reproduce withdrawal during conversion and requests for stored derivatives.