Skip to content

Support YAML front matter in Markdown uploads#19

Open
Said-MZ wants to merge 4 commits into
AliOsm:mainfrom
Said-MZ:feat/markdown-front-matter
Open

Support YAML front matter in Markdown uploads#19
Said-MZ wants to merge 4 commits into
AliOsm:mainfrom
Said-MZ:feat/markdown-front-matter

Conversation

@Said-MZ

@Said-MZ Said-MZ commented Jul 13, 2026

Copy link
Copy Markdown

Summary

  • Parse a leading ----delimited YAML front matter block off Markdown uploads before rendering (MarkdownDocument). title overrides the H1/filename fallback for the page <title>; description renders as a <meta name="description"> tag.
  • Malformed front matter, a non-mapping YAML block (e.g. a list), or its absence entirely all fall back to treating the whole file as plain Markdown body — front matter is never load-bearing for rendering.
  • Bonus fix: Procfile.dev's css process used Tailwind's --watch, which stops as soon as stdin closes — which Foreman does immediately for spawned processes — killing the whole mise run dev process group right after boot. Switched to --watch=always.

Before

Screenshot 2026-07-13 at 10 28 03 PM

After

Screenshot 2026-07-13 at 10 33 34 PM

tested md syntax:

---
created: 2026-07-04
categories:
  - "[[Projects]]"
type:
  - "[[SaaS]]"
  - "[[AI]]"
status:
  - "[[Idea]]"
---
SaaS idea on the [[Ventures]] track.

#saas-idea
### **ChurnAI – AI-Powered Customer Retention & Churn Prevention**

#### **🔹 Problem Solved:**

- **Businesses lose customers** without knowing why until it’s too late.
- **Current churn trackin...........
...
..
.

Test plan

  • bin/rails test test/models/markdown_document_test.rb — all pass
  • bin/rails test test/models/paste_test.rb — all pass (no regression in the ingest path)
  • Manually rendered a sample Markdown file with front matter via rails runner and confirmed <title>/<meta description> are set correctly and the YAML block doesn't leak into the body
  • Verified mise run dev keeps all three Foreman processes (web/js/css) alive after the Procfile fix

🤖 Generated with Claude Code

Said-MZ added 4 commits July 13, 2026 22:34
Tailwind's --watch stops as soon as stdin closes, which Foreman does
immediately for spawned processes, so `mise run dev` exited the whole
process group right after boot. --watch=always keeps it running
regardless of stdin.
Parse a leading --- delimited YAML block off Markdown uploads before
rendering. `title` overrides the H1/filename fallback for the page
<title>, and `description` renders as a meta description tag. Malformed
or non-mapping front matter (or none at all) falls back to treating the
whole file as plain Markdown body, so nothing breaks rendering.
Psych::SyntaxError and Psych::DisallowedClass aren't the only failure
modes -- YAML aliases (&anchor/*alias) raise Psych::BadAlias or
Psych::AliasesNotEnabled, which weren't rescued and would break
rendering instead of falling back to plain Markdown body.

Co-authored-by: Copilot code review
Redundant with the broadened Psych::Exception rescue itself; not
worth the upkeep.
Copilot AI review requested due to automatic review settings July 13, 2026 19:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for optional YAML front matter in Markdown uploads by extracting metadata (title/description) before rendering, while keeping rendering resilient when front matter is missing or invalid. Also adjusts the local dev Procfile so Tailwind CSS watching doesn’t exit immediately under Foreman.

Changes:

  • Parse and strip leading ----delimited YAML front matter in MarkdownDocument, using title for the HTML <title> and description for a <meta name="description"> tag.
  • Extend test coverage for front matter parsing behavior and fallback scenarios.
  • Update Procfile.dev CSS watcher to use --watch=always so mise run dev stays up reliably.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
app/models/markdown_document.rb Implements front matter splitting + uses metadata for <title> and meta description.
test/models/markdown_document_test.rb Adds tests validating front matter extraction, fallback behavior, and HR non-detection.
Procfile.dev Keeps Tailwind CSS watch process alive under Foreman via --watch=always.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +170 to +174
match = FRONT_MATTER_PATTERN.match(raw)
return [ {}, raw ] unless match

data = YAML.safe_load(match[1], permitted_classes: [ Date, Time ])
data.is_a?(Hash) ? [ data, match[2] ] : [ {}, raw ]

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed locally, with one additional regex edge case: ---\n---\nbody does not match FRONT_MATTER_PATTERN at all because the pattern requires a newline between the captured YAML and the closing delimiter. ---\n\n---\nbody does match, but safe_load returns nil and the code falls back to the raw body. Both forms therefore render the delimiters (including an <hr>), so the fix needs to make the pattern accept an adjacent closing delimiter as well as treating nil as an empty mapping.

<meta name="viewport" content="width=device-width, initial-scale=1">
<title>#{CGI.escapeHTML(document_title)}</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
#{description_meta_tag}<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. The resulting HTML remains valid, so I consider this cosmetic/non-blocking, but constructing the <head> as consistently indented lines would remove the formatting glitch.

@AliOsm AliOsm left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detailed review findings are inline. I found two failure modes beyond the existing automated comments; both were reproduced locally with the repository's pinned mise toolchain.

# <title>.
def document_title
@heading.presence || File.basename(@filename, ".*").presence || "Untitled"
@front_matter["title"].presence || @heading.presence ||

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: @front_matter["title"] is not guaranteed to be a String. YAML.safe_load can return an Integer, TrueClass, Date, Array, or Hash here, and CGI.escapeHTML(document_title) then raises TypeError. I reproduced this with title: 2026-07-13 (a Date, because dates are explicitly permitted), title: 1984, title: true, a list, and a mapping. This makes an otherwise valid Markdown upload fail before model validation. Please validate the metadata schema or deliberately normalize supported scalar values before returning the title, and add regression tests for implicit YAML types.

match = FRONT_MATTER_PATTERN.match(raw)
return [ {}, raw ] unless match

data = YAML.safe_load(match[1], permitted_classes: [ Date, Time ])

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High-severity resilience issue: deeply nested valid YAML can raise SystemStackError, which is not covered by the Psych::Exception rescue below. I reproduced SystemStackError: stack level too deep with a 4,021-byte front-matter document containing a 2,000-level nested flow sequence. That escapes the promised plain-Markdown fallback on public creation paths. Please enforce a safe nesting limit/parser strategy, or narrowly handle SystemStackError around this parse operation and fall back.

test "does not treat a body horizontal rule as front matter" do
html = MarkdownDocument.new("no leading marker\n\n---\n\nafter the rule", filename: "x.md").to_html
assert_includes html, "<hr"
end

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extend the boundary coverage before merging: non-string/implicitly typed title values, empty ---\n--- front matter, deeply nested YAML, aliases, and CRLF input. In particular, the alias case motivated broadening the rescue to Psych::Exception, but its regression test was removed in the final commit; keeping that test would protect the behavior from a later narrowing of the rescue.

Comment thread Procfile.dev
web: env RUBY_DEBUG_OPEN=true bin/rails server
js: yarn build --watch
css: yarn build:css --watch
css: yarn build:css --watch=always

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified this is functionally correct: with the pinned mise toolchain, yarn build:css --watch=always remained alive after stdin was closed. It is unrelated to YAML front matter, though, so consider moving it to a separate PR to keep review and rollback scope focused. Non-blocking.

AliOsm commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Review summary

Verdict: request changes before merge.

The implementation is readable and the happy path is well covered, but the inline review documents two untested rendering failures:

  • non-string/implicitly typed YAML titles reach CGI.escapeHTML and raise TypeError;
  • deeply nested YAML raises SystemStackError, escaping the Psych::Exception fallback.

I also confirmed the existing empty-front-matter issue and added the regex-specific detail to that thread. The meta-description indentation issue is valid but cosmetic.

Local validation

All checks below were run through the repository's pinned mise toolchain:

  • 476 tests / 2,217 assertions passed;
  • full RuboCop passed;
  • Brakeman reported no warnings;
  • Bundler Audit reported no vulnerabilities;
  • git diff --check passed;
  • HTML escaping for string title/description values worked correctly;
  • the Tailwind --watch=always process remained alive with stdin closed.

The hosted GitHub Actions run is not green yet: it concluded action_required with zero jobs, so it appears to require approval before CI can actually execute.

Before merging, I recommend fixing the typed-title, parser-depth, and empty-front-matter cases; restoring the YAML-alias regression test; and obtaining a real CI run. The Procfile change is correct, but would be cleaner as a separate PR because it is unrelated to front-matter support.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants