The homepage only displays articles that have already been published, while the RSS feed retrieves the entire content collection. This discrepancy is difficult to detect during a manual review of the page: there are no draft links in the navigation, but the RSS reader already has the titles, summaries, and even the full text. The public boundary should be defined by the data layer, not assigned separately to each page.
Assume the site is a static blog built on a schedule, with articles containing draft and a clearly defined publication date. All public exports should consume the same getPublishedArticles(), including detail routes, lists, tags, search indexes, RSS feeds, and sitemaps.
Let feeds consume records whose publication status is already decided
The RSS generator should receive publicly available records, not decide on its own which ones to publish based on the original drafts. The upstream should handle identity, approval, and scheduling, while the downstream is responsible for converting titles, addresses, and times, along with summaries, into a subscription format. This way, the RSS feed doesn’t need to understand the status of private drafts, and it won’t miss filtering when a new field is added. The function name “Published” is not sufficient; testing should demonstrate that its return value does not contain unpublished records.
Assume that two articles are published every morning at 9:00 AM. The scheduling system generates and publishes the articles at 9:00 AM, while simultaneously reading these articles and other existing articles. RSS does not need to select articles marked as “time expired” from a private directory, because expiration does not necessarily mean that the review process is still valid. The conversion only occurs after approval, content fingerprint, and scheduling conditions are met, which prevents editors from making late-night changes to articles that are then automatically published the next day with the old approval.
Even when the build fails, it’s crucial not to allow RSS updates first, followed by waiting for the detail page deployment. Instead, the list, detail, and RSS should be part of a single, complete product, and versioning capabilities provided by the platform should be used to switch between versions. Whether a complete switch is possible depends on the specific hosting platform, and it cannot be assumed that all storage and caching are inherently atomic. Browser and edge cached versions may still exist temporarily. The system that generates RSS at runtime requires a stable, published version identifier, and it should not read the database when only half of the data has been written. The focus here is on publishing consistency. Correctly formatted XML cannot guarantee that the page referenced by an item actually exists.
type PublicationRecord = {
data: { draft: boolean; publishedAt: string };
};
export function publishedArticles<T extends PublicationRecord>(items: T[], now: Date): T[] {
const cutoff = now.getTime();
return items.filter(({ data }) => {
const timestamp = Date.parse(data.publishedAt);
return data.draft === false
&& Number.isFinite(timestamp)
&& timestamp <= cutoff;
});
}
This code assumes that publishedAt contains a clear time zone, such as 2026-10-12T09:00:00+08:00. The standard ISO date-only format YYYY-MM-DD is interpreted by Date.parse as UTC midnight, and does not change with the local time zone of the machine it is running on. However, it cannot express “9:00 AM in Hong Kong.” Dates and times without offsets are interpreted according to the local time zone and are affected by the machine’s settings. First, strict schema validation is performed to reject these two inputs that do not comply with the release time protocol. Then, public filtering is performed, which cannot silently interpret invalid times as the current time. Date.parse only converts validated records and does not perform strict format and valid calendar validation.
When building begins, create a now once and pass it to all outputs. Otherwise, if the boundary is defined during the build process, different pages may contain different articles. More importantly, static deployment will not automatically update due to expiration, and a scheduled build and deployment mechanism is required. The filtering conditions are merely a necessary condition, not a scheduler.
The RSS feed’s stable identifier should not change with the title.
The GUID in RSS 2.0 can use a permanent link or a stable, opaque value. When using non-URL identifiers, it is important to clearly indicate isPermaLink="false" according to the specified format, and not to disguise internal numbers as accessible addresses. Instead of simply viewing the incoming ID, it is crucial to verify that the parsing generator actually outputs the correct fields. Using the same identifier for both the link and the GUID is a valid option, but it is not a mandatory requirement for all subscription formats.
Summary subscriptions and full-text subscriptions offer different trade-offs. A summary subscription provides a simpler rendering of the main content, while a full-text subscription allows for more complete offline reading. When using full-text, you need to handle relative image URLs, code blocks, and HTML cleaning. You cannot directly insert scripts or interactive components from the editor preview into the XML. RSS parsing is strict, and unescaped characters can render the entire source invalid.
The GUID determines how a reader identifies the same article; publication time records its first release, update time records a revision, and list order determines its position in the feed. Keep these four concepts separate. Updating a citation may justify changing the body, but whether that revision should notify readers is a feed-policy decision. Resetting the publication time for every edit can make a minor correction look like a new article and destroy the reader’s sense of chronology.
When using a URL as an identifier, domain migration can cause subscribers to re-identify items. A stable internal identifier can reduce this risk, but it must comply with the format and output conventions used by the generator, and not rely on file sorting. Before migration, you can verify the reader’s behavior in a test source. If necessary, you can accept the reality that some clients will re-index partially, and you cannot claim that a particular identifier can control the implementation of all subscribers.
Sorting needs a stable secondary key. When two articles share the same timestamp, sorting by a stable identifier prevents their order from changing between builds. When a site emits only the newest entries, unstable ordering near the cutoff can make articles repeatedly enter and leave the feed. Limiting the number of entries controls download size, but older articles should remain accessible on the site; RSS is not the complete content database.
Full-text subscriptions require their own rendering boundaries.
Markdown, when rendered within a website, does not automatically translate to content that can be directly inserted into an RSS feed. Interactive components rely on scripts, and the RSS subscriber may not have an execution environment. Relative image paths depend on the website’s current location, while the subscriber may display incorrect source information. Raw HTML, if not cleaned, can also allow untrusted input to affect the reader. The output should use a clearly defined set of static elements, preserving text, links, images, and code, while rejecting unnecessary executable content.
Link conversion should resolve against the article’s absolute URL, not the RSS document URL. For example, resolving ./diagram.png from an article body against /feed.xml would incorrectly point into the site’s root directory. The renderer should handle images and ordinary links, reject unsupported protocols, and rewrite both src and href outside code blocks. When an external resource fails, its alternative text should still convey the image’s information.
Code blocks can retain language markers and readable whitespace, but should not rely on website highlighting scripts. Complex tables may render differently across different readers, and key conclusions should not be hidden solely within color or interactive expansions. Therefore, full-text subscriptions should include at least one genuine reader or a static rendering preview, not just successful XML parsing. If the site uses a summary mode, the summary should have independent value, rather than simply ending at the 100th character after extracting text.
Deleting an article doesn’t mean the file is permanently deleted.
After an article is removed from a feed, its detail page may remain in a stale deployment artifact, and subscribers may already have cached the content. The publishing system should deploy a complete build artifact instead of merely uploading new files. For sensitive information, assume that anything made public may have been copied; removing every copy cannot be guaranteed.
The RSS feed’s cache lifetime also affects when readers see updates. Response headers can allow reasonable caching, but emergency withdrawal needs a defined cache-purge mechanism. Do not assume that updating the feed, CDN, and client caches at the same time makes every copy change immediately.
Considerations for Cache Validation Based on Reader Fetch Behavior
After a site deployment, a reader may not have fetched the feed again; that is normal client behavior. The server can provide a validator for conditional requests, reducing bandwidth while the feed is unchanged. The validator must change whenever the feed changes. If the ETag reflects only the template’s modification time and ignores the article collection, the reader may receive an incorrect “not modified” response. The validator should reflect the final output bytes or an equally reliable content version.
Testing Preparation:
- Initial request
- Request with no changes
- Request after adding an article
- Request after withdrawing an article
Check status codes and content. Also, verify whether the CDN cache allows for timely revalidation.
The emergency withdrawal process should clear controlled caches for both the detail page and the feed, but it cannot guarantee deletion of content already stored by clients. State this limitation explicitly instead of implying that withdrawal can erase every copy.
The sentinel string testing should also search for the search index, client-side scripts, generated logs, and source maps before compression. While the draft might not be in HTML, it could be serialized into browser assets by a full indexing sequence. A comprehensive negative check of the public output, combined with preventing private directories from participating in the public loader, provides a two-layered defense. Each new public endpoint should be included in this testing suite to prevent the release boundary from silently expanding as the site’s functionality grows.
Updating Subscription Sources: A Compatibility Change
Switching from the summary to the full text, or from the last ten entries to fifty, can all affect the number of downloads and user expectations. Before making any changes, it’s important to estimate the output volume, check how long code, embedded images, and special characters are displayed, and ensure that the formatting of the entries remains consistent. You should not force the reader to refresh by simply assigning new labels to all the old entries, as this could be misinterpreted as dozens of new articles, which would disrupt the user’s “unread” status.
Maintain a stable URL for the same source. If the address needs to be moved, provide a clear redirect and retain the old URL for an extended period, and test the reader to ensure it follows. Also, update links found within the site to allow the browser and subscription tool to find the new source. The subscription button on the page and the auto-discovery tag in the HTML are two entry points. If the latter is missing, the reader may still use the old address. Using the correct media type can reduce the risk of misinterpretation by the tool, but it cannot fix damaged XML.
The language information should correspond to the actual content. Multilingual sites can provide independent language sources or explicitly define mixed sources. Do not suddenly load all Chinese drafts into the English source simply because they share a common collection. Language filtering must occur after the collection has been publicly released. First, isolate the content based on its publication status, and then select the desired language. This ensures that adding a new language does not change the fundamental judgment of whether private content is publicly available.
Compatibility testing involves maintaining a set of published samples, comparing the identifiers, timestamps, order, and links before and after the upgrade. Changes in the document format can be intentional, but any changes in identity should be clearly documented. Since the reader cache is not fully controlled by the server, the focus should be on maintaining stable contracts and clear migration, rather than forcing all clients to refresh at the same time.
Explaining the Coherence of Two Articles’ Publication Dates Using an Event Timeline
Imagine that the nine o’clock task converts articles A and B into publicly accessible inputs. However, the task fails to complete at 9:01. The private queue cannot immediately record “already published,” otherwise the next task will skip articles that have not yet been published. It also cannot arbitrarily re-select the other two articles without any record, which would keep this batch in an intermediate state indefinitely. It is necessary to distinguish between selecting, converting, building, and deploying/accepting as recoverable states, and to provide a stable identifier for this batch. Status recording does not mean that each step will automatically retry; when the external deployment result is unknown, first check the actual version.
RSS is one of the outputs within this category. If the deployment platform returns a timeout, the actual publication may have already been successful. Instead of directly re-publishing and rewriting the date, it should be confirmed through a non-sensitive version list and a public address. Retrying the same batch should maintain the target ID and the expected time of the initial publication. The same article should not be treated as a new news item simply because the execution time has changed. Stable batches can be traced, but they cannot replace the platform’s ability to query the deployment status.
Once the build is successful, first verify the details and corresponding entries in the preview output against the source. Then, publish and verify the actual entries. Caching may temporarily cause the source and details to belong to different versions, so acceptance requires understanding the boundaries between platform switching and cache propagation. Instead of indefinitely waiting and then recording success, you can retry reading within a limited time, and report an error if the deadline is exceeded. If the source already contains new entries but the details are missing, prioritize fixing the public consistency issue, rather than adding the next batch of articles to cover up the problem.
The batch logs only store the stable article ID, version, and stage. Private content is not logged publicly. The recovery tool should clearly show what it will do and verify that the input fingerprint has not changed. If the author continues to modify the content during a failed attempt, the same batch should not be able to silently use the new content to reuse previously approved content. This constraint is particularly important in daily tasks, as unattended mode should not mean bypassing the editing evidence.
Separate XML Validity and HTML Security Validation
When the title contains A & B, the XML serializer needs to correctly escape it; when the body contains HTML, it’s also necessary to decide how the subscription format will handle the content. XML escaping addresses issues with document structure, while HTML sanitization addresses issues with executable or dangerous content. These are not the same task. Simply replacing a few characters in a string can lead to double escaping, or it may not create a safe element set. Prioritize using mature RSS generators and sanitizers that are suitable for the output format.
The code example itself may contain literal script tag text. The cleaning process should preserve the code that the reader is currently reading, rather than treating it as a valid, executable node. Conversely, the original HTML of Markdown should not be automatically trusted simply because it appears in the author’s file. The rendering order must be clearly defined: first, convert the Markdown according to the agreed-upon standard, then process any allowed HTML and link protocols, and finally, serialize the XML using a tool. If CDATA is used, the tool must also handle its special closing sequence, and it should not be assumed that it can contain any arbitrary original text.
The test samples cover angle brackets, ampersands, quotes, non-ASCII characters, relative links, and code delimiters, and then parse the output back into fields. The semantic content is then compared. Simply asserting that XML does not error will miss the issue of text that is displayed as entities after being doubly escaped. Safe samples use synthetically created dangerous tags to confirm that the output does not retain any executable functionality. Do not run uncleaned private articles in a real reader to “see what happens.”
The images also introduce access and privacy boundaries. External images may initiate requests to third parties when a user opens an item, and whether the images are loaded is determined by the reader’s policy. Websites can reduce unnecessary external resources and provide meaningful alternative text, but they cannot guarantee that all clients will block tracking. Image URLs that require login should not be accessible from public sources; similarly, signed image URLs may expose access credentials or become invalid quickly, making them unsuitable for permanent subscriptions.
Establish a realistic delivery budget for the feed
The original text has been expanded from ten to fifty articles, and the download volume is not simply five times larger due to differences in article length, image representation, and code density. The budget should be based on the final serialized byte size, including the volume before and after compression, and should separately observe the size of the static output and the actual transmission volume. Frequent retrieval, compression, and conditional requests can reduce costs, but it should not be assumed that all clients correctly use the validator.
You can limit the number of recent items or the time window, while maintaining stable sorting. When choosing between full text and summary, consider the offline reading value and delivery cost, rather than treating full text as the only premium option. If providing two sources, the address and description should clearly distinguish between them, and the item identifiers should remain stable within their respective contracts. Avoid repeatedly switching strategies on the same URL, so that the client does not know whether the old items still represent the same content.
The source generation should not repeatedly read private data for each request. Static blogs can generate public XML during build time, while runtime systems read from a clearly defined public version, limiting generation costs. When the cache expires, a large number of readers simultaneously fetch from the source, requiring consideration of source generation latency and server capacity. Generating and hosting static XML is generally easier to predict, but it’s still necessary to monitor loading errors, unexpected empty entries, and unusual volumes.
As the number of articles increases, a dedicated archive page can be maintained to help readers find older content without forcing RSS to include the entire site history. The subscription feed is not a backup, nor is it an export of the article database. When historical articles are removed from the “Recent” feed, the corresponding links should still be accessible according to the site’s long-term maintenance rules. This separation of budget and content preservation responsibilities prevents the adjustment of the “Recent” window from being misinterpreted as the deletion of articles.
When a leak occurs, immediately stop any new public exports.
If a sentinel check detects a private summary entering the source, first stop the problematic build or publish path, and then clear the controlled source and details cache. During investigation, confirm the scope of the leak: does it only contain the title, the summary, or the full content? Does it also enter the search index and client assets? Do not copy the complete private collection again to a public CI solely for the purpose of locating the issue. Use a minimized synthetic sample to reproduce the error, while retaining the necessary versions and timestamps.
After fixing the issue, perform comprehensive negative testing on all outgoing paths, rather than simply adding a filtering condition to RSS. The root cause may be an incorrect interpretation of the state of the publicly available collection, and local filtering may leave other leakage paths open. While content that has been publicly disclosed cannot be guaranteed to be completely removed, it can be stopped from further propagation, isolated, and the scope of control can be clearly stated. An incident involving a security boundary should not be downplayed by saying “there are no links on the page.”
Use sentinel articles for negative acceptance tests
Create three records specifically for testing: “Published,” “Private Draft,” and “Future Publication Date.” Give each unpublished record a unique sentinel string. After building, search all of dist and confirm that its title, body, and slug are absent. Then parse the RSS feed and verify that it contains only published records with URLs on the production domain.
The tests should cover XML parsing, unique GUIDs, valid dates, and reachable links. Perform equivalent leakage checks on tag pages and the search index. The most valuable automated test is not “RSS returns 200”, but “private content does not exist in any output.”
When the reader doesn’t have new items, first check if the server provides a new version, then check if the XML is parsable, and finally check when the client pulls the items. These three steps have different responsibilities, and repeatedly republishing articles can create more duplicate items. The logs can record the source version, the number of items, and the generation time, without needing to record the full text. If the number of items generated suddenly becomes zero, it should be handled as a deployment failure or require confirmation, to avoid mistaking a loading error for a normal clearing.
When using public test subscription sources, only include synthetic articles, not real private content. Test files should also include unique markers and verify that the production build does not contain them. This allows you to test special characters, long titles, and image URLs without circumventing the original isolation requirements for debugging the subscription. RSS is a continuously maintained interface. When changing summary strategies, identifier rules, or time fields, you should check with existing consumers to see what changes they will receive, just as you would with an API.
References: Astro RSS, RSS 2.0 Specification.