Validation should speak to the file
M31V’s build stopped handing writers stack traces and began reporting source, field, consequence, and correction in editorial language.
M31V's first validation error looked like this:
TypeError: Cannot read property 'map' of undefined
at renderTopics (archive.js:84:17)The actual mistake was an article whose metadata said topic instead of topics. The writer had one incorrect field in one source file. The build reported the line where its own assumption finally crashed.
A file-based publishing system asks writers to work outside a constrained database form. Validation becomes part of the authoring interface. It should speak to the file and task, not only to the generator's call stack.
An error needed four parts
I standardized editorial diagnostics around four questions:
Where is the problem?
What is wrong?
Why does it prevent publication?
What is the likely correction?The topic error became:
content/articles/a-small-release.md:4
Unknown metadata field “topic”.
Topic archives are generated from the “topics” list.
Rename this field to “topics” and provide one or more topic IDs.The message still included an internal diagnostic code and stack trace under a verbose flag. Ordinary output led with the source the writer could change.
This format was longer than invalid front matter and faster to act on. It treated correction time as a design metric.
A malformed header could prevent the parser from producing any metadata object. A valid header could still violate the publication schema.
M31V separated diagnostics:
- Syntax: indentation, delimiters, quoting, or list structure could not be parsed.
- Schema: a field was missing, unknown, or the wrong type.
- Relationship: a declared author, topic, asset, or redirect target did not exist.
- Policy: the content was structurally valid but violated a publication rule.
- Artifact: generation produced a broken link, collision, or invalid output.
The category selected language and likely correction. A parser error pointed to the exact line and nearby text. A relationship error named the unresolved ID and showed close known values. A policy warning explained the publication consequence.
I avoided wrapping every failure in “Validation error.” The useful noun was the source, link, author, or release path that needed attention.
The pipeline could now stop at the narrowest trustworthy stage instead of letting malformed input travel until a template crashed.
Source position survived normalization
Once M31V parsed content into a normalized graph, it was easy to lose the line where a value originated. Later checks knew that an author ID was invalid but not where the writer had typed it.
Normalized fields carried source references:
interface SourceRef {
file: string;
line: number;
column: number;
field?: string;
}The graph kept those references outside the public content model. A relationship validator could attach the error to author: robin-smth in the article header rather than to the author archive generator.
Derived values retained provenance too. A canonical path collision could name both source files whose slugs produced the same URL. A redirect loop listed every redirect declaration involved.
This required the parser to preserve more than final values. The cost paid off across the entire pipeline. Diagnostics could lead back from artifact failure to editable source.
Provenance was not only for historical audit. It was basic ergonomics for a compiler-like publishing tool.
Unknown fields failed deliberately
Ignoring unknown metadata made typos dangerous. descripton looked harmless in the article file, then the feed silently fell back to a body excerpt while the archive showed no summary.
M31V rejected unknown fields and suggested close matches:
Unknown field “descripton”. Did you mean “description”?The suggestion used a conservative similarity rule. It did not rewrite source automatically. A field could be intentionally new and unsupported by the current build; changing it silently might conceal a version mismatch.
Extension fields, when genuinely needed, lived under an explicit namespace rather than making every arbitrary key valid. The schema remained capable of detecting mistakes.
Strictness was paired with good messages. A system that rejects flexible files without explaining accepted shape merely transfers the burden to documentation searches.
The validator printed a compact valid example when a field had structural requirements, such as a list of topic IDs.
Required did not always mean nonempty
The first schema checked only that description existed. An empty string passed. A title containing whitespace passed. An empty topics list passed even for article types that required a primary topic.
Field validation reflected editorial meaning:
title: nonempty text after trimming
description: one usable sentence within publication bounds
published: a valid, unambiguous date when status is published
topics: one or more declared topic IDs for standard articles
slug: valid path segment under URL policyI avoided turning every editorial preference into a hard error. An unusually long title produced a warning with the surfaces it might affect. A missing canonical identifier was an error because it could produce unstable URLs.
The error or warning level came from consequence, not taste. Hard failures protected integrity and release safety. Warnings invited judgment where a writer could reasonably choose an exception.
This kept validation from becoming an automated style critic that authors learned to ignore.
Relationship errors showed the local vocabulary
An article referencing topic web-perf failed because the declared topic ID was web-performance. The original message said “foreign key not found,” language inherited from how I imagined a database.
The revised diagnostic said:
content/articles/performance.md:7
Topic “web-perf” is not declared.
Closest topic: “web-performance” (Web performance).
Topic IDs are defined in content/topics.yml.The suggestion displayed stable ID and human label. If several candidates were close, it listed a small set without guessing.
For a removed author or topic, the message could reference a migration note or replacement ID. This made controlled vocabulary changes part of editorial workflow.
Relationship checking used the complete candidate graph, so a new topic added in the same change was valid. Validation did not require publishing dependencies in a particular order.
The graph became a collaborative vocabulary, and errors helped writers navigate it.
URL collisions named both claimants
Two articles could declare the same slug. An article path could collide with a topic archive. A redirect source could occupy a path generated by live content.
The first generator failed on whichever file wrote second, making the earlier claimant invisible. The validator built the complete path namespace and reported every owner:
Public path “/notes/reliable-releases/” has two owners:
— article from content/articles/releases.md:3
— redirect from content/redirects.yml:18
Choose a new article slug or remove the redirect only if the old URL no longer needs it.The correction text avoided automatically preferring newer content over a redirect. Old public URLs were product commitments, not clutter blocking a fresh slug.
Path checks ran before writing output. The build did not rely on file-system overwrite order to choose a winner.
A collision is easier to resolve when every competing claim and its consequence is visible at once.
Internal links needed editorial context
A broken-link checker can report 404. M31V knew the candidate graph and could say more.
For links written with stable content references, it resolved the target and generated the correct public path. For literal internal paths, it checked the candidate namespace. If a target had moved and a redirect existed, the validator warned that source should use the canonical path while preserving the redirect for external readers.
content/guides/deploying.md:42
Internal link uses old path “/articles/releases/”.
The canonical article path is “/notes/reliable-releases/”.
External requests will redirect, but source links should point directly to the canonical path.Fragment links checked heading identifiers in the target document. The error named the nearest headings rather than leaving a dead in-page jump.
External links were checked separately and cautiously. A remote timeout was not automatically proof that publication should fail. Internal integrity was fully under M31V's control and received the stronger gate.
Asset errors described the rendering consequence
Missing an image file was straightforward. Incorrect dimensions, unsupported format, or absent alternate text needed more context.
An article image diagnostic included the source reference, requested asset path, and content role:
content/articles/workshop.md:31
Image “../images/lathe-label.jpg” was not found.
The article cannot generate its figure or feed preview.
Paths are resolved relative to the article source.If an image was decorative, the markup needed to declare that deliberately. An empty alternate value without the decorative flag received an error explaining the distinction.
Oversized assets produced performance warnings with generated display dimensions and transfer estimate. The message pointed to the asset pipeline command that generated a suitable derivative.
The validator did not merely enforce accessibility and performance as abstract quality badges. It explained which publication surfaces and readers the missing information affected.
Scheduled content used an explicit clock
A scheduled article could be valid at one build time and invalid at another. Calling the system clock directly made tests and preview inconsistent.
The build received an explicit publication time. Validation used it to determine whether status and dates agreed:
status: scheduled
published: 2015-03-10T09:00:00-05:00
build time: 2015-03-09T16:00:00-05:00
result: valid scheduled article, excluded from public artifactA published article dated in the future produced a message offering the two likely corrections: change status to scheduled or correct the date. A scheduled article in the past warned that the publishing trigger may have been missed.
Dates required explicit offsets where time of day mattered. The message showed both source value and normalized publication time.
Treating time as input made errors reproducible. “It built yesterday” no longer depended on an unstated clock and timezone.
One error could cause twenty more
A missing author record could make the article invalid, the author archive absent, related navigation broken, and feed generation fail. Reporting every downstream symptom overwhelmed the original cause.
The validator tracked dependency. Once an article failed normalization, later stages marked dependent checks skipped rather than emitting speculative errors.
Output looked like:
1 error
content/articles/profile.md:5 — author “rboin” is not declared
3 checks skipped because the article could not be normalized:
author archive, feed entry, related-content graphThis kept the diagnostic set stable and prevented writers from fixing generated symptoms in random order.
After correction, the pipeline reran and revealed any independent issues. The tool prioritized actionable root causes over maximum error count.
Dependency-aware reporting was especially valuable in a complete graph build, where one malformed node touched many surfaces.
Multiple errors were grouped by work
Stopping at the first error forced a slow edit-build cycle. Reporting everything in traversal order produced a jumble across files.
M31V collected independent diagnostics and grouped them by source file, then by field or line. A summary grouped counts by category:
3 errors in 2 files
— metadata schema: 1
— unresolved relationships: 1
— internal links: 1The default output showed enough context to fix each. A compact mode emitted one line per issue for editor integration. A machine-readable format supported automation without making JSON the human interface.
Warnings appeared after errors and did not obscure the release blockers. A clean build said what it checked and how many sources entered the artifact rather than printing only “success.”
Diagnostics were organized around an editing session, not the validator's internal pass order.
Each diagnostic had a code such as META_UNKNOWN_FIELD or PATH_COLLISION. The code linked to a short reference with accepted forms, examples, and why the rule existed.
The message remained self-sufficient for common corrections. Documentation covered complicated cases without turning the console into a chapter.
Codes also helped tests assert the kind of failure without freezing every word of editorial copy. The source reference and structured parameters could be tested separately from final formatting.
I avoided numeric codes that carried no mnemonic value. Writers rarely needed to memorize them; they helped search, automation, and support.
Changing a diagnostic's phrasing did not change its semantic identity. Changing its meaning required a new code or documented compatibility behavior.
This gave error design a small, maintainable API.
Warnings become useless when they accumulate permanently. Every warning named the policy and offered either a correction or an explicit suppression with a reason.
For an intentionally long title, the article could declare a narrow override in metadata with a comment-like reason. The build manifest recorded active suppressions. A global “ignore warnings” flag was not part of normal release.
Some warnings expired. A temporary external-link exception carried a review date. Others, such as an unlisted article, represented stable editorial intent and used a dedicated field rather than suppression.
The release summary counted new and existing suppressions. This prevented the warning layer from becoming background noise nobody owned.
Hard integrity errors had no suppression path. A public path collision could not be waived because the file system would still choose one output.
The difference kept flexibility deliberate and guarantees firm.
Stack traces stayed available for tool failures
Not every exception was an editorial mistake. A generator bug, file permission failure, or unexpected parser state needed developer diagnostics.
The top-level handler distinguished known validation results from internal errors. An internal failure said:
M31V failed while generating the topic archive.
This appears to be a build-tool error, not a content validation error.
Run with --verbose for the stack trace and build context.The default did not dump a wall of frames at a writer. It also did not convert an unknown exception into a misleading “invalid article.”
Verbose output included tool version, build ID, sanitized configuration, and stack. A minimal reproduction command named the affected source subset when known.
Separating product validation from tool failure protected trust in both. Writers knew when to edit content and when to report the build system.
Fast validation shaped the workflow
The full candidate build checked every source and output. During editing, waiting for the complete asset pipeline after each keystroke made good validation easy to bypass.
M31V offered layered commands:
validate current file and direct relationships
validate changed content graph
build and validate complete candidate
verify rendered artifactThe quick path reused the same schema and diagnostic format. It did not invent a lenient second validator. Checks requiring the whole namespace, such as path collisions, ran against a cached current graph or waited for the complete build.
Editor integrations could invoke compact diagnostics on save. The release still required the full, clean candidate check.
Fast feedback and complete assurance served different moments. The shared rules prevented a file from passing locally only to fail with unrelated wording later.
Error examples became test fixtures
Every important diagnostic had a tiny invalid source fixture and expected structured result:
fixture file
diagnostic code
source line and field
parameters
severity
suggested correction kindFormatting tests checked that the human output remained readable. Integration tests built a graph with several related errors and verified root-cause suppression and grouping.
Fixtures used invented content and stable paths. They did not depend on the current publication archive, which would make diagnostics change whenever an article was renamed.
I added regression fixtures whenever a real editing mistake produced a stack trace or vague message. The quality bar became: the next person making this mistake should receive the file, reason, consequence, and correction.
Validation grew from editorial experience rather than an abstract desire to reject every possible invalid value.
Good diagnostics reduced the need to study a full schema before writing. A new author could begin with a valid generated article, make a mistake, and receive a local explanation connected to the source.
The validator did not replace documentation. It made the format learnable at the moment a rule mattered. Unknown fields suggested known ones. Relationship errors showed where vocabularies lived. Date errors printed accepted examples. Path collisions explained the public identifier policy.
This was the authoring experience a CMS often supplies through labels and controls. M31V supplied it through generated files, editor feedback, preview, and precise errors.
The choice of plain text therefore carried an obligation: the system had to make invisible constraints visible without blaming the writer for not knowing internal code.
Validation became a publication feature
The first stack trace described where M31V crashed. The final diagnostic described where the writer could act.
That change required source provenance, staged parsing, graph-wide relationship checks, dependency-aware reporting, stable diagnostic codes, and tests for error language. It was not a cosmetic wrapper around exceptions.
The strongest errors connected technical rule to public consequence. A missing description affected archive and feed. A duplicate slug claimed one URL twice. A missing image broke a figure and preview. A stale internal link spent a redirect the publication wanted to preserve for external readers.
Once validation spoke that language, it became part of the publishing product. It protected the release and taught the source format at the same time.
A file-based system does not get editor ergonomics for free. It earns them every time a mistake points back to the file with enough context to fix it confidently.