DEV Community

xiaoxu
xiaoxu

Posted on

Using Front Matter as a Multi-Platform Publishing Contract

Using Front Matter as a Multi-Platform Publishing Contract

Why this matters

A Markdown article often looks self-contained: front matter at the top, prose
below it, and perhaps a few local images. But once a publisher targets more
than one platform, those first lines stop being editorial decoration. They
decide where a network write goes, whether it creates a draft or a public
article, which tags cross the API boundary, and whether a canonical URL or
cover image is attached.

That makes front matter deployment configuration.

The dangerous failure mode is not always invalid YAML. A syntactically valid
file can still express a surprising operation. In the publisher I examined,
omitting platforms enables both supported platforms by default. Omitting
published is safer—it defaults to false—but setting it to true changes the
default operation to public unless the command explicitly overrides the mode.

I traced the contract from parser to adapter, exercised its defaults and
rejections with temporary fixtures, and captured the outgoing DEV.to payload
without making a network request.

What I built or tested

The publisher uses a typed metadata shape:

interface ArticleMeta {
  title: string;
  slug: string;
  description?: string;
  tags: string[];
  cover?: string;
  canonical_url?: string;
  published: boolean;
  platforms: Partial<Record<"devto" | "hashnode", boolean>>;
}
Enter fullscreen mode Exit fullscreen mode

I tested four boundaries:

  1. parsing a fully explicit article;
  2. parsing a minimal article and observing defaults;
  3. deriving and overriding the effective release mode; and
  4. mapping metadata into a captured DEV.to request.

I also supplied three malformed values: a non-URL canonical value,
comma-separated tags instead of an array, and a string where a platform
boolean was required. All three failed locally with ZodError.

The experiment did not contact DEV.to or Hashnode. It verifies the local
contract and payload construction, not remote acceptance.

Setup

The source article uses this shape:

---
title: "A descriptive title"
slug: "a-descriptive-title"
description: "A short platform summary."
tags:
  - typescript
  - testing
published: false
platforms:
  devto: true
  hashnode: false
---
Enter fullscreen mode Exit fullscreen mode

Two rules are worth making explicit in a review checklist:

  • tags is a YAML array, not a comma-separated string.
  • Every production article should state its platform booleans even though the parser provides defaults.

The parser accepts an optional local cover path and an optional
canonical_url. The canonical value must be a URL. A cover takes a separate
asset path through image processing before it becomes a platform-facing URL.

Step-by-step walkthrough

1. Parse once into a narrow metadata type

The Markdown loader separates YAML from body content, then validates the
metadata with Zod. title and slug must be non-empty strings. Tags default
to an empty array, published defaults to false, and platforms defaults to
this:

{ devto: true, hashnode: true }
Enter fullscreen mode Exit fullscreen mode

Defaults make small examples convenient, but the platform default is broad.
If both credentials exist, an omitted field can expand the blast radius of a
later publish command. My mitigation is simple: allow the parser default for
backward compatibility, but require explicit platform values in generated and
reviewed production content.

2. Turn metadata into an operation

The publisher derives the effective mode from the article when no command
option is present:

const mode = options.mode ?? (article.meta.published ? "public" : "draft");
Enter fullscreen mode Exit fullscreen mode

The CLI can supply --draft or --public, and rejects using both. This creates
a useful precedence rule:

explicit command mode > front-matter published flag > safe false default
Enter fullscreen mode Exit fullscreen mode

Keeping published: false in the canonical source is therefore compatible
with a reviewed public release: the automation can run all checks first, then
make the final command's --public flag the release decision.

3. Intersect article targets with command scope

Front matter says which platforms an article permits. A repeated
--platform option can narrow that list further. The publisher collects
enabled metadata targets and intersects them with the requested platforms.

That distinction matters:

  • front matter is the article's durable allowlist;
  • the command is the current operation's requested scope.

A command should be able to narrow an allowlist, not silently widen it.

Mermaid diagram 1

Durable article policy and one-run command scope meet before any adapter is
called.

4. Map fields at the adapter boundary

After local images are processed, the publisher constructs a platform-neutral
PublishInput containing:

  • title and slug;
  • transformed Markdown;
  • description and tags;
  • canonical and cover URLs; and
  • the effective public/draft boolean.

The DEV.to adapter translates that input into its API object. It converts the
tag array into a comma-separated value and takes at most four tags:

tags: input.tags.slice(0, 4).join(",")
Enter fullscreen mode Exit fullscreen mode

The current
Forem API documentation
documents the same create fields—title, Markdown body, published state, tags,
main image, canonical URL, and description—and limits an article to four
tags.

My intercepted request started with five tags and contained only:

typescript,testing,automation,devops
Enter fullscreen mode Exit fullscreen mode

That proves the local truncation behavior. It does not prove that the fifth
tag is unimportant. Silent truncation can hide a content mistake, so the
better generator-side rule is to reject more than four tags before the
adapter.

What went wrong

The most surprising result came from the minimal valid fixture:

---
title: "Minimal article"
slug: "minimal-article"
---
Enter fullscreen mode Exit fullscreen mode

It parsed successfully with no tags, draft mode, and both platforms
enabled. That is valid according to the repository schema, but it is a risky
default for unattended publishing. The meaning of “field absent” is not “do
nothing”; it is “allow both targets.”

There is a second asymmetry. The parser accepts any number of tags, while the
DEV.to adapter sends only the first four. Validation and transport therefore
do not express the same limit. The payload is legal, but an author can believe
five tags were selected when one was silently discarded.

Finally, the test suite has focused DEV.to adapter coverage but no direct
table-driven test for front-matter defaults and malformed values. TypeScript
interfaces cannot close that gap because YAML arrives as runtime data.

Fix or mitigation

I would enforce the contract at three levels.

First, keep runtime schema validation. It correctly rejected:

  • canonical_url: "not-a-url";
  • tags: "typescript,testing"; and
  • devto: "yes".

Second, make production policy stricter than parser compatibility:

published: false
platforms:
  devto: true
  hashnode: false
Enter fullscreen mode Exit fullscreen mode

Require one to four lowercase tags for DEV.to and omit canonical_url unless
the article actually has an original publication URL. Require a local relative
cover path only when that file exists.

Third, add table-driven contract tests. A small matrix should assert:

Case Expected result
explicit single target only that target is enabled
omitted platforms documented default is returned
public flag with draft override effective mode is draft
five DEV.to tags validation fails before truncation
malformed canonical URL parser rejects
string platform flag parser rejects

The key is to test both valid defaults and invalid inputs. Testing only the
happy-path YAML leaves the most operationally important semantics implicit.

Trade-offs

Front matter keeps article policy beside the content. It is reviewable in Git,
portable across environments, and easy for a generator to produce. A
platform-neutral PublishInput also prevents every adapter from reparsing
Markdown metadata independently.

The same convenience creates coupling:

  • changing a parser default changes old articles that omitted the field;
  • platform-specific limits can be hidden behind a generic type;
  • a boolean published flag mixes authoring intent with release behavior;
  • canonical URLs and covers require validation beyond TypeScript's compile time; and
  • multi-platform metadata tends toward the least common denominator.

For a larger system, I would separate compatible parsing defaults from a
stricter release policy validator. The parser answers “can I understand this
file?” The release gate answers “is this exact operation allowed now?”

How I verified it

The isolated experiment observed:

{
  "minimalDefaults": {
    "tags": [],
    "published": false,
    "platforms": {
      "devto": true,
      "hashnode": true
    }
  },
  "modeFromPublishedFlag": "public",
  "explicitModeOverride": "draft",
  "capturedDevtoTags": "typescript,testing,automation,devops",
  "invalidCanonicalUrl": "ZodError",
  "commaSeparatedTags": "ZodError",
  "nonBooleanPlatform": "ZodError"
}
Enter fullscreen mode Exit fullscreen mode

The adapter call used an in-memory replacement for fetch, so no API received
the fixture or fake credential. The repository's focused DEV.to test file also
passed all three tests.

Before public release, I additionally ran the article validator, Mermaid
rendering and visual inspection, the publisher dry-run, the TypeScript
typecheck, and the complete repository test suite.

Conclusion

Front matter is a compact publishing contract only when its semantics are
explicit and tested. The most important fields are not the visible title and
description; they are the ones that control side effects: targets, mode,
canonical identity, cover processing, and platform limits.

Parse runtime data into a narrow type, intersect durable article permissions
with one-run command scope, and validate platform constraints before the
adapter silently normalizes them. Keep the source in draft mode and make the
final public flag a reviewed release decision.

That turns a few lines of YAML from hopeful metadata into an auditable safety
boundary.

AI assistance disclosure

I used an AI coding assistant to trace metadata from parser to platform
adapter, create the isolated fixture experiment, compare the payload with
official Forem documentation, and edit the draft. I reviewed the cited source,
executed the reported checks, inspected the rendered diagram, and made the
final publication decision only after the automated gates passed.

Top comments (0)