All notes

Catch Editorial Errors Before Building with Content Schemas

An executable metadata contract for Astro articles, without confusing valid structure with trustworthy content.

English edition, edited for length. Read the complete Chinese original.

An article changes draft: false to draft: "false", yet the page may still render. In JavaScript, even the string "false" is truthy. If the filter is written carelessly, what the site publishes can differ from what the editor intended. A content-collection schema should stop this error before it reaches page code.

This discussion focuses on how the Astro content loader handles Markdown files. Due to differences in project versions, import paths and content APIs may vary. Therefore, always refer to the documentation for the specific version of your project that you are using. The goal is not to maximize the number of fields, but rather to ensure that the publishing conditions align with the actual dependencies of the page.

Validate the fields that actually drive publication decisions

import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const articles = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/articles' }),
  schema: z.object({
    title: z.string().trim().min(1),
    description: z.string().trim().min(20),
    draft: z.boolean().default(true),
    tags: z.array(z.string().trim().min(1)).max(5),
    publishedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  }),
});

export const collections = { articles };

By default, draft: true represents a conservative approach: missing fields will not be automatically revealed. While the minimum length requirement prevents empty summaries, it cannot guarantee that the summary is useful. The date regular expression only validates the format, and 2026-02-31 may still match, therefore, further validation is needed, either at the calendar level or through a date parsing strategy with a clearly defined time zone.

Do not make every field optional just to keep automated submissions passing. If a page requires a title and publication date, those fields should be required. Truly optional fields, such as a cover image, need a defined fallback layout rather than an undefined value leaking into a URL.

The simplified example previously requested that all records have publishedAt, which is suitable for a loader that only reads prepared articles, but not for a full-fledged editing system. Requiring the date to be filled in when the time is not yet determined can lead editors to enter false values. A more reasonable internal contract distinguishes between drafts and records ready for publication: drafts have titles, descriptions, and tags; records ready for publication also require a valid publication date, an audit indicator, and a stable slug. Do not confuse “not yet filled” with “format error” as the same type of failure.

This can be achieved by inferring the combined expression of these states, or by independently validating them during the publishing process. The former allows the type system to directly identify the presence of fields, but requires the editing tool to understand multiple record formats. The latter keeps private drafts simple, but places the responsibility for completing and validating on the publishing program. For personal blogs, generating public metadata during publishing is often more intuitive. Multi-user content platforms may, however, require explicit workflow states to avoid conflicts between editing, approval, and scheduling within a single boolean value.

The date constraints also need to be broken down into three levels. The first level is format validity, the second is whether the calendar date actually exists, and the third is whether the business time is reasonable. For example, a publication date that is later than the current time does not necessarily invalidate it; it may simply be a scheduling issue. However, an update date that is earlier than the publication date is usually something to reject. Validation should not delete all future dates, nor should it allow any arbitrary string to accommodate scheduling. During testing, fix the current time to avoid a scenario where a sample passes today but fails tomorrow.

Complete the conversion at the boundary, and the page only handles the standard form.

Assume that historical articles have tags that exist in both an array and a comma-separated string format. While directly allowing the schema to accept both types might seem convenient in the short term, it would essentially create a compatibility layer for every page. A more robust migration approach would involve identifying the old format when reading the original data, converting it into a unified array, and then applying the formal constraints. After the migration is complete, the support for the old input formats should be removed. This ensures that each page only receives one type, and that the migration process has a clear exit point.

The conversion must be visible. After splitting into an empty string, an empty array should be reported, rather than silently inserting a “Other” tag. Case normalization may also merge originally different words, and a change log should be provided for editors to confirm. Default values are suitable for expressing intended default states, but should not be used to conceal missing evidence. Setting the default publication date to the build time is particularly dangerous, as rebuilding will make the same article appear to have just been published.

The image field can further express the asset contract: The local cover requires a valid alternative text, while the remote address requires allowed protocols. It is not sufficient to simply validate the address syntax to assume that the image can be downloaded, and it is also not appropriate to indefinitely wait for the remote server during construction. The existence of the asset, file size, and dimensions are independent checks that can be provided in a report together with metadata checks. Retaining the legal status of articles without covers is better than forcing authors to use meaningless decorative images.

Valid individual fields do not guarantee a valid collection

The schema is well-suited for validating individual articles, but it cannot naturally answer the question “Do two files use the same slug?”. Such cross-record constraints require additional checks. A build script can maintain a Map, outputting the two filenames when a duplicate slug is encountered, and failing the build. Labels can also use a canonical vocabulary to prevent Cloudflare, cloudflare, and CF from becoming three separate topics in the filter.

Another class of constraint concerns path safety. Restrict new slugs to lowercase ASCII letters, digits, and hyphens; reject path separators, leading or trailing whitespace, and reserved route names. Static sites may not face database injection here, but uncontrolled slugs can still create route collisions and unpredictable build output.

Using a single error report per small project is sufficient, but it can slow down the fix process when importing dozens of articles. Instead, first perform field validation on each file to collect errors. Then, only perform slug, path, and reference consistency checks on valid records. Finally, output a single, grouped diagnostic report per file, and let the build fail. This prevents false positives for dozens of path conflicts caused by a single invalid slug, and also prevents failed records from being mixed into subsequent rendering.

The error messages should clearly distinguish between “input value,” “expected rules,” and “suggested fixes.” For example, a date error should indicate that a time zone is required, while a label error should list the supported vocabulary. However, do not output the entire text or private summaries to public CI logs. Whether the log is public or whether a draft is published on the website are two distinct boundaries. A private writing directory should not be scanned and printed by a public pipeline.

We also need a clear strategy for handling unknown fields. Rejecting unknown fields can help identify errors like “publisedAt,” while allowing them can make it easier for independent tools to save extension information. There is no single, universally correct approach. If multiple people are editing and using multiple tools, using dedicated extension objects is easier to maintain than allowing any mis-typed fields to pass through unnoticed. The strictness of the schema should be determined by the author’s ability to understand and correct errors, and the ease with which the schema can be upgraded.

Isolation through page filtering alone is insufficient.

getCollection('articles', ({ data }) => !data.draft) allows you to hide drafts in the list, but if the getStaticPaths() on the detail page uses an unfiltered collection, the drafts will still have a directly accessible address. RSS, sitemap, tag pages, and internal search also need to use the same public collection.

Stronger isolation keeps private drafts out of the public content loader altogether: drafts remain outside the build input, and only approved files are copied during publication. The schema defines the contract for publication inputs; it is not an access-control system. If private content is present in the repository or deployment artifact, the absence of navigation links does not make it private.

Suppose an article was approved yesterday and its body changed today. Its metadata may still be valid, but the approval no longer applies to the current content. The publishing system can store a content fingerprint, recalculate it before publication, and require another review when it changes. A fingerprint answers “is this the same content?”, not “is this content correct?” or “who wrote it?” Its role is to prevent stale approvals from being reused accidentally.

When calculating fingerprints, define the scope clearly. If the description, title, and references all affect the public output, hashing only the body is insufficient. If the publication date is generated by the publishing process, record the internal approval object separately from the final public record. Do not omit code blocks merely to reduce re-review; changing one character in a technical article can change whether a command is destructive.

After the conversion is complete, run the public schema again to ensure that no fields are missing. Then, create a temporary directory. Only replace the official output after all checks have passed. This way, even if the original document is valid and the converter has defects, no incomplete articles or incomplete indexes will be left. Separate input validation, set validation, and output checking. This makes it clear which errors occurred, rather than all problems being labeled as “build failure.”

Data contracts must also be consistent with template dependencies.

One layout began to display reading time, but did not require content providers to provide reading time or a reliable calculation method. Even with a strict schema, it was impossible to prevent empty values from appearing on the page. Each time a new template dependency was introduced, a decision had to be made about where the data came from: from the editor, from a calculation, or from a runtime service. Calculated fields should be generated by the same build step to avoid discrepancies between the author’s manually entered word count and the actual text; fields that cannot be derived should be explicitly entered and validated.

Taking reading time as an example, the algorithm needs to specify how Chinese characters and English words are estimated, and whether code is included in the calculation. It provides a reading reference, not a guarantee that every reader will require the same amount of time. The results should not determine whether an article is factually correct, but can help identify unusually short articles. The test should involve mixed Chinese and English, code blocks, and empty text, to ensure that the results do not show one minute due to the counter missing Chinese characters.

The abstract is also part of the template contract. Automatically extracting the main text may contain Markdown tags, incomplete links, or uninterpreted code, which may be suitable for fallback purposes but not necessarily for search results. A human-written abstract should describe the problem the article solves and its scope of application. Verification can only confirm that it exists, has a reasonable length, and does not have obvious formatting residue. Do not equate a fixed minimum word count with information density, and do not skip the editorial review simply because the abstract is long enough.

Maintaining a list of required fields for a page is more effective than adding dozens of unused metadata elements. When deleting fields, check if RSS, search indexes, and structured data still depend on them. When adding fields, check if existing content needs to be migrated. Only when the type system’s constraints and output requirements change in sync, can it reduce real errors, rather than creating a false sense of “all objects have passed validation.”

Using a Single Import Task to Differentiate Original Data from Release Records

Assume you need to import twenty old website articles, with dates ranging from pure dates to dates with time zones and empty values, and tags that include both arrays and free-form text. First, save the original data and source identifiers without modifying them while parsing. Then, for each record, output the conversion results and diagnostics: which fields are preserved as is, which are converted according to defined rules, and which require manual decision-making. The original data allows for reproducing failures, and the conversion records allow the author to understand exactly what the system has changed.

Pure dates should not be silently converted to midnight in a specific time zone for scheduling purposes, as the author may only intend to indicate the calendar day the article belongs to. If the publishing system requires a specific time, it is necessary to clearly specify the site’s time zone and default time, and to indicate that the value comes from the rules rather than the original document in the import records. Time values that already include time zones should not be reinterpreted using the local machine’s time zone. Input, internal standard values, and display values should each have distinct responsibilities, and mixing them into a single string will cause the migration results to vary depending on the running machine.

The migration of “slug” values requires more external mapping. Old links may have already been referenced, and it’s not possible to simply rewrite them based on the new rule requiring lowercase hyphens without preserving the redirection plan. Each record should have the old address, the new address, and the result of any conflict resolution. When two old addresses are normalized to the same new address, stop and choose a stable identifier instead of adding numbers based on the import order. The latter approach, while seemingly functional, could lead to a different set of addresses if the file order is changed again.

Finally, the confirmed records should be transferred to the official schema, while unconfirmed records remain in the isolation area. The import tool should not force-mark erroneous objects as drafts and transfer them to the public loader solely to achieve the “twenty articles completed” goal. The “draft” status indicates an intention to edit, while the “isolated” status indicates a conversion failure. If these two statuses use the same field, subsequent publishing programs may mistakenly interpret the failed input as a normal article with only a date difference.

Diagnostic Sets Required for Contract Changes

Schema testing not only preserves valid samples but also preserves past error patterns that have actually occurred. This allows for the definition of stable code, such as cases where dates are invalid, paths conflict, or there is a lack of publication evidence. The instructions that users see can be improved, focusing on testing the code, files, and field paths. This prevents a large number of test failures when third-party validation libraries update their wording, as the tests will primarily assert the expected behavior.

The diagnosis also needs to include the severity of the issues. Missing essential headings should prevent publication; a summary that is too long can be flagged by the editor; whether the citations support the conclusions still needs to be manually verified. Do not combine these conclusions with a single “all clear” green light. When generating the report, clearly display the issues that need to be blocked, the recommendations, and the items that require manual confirmation. The absence of formatting errors does not mean the article is ready for publication, and manual approval should not bypass the hard constraints of the input structure.

For calendar validation, we use examples involving leap years, month boundaries, invalid dates, and specific time zones. For path validation, we use examples involving case differences, leading/trailing whitespace, conflicts between retained names and normalization, and changes in fingerprints. The purpose of negative testing is to demonstrate that the system rejects invalid inputs, rather than to prove that the code can find a fallback for any input.

The replay diagnostic set also aids in identifying upgrade dependencies. Instead of simply comparing the number of errors, run the old and new versions on the same set of inputs and compare the acceptance sets and standardized results. The new version reports three fewer errors, which may be due to improved rules or silent acceptance of an unsafe state. Clearly categorize each difference and then decide whether to migrate. Real private articles should not be copied into public test repositories; instead, use a minimal and anonymized sample to reproduce the structural issues.

Verify Content Projection After Generating the Output

A validated public record will be projected into multiple outputs: the main content page, a summary list, an RSS feed, a sitemap URL, and search text. Each projection could introduce new errors. For example, the main content has been updated, but the search index still reads the old cached version; the summary list displays a manually created summary, while the RSS feed incorrectly extracts the original content enclosed in code tags. The input contract alone cannot prove that these outputs are consistent.

It’s possible to create a test article with titles, abstracts, body text, and slugs that all have independent and non-sensitive markers. Once built, parse the corresponding HTML, RSS, and sitemap, asserting that the markers appear in the correct locations, and confirming that markers from private drafts are absent. Here, a parser should be used instead of simply searching for the presence of a string within the file: HTML title escaping and XML encoding can change the surface text, but they may still retain the same meaning.

Output verification must also detect stale files. If a public article becomes private, does its old page remain in the output directory? An incremental build that writes only new files without cleaning old output can leave a withdrawn URL accessible. A test should first generate the public page, change the input, rebuild, and confirm that the old path is absent from the final deployment directory. Building into a clean directory and atomically replacing the release is easier to reason about, but you must still confirm which directory the deployment platform actually uploads.

A list can be generated between the public collection and the audit records, listing the input fingerprints and output paths for each public slug. This helps verify whether a particular article comes from the expected version and should not contain private content or a complete private list. The number of URLs in the sitemap may not be the same as the number of public slugs, as there are also the homepage, tags, and other pages; when verifying, compare the article subsets to avoid simply matching the numbers to determine content correctness.

Don’t Let Editorial Convenience Bypass Trust Boundaries

Preview tools are often designed to be more lenient with input, making it easier for authors. If a lenient preview mode is used in conjunction with strict publishing, it’s crucial to explicitly distinguish between the running and publishing modes, and to test branches that bypass validation checks in the published version. When environment variables are missing, a strict mode should be selected instead of defaulting to permissive mode. Furthermore, misconfigurations should also trigger errors, preventing them from becoming another mode that allows public drafts.

While preserving the ability to identify failure paths, the preview should display “Publication date not filled in,” but should not generate a realistic-looking publication date that could mislead the author into believing that the publication has already been scheduled. The bulk repair tool can suggest tag normalization, but should not automatically merge ambiguous proprietary names. The editing tools reduce input costs, the schema constraints ensure a defined structure, and the review process assesses the credibility of the content. Only through the combined use of these features can we avoid the “convenience” ultimately becoming a reason to bypass publication boundaries.

Validate Upgrades, Not Just Current Data

Before modifying field rules, run the existing content through a new validation check and save the list of incompatible content. First, fix or migrate the content, then enable mandatory failure. Otherwise, a single upgrade dependency could prevent all old documents from being built. Special testing should be reserved for the conversion from default drafts to explicit publication, ensuring that failure still occurs when missing input fields are present, rather than suddenly publishing due to changes in default values.

The smallest test set should include both valid drafts and valid publication records, as well as missing publication times, invalid calendar dates, path conflicts, and migrations from old formats. The assertions should focus on specific error categories and their sources, without locking down the entire third-party error text, to avoid meaningless test fluctuations caused by tool upgrades. The ultimate benefit is not “The schema is complex,” but rather that the page code no longer makes arbitrary guesses about content status, and editors can know which contract is not being met before publication.

Assume you are preparing to remove the old field summary and replace it with description. The details template has already been migrated, but the RSS converter still reads the old value. If you remove summary from all files in the first step, the schema might pass, but the subscription summary will become empty. First, list the field consumers: template, subscription, search, import, and preview, and record their reading locations; then, set two different markers for the same synthetic record to determine which value is ultimately consumed.

During the transition period, do not allow both fields to be edited independently. Designate description as the authoritative input, and summary, required by the old tool, should be generated through a compatible conversion. If both inputs are provided and conflict, the author should confirm, rather than selecting one based on the reading order. Once all consumers have switched, the official input will reject the old fields. The old data can only be converted through an explicit import mode. This prevents the “old fields can still be read” from indefinitely extending into “new drafts can also continue to write to the old fields.”

To exit the testing phase, we will use three sets of records: only new fields should pass, only old fields should fail in the final release, and any conflicting fields should receive a clear diagnosis. We will then rebuild the RSS and search samples to verify that the new summaries actually appear. This process tests the field lifecycle, not just adding more required fields. Each time the contract is reduced, it should clearly indicate which fields are no longer read, preventing the schema cleanup from becoming silent content deletion.

References: Astro Content Collection, Zod.