Skip to content

♻️ Rules.Content - Script a one-off reformat of all rule.mdx files to the TinaCMS standard #2683

Open

Description

@joshbermanssw

❗❗ DO NOT ACTION YET ❗❗

      > 3. Do you think all SSW rules should be updated to the new format
Yes - When speaking to Jack P and Kosta, they agreed there should be a script that goes through and formats all rules to use TinaCMS standard, so that we are less likely to run into this problem.

Adam disagrees with doing this because he’s on the fence.

Wicksy and I disagree too; he would prefer to pay the price for this formatting one PR at a time.

      > 4. Do you think that TinaCMS should have an option “[x] Don’t clean up markdown (for simpler reviewing)
❌ No - TinaCMS runs a formatter on all markdown that is parsed through it. This is to enforce certain rules and reduce the likelihood of errors in implementation. Without these guardrails - we’d be more likely to have issues arising from drifting standards. 

Hi Team!

cc: @KostaMadorsky, @0xharkirat, @Marxoz, @PothieuG, @Freego1783, @adamcogan

Related to #2375 - this PBI is the concrete implementation of that idea: write a script, run it once across all of SSW.Rules.Content, and get every rule.mdx onto the TinaCMS serializer's output format.

Pain

Most of the ~3,800 rules in SSW.Rules.Content are still in the legacy (V2) Markdown format. The moment anyone edits one of them through TinaCMS, Tina re-serializes the whole file on save, so a 1-line content change comes back as a full-file rewrite.

Real example - SSW.Rules.Content#11881 added a single list item ("Number 12: Fast Forward Edit") and renumbered the items after it. The diff:

public/uploads/rules/video-editing-terms/rule.mdx   +373  -456

One paragraph of intent, buried in 800+ lines of noise. Reviewers cannot see what actually changed, so PRs either get rubber-stamped or stall.

This will keep happening on every single rule until each one has been through Tina once. Doing it as a byproduct of normal editing means we carry the pain for years. Doing it once, deliberately, in a dedicated PR costs us one review we can safely skim.

What "the TinaCMS standard" actually means

These are the transformations observed in PR #11881, comparing legacy input to Tina's saved output:

Frontmatter

  1. Keys are reordered from alphabetical into the schema order defined in tina/collection/rule.tsx: type, title, uri, categories, authors, related, redirects, guid, seoDescription, then the audit fields.
  2. Sequence items get indented 2 spaces under their key (- title: becomes - title:).
  3. URLs and strings containing special characters get single-quoted ('https://ssw.com.au/people/andrew-forsyth', 'Piers Sinclair [SSW]').
  4. Dates convert to ISO 8601 with milliseconds: 2023-01-19 07:01:07+00:00 becomes 2023-01-19T07:01:07.000Z.
  5. Long values stop being wrapped at ~80 chars and go onto a single line (seoDescription is the usual culprit).

Body

  1. JSX expression props collapse to plain string props: description={"Video: ..."} becomes description="Video: ...".
  2. Runs of 2+ blank lines around MDX components (<youtubeEmbed />, <endIntro />, <boxEmbed />) collapse to a single blank line.
  3. Blank lines inside body={<>...</>} children are emitted with the surrounding indentation as trailing whitespace.

Point 8 matters: if our script emits clean blank lines there but Tina emits indented ones, every rule drifts again on its next save and we are back where we started.

Suggested approach

Do not reimplement the serializer. Reuse Tina's own, so the output matches byte-for-byte:

  1. Load the rule collection schema from tina/collection/rule.tsx.
  2. For each rule.mdx, parse with parseMDX and re-emit with stringifyMDX from @tinacms/mdx (the same path the Tina backend takes on save), pinned to the versions this repo uses today (tinacms 3.10.0, @tinacms/cli 2.5.3).
  3. Write the file back only if the bytes changed.
  4. Verify by round-tripping: running the script a second time must produce zero changes.

The script should live in SSW.Rules (where the schema lives) and be runnable against a local clone of SSW.Rules.Content.

Acceptance Criteria

  1. A repeatable script exists that reformats rule.mdx files to the TinaCMS serializer's exact output.
  2. The script is idempotent - a second run over already-formatted content produces zero changes.
  3. All rule.mdx files in SSW.Rules.Content are reformatted in a single dedicated PR containing no content changes.
  4. lastUpdated, lastUpdatedBy, and lastUpdatedByEmail are left untouched. A formatting pass must not rewrite every rule's audit trail or flood "Recently updated rules" with bot edits.
  5. No rendered output changes. Spot-check a sample of reformatted rules on a preview deployment against production, including rules with boxEmbed, youtubeEmbed, tables, and code blocks.
  6. Rule count, guid values, uri values, and redirects are all unchanged after the run (diff the frontmatter values, not the formatting).
  7. After the pass, editing a rule in TinaCMS and saving produces a diff limited to the actual change.
  8. The formatting commit SHA is added to .git-blame-ignore-revs in SSW.Rules.Content so git blame still points at the real authors.

Notes and risks

  • Timing. This invalidates every open PR and in-flight Tina branch in SSW.Rules.Content. Run it when open PRs are at a low, and give the team notice to merge or rebase first.
  • Verification burden. Nobody can meaningfully review a 3,800-file diff line by line. Confidence has to come from the round-trip test (AC2), the frontmatter value diff (AC6), and rendered spot-checks (AC5), not from reading the PR.
  • Follow-up. ♻️ Normalize formatting across Rules V3 to reduce noisy pull requests #2375 also asks for CI enforcement to stop future drift. Worth its own PBI once the one-off pass has landed, since drift should be near-zero afterwards.

Suggested tasks

  • Confirm the exact serializer entry point Tina's backend uses on save
  • Write the script in SSW.Rules and test it on ~20 representative rules
  • Verify idempotency and frontmatter value preservation across the full set
  • Deploy a preview and spot-check rendered output
  • Announce the freeze window to the team
  • Run the full pass and raise the dedicated PR
  • Add the commit SHA to .git-blame-ignore-revs

Activity

  1. github-actions commented on Jul 26, 2026

    @github-actions
    Contributor

    🤖 Triage Research

    Summary

    This is a well-scoped feature request. The goal is clear: write a one-off script that runs parseMDX → stringifyMDX (from @tinacms/mdx) over every rule.mdx to normalise it to what TinaCMS would emit on save, so future edits produce minimal diffs. The issue is already well-specified, but a few technical questions need to be answered before the script can be implemented safely.


    Relevant files

    File Why it matters
    tina/collection/rule.tsx Defines the field order that frontmatter keys must follow, and the body field definition needed as input to stringifyMDX
    tina/collection/shared/historyFields.ts Defines lastUpdated, lastUpdatedBy, lastUpdatedByEmail — these must be explicitly preserved / excluded from reformatting
    scripts/generate-people-latest-rules.js Good style reference for a script that walks the Content repo and uses gray-matter to read frontmatter
    scripts/prepare-content.js Shows the LOCAL_CONTENT_RELATIVE_PATH env-var pattern for locating the Content repo
    package.json Confirms gray-matter ^4.0.3 (direct dep) and tinacms 3.10.0 / @tinacms/cli 2.5.3 (direct); @tinacms/mdx 2.1.9 is a transitive dep (confirmed in pnpm-lock.yaml)

    Potential approach

    // scripts/reformat-rules.mjs  (rough sketch)
    import fs from "fs";
    import path from "path";
    import matter from "gray-matter";
    import { parseMDX, stringifyMDX } from "`@tinacms/mdx`";       // ← needs API verification
    import { bodyField } from "../tina/collection/rule.tsx";       // body field definition
    
    const CONTENT_PATH = process.env.LOCAL_CONTENT_RELATIVE_PATH ?? "../../SSW.Rules.Content";
    const rulesDir = path.resolve(CONTENT_PATH, "public/uploads/rules");
    
    // Schema field order (from rule.tsx top-to-bottom):
    const FIELD_ORDER = ["type","title","uri","categories","sidebarVideo","authors",
      "related","redirects","guid","seoDescription","thumbnail",
      "created","createdBy","createdByEmail",
      "lastUpdated","lastUpdatedBy","lastUpdatedByEmail",
      "isArchived","archivedreason"];
    
    // Audit fields that MUST NOT be touched
    const PRESERVE_AS_IS = new Set(["lastUpdated","lastUpdatedBy","lastUpdatedByEmail"]);
    
    for (const ruleDir of fs.readdirSync(rulesDir)) {
      const filePath = path.join(rulesDir, ruleDir, "rule.mdx");
      if (!fs.existsSync(filePath)) continue;
    
      const raw = fs.readFileSync(filePath, "utf8");
      const { data: fm, content: body } = matter(raw);
    
      // 1. Re-stringify body via Tina's own serializer
      const ast = parseMDX(body, bodyField);           // ← API TBC
      const newBody = stringifyMDX(ast, bodyField);    // ← API TBC
    
      // 2. Reorder frontmatter keys per schema order
      // 3. Normalise dates, YAML quoting (as Tina does)
      // 4. Reconstruct file
      // 5. Write only if changed
    }

    Key open questions / blockers

    1. @tinacms/mdx API signatures — parseMDX and stringifyMDX take a field parameter matching a Tina rich-text field definition. The exact signature in v2.1.9 needs to be confirmed. Does it accept the full collection fields array, or just the body field? Does it handle frontmatter too, or only the MDX body? Run node -e "const x = require('@tinacms/mdx'); console.log(Object.keys(x))" in the project root to inspect.

    2. Frontmatter serialisation — the issue lists 5 frontmatter transformations (key reorder, indent, quoting, dates, line-wrapping). parseMDX/stringifyMDX handle the body, not the frontmatter. Frontmatter needs to be rebuilt manually (or via a different Tina internal). The key question is: which YAML serialiser does TinaCMS use, and can it be imported directly? Check node_modules/@tinacms/schema-tools`` or node_modules/tinacms/dist for a `stringifyDocument` or `buildASTDocument` export.

    3. lastUpdated date format drift — if the script normalises date values in frontmatter (step 4 in the issue spec: 2023-01-19 07:01:07+00:00 → 2023-01-19T07:01:07.000Z), does that apply to lastUpdated too? The issue says "leave audit fields untouched" (AC4), so probably not — but it means those rules may still drift slightly on their next Tina save. Needs a decision.

    4. body={<>...</>} trailing whitespace — the issue explicitly warns about blank lines inside JSX children being emitted with surrounding indentation as trailing whitespace (point 8). This must come out of stringifyMDX naturally. Verify this against a known-reformatted file before running at scale.

    5. Script entry point format — other scripts in scripts/ use CommonJS (require). This script needs import for @tinacms/mdx (ESM). Should use .mjs or "type": "module" or a --input-type=module flag. Check whether tina/collection/rule.tsx can be imported directly from a plain Node script (it has JSX + TypeScript), or whether the body field needs to be extracted/duplicated as plain JS.


    Suggested first steps

    • node -e "const {parseMDX,stringifyMDX}=require('@tinacms/mdx');console.log(parseMDX.toString().slice(0,200))" to inspect the actual function signatures available in this project's node_modules
    • Test the round-trip on a single known-complex rule (e.g. video-editing-terms/rule.mdx) — parse then stringify and diff against production
    • Decide how to handle the tina/collection/rule.tsx import (TSX file — may need a small JS wrapper that re-exports just the body field definition, or use tsx / ts-node to run the script)

    Confidence

    ~45% — LOW. The overall shape of the script is clear, but the exact @tinacms/mdx v2.1.9 API, the YAML frontmatter serialisation strategy, and the JSX trailing-whitespace behaviour all require hands-on verification before a correct implementation can be written. The issue is correctly labelled "Needs Refinement". Recommend completing the investigation tasks above (especially the parseMDX API check and a single-file round-trip test) before implementing the full script.

    Generated by Issue Triage for issue #2683 · ● 963.3K · ◷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions