Awesome
remark-lint-frontmatter-schema
📑
<!-- [![Build Status](https://img.shields.io/github/workflow/status/JulianCataldo/remark-lint-frontmatter-schema/release/master.svg)](https://github.com/remark-lint-frontmatter-schema/actions/workflows/release.yml?query=branch%3Amain) -->
<!-- [![Renovate](https://img.shields.io/badge/Renovate-enabled-17a2b8?logo=renovatebot)](https://app.renovatebot.com/dashboard) -->
Validate Markdown frontmatter YAML against an associated JSON schema with this remark-lint rule plugin.
Supports:
- Types validation, pattern, enumerations,… and all you can get with JSON Schema
- Code location problems indicator (for IDE to underline)
- Auto-fixes with suggestions
- Command Line Interface reports
- VS Code integration (see below)
- Global patterns or in-file schemas associations
- In JS framework MD / MDX pipelines
Demo
<div align="center"><sup><sub>(w. Astro Content — Editor)</sub></sup>
</div>Jump to:
- 👉 Play with pre-configured ./demo
👉 Play with pre-configured ./demo
Quick shallow clone with:
pnpx degit JulianCataldo/remark-lint-frontmatter-schema/demo ./demo
Installation
Base
pnpm install -D \
remark remark-cli \
remark-frontmatter \
remark-lint-frontmatter-schema
Remove
-D
flag for runtimeunified
MD / MDX pipeline (custom, Astro, Gatsby, etc.), for production.
Keep it if you just want to lint with CLI or your IDE locally, without any production / CI needs.
VS Code (optional)
code --install-extension unifiedjs.vscode-remark
Configuration
CLI / IDE (VS Code) — Static linting
👉 See ./demo folder to get a working, pre-configured, bare project workspace.
You also get example Markdown files and associated schema to play with.
Supports remark-cli
and/or unifiedjs.vscode-remark
extension.
📌 Check out the demo/README.md for bootstrapping it.
Workspace
Create the root config. file for remark
to source from:
touch ./.remarkrc.mjs
Paste this base configuration:
import remarkFrontmatter from 'remark-frontmatter';
import remarkLintFrontmatterSchema from 'remark-lint-frontmatter-schema';
const remarkConfig = {
plugins: [remarkFrontmatter, remarkLintFrontmatterSchema],
};
export default remarkConfig;
You can use YAML / JSON / …, too (uses cosmiconfig).
Schema example
./content/creative-work.schema.yaml
type: object
properties:
title:
type: string
# …
🆕 Add references to external definitions (advanced)
Referencing schema definitions allows re-using bit and piece instead of duplicate them, accross your content schemas.
You can reference an external schema relatively, using $ref
.
For example we can -kind of- merge an host object with a reference properties:
The host schema, content/articles/index.schema.yaml
allOf:
- $ref: ../page.schema.yaml
- properties:
layout:
const: src/layouts/Article.astro
category:
type: string
enum:
- Book
- Movie
foo:
type: string
required:
- layout
- category
A referenced schema, content/page.schema.yaml
properties:
title:
type: string
maxLength: 80
# ...
# ...
required:
- title
The result will be (virtually) the same as this:
properties:
title:
type: string
maxLength: 80
# ...
# ...
layout:
const: src/layouts/Article.astro
category:
type: string
enum:
- Book
- Movie
foo:
type: string
# ...
required:
- title
- layout
- category
Schemas associations
Inspired by VS Code JSON Schema
and redhat.vscode-yaml
conventions.
Inside frontmatter
See ./demo/content files for examples.
Schema association can be done directly inside the frontmatter of the Markdown file,
relative to project root, thanks to the '$schema'
key:
---
# From workspace root (`foo/…`, `/foo/…` or `./foo/…` is the same)
'$schema': content/creative-work.schema.yaml
# —Or— relatively, from this current file directory (`./foo/…` or `../foo/…`)
# '$schema': ../creative-work.schema.yaml
layout: src/layouts/Article.astro
title: Hello there
category: Book
# …
---
# You're welcome!
🌝 My **Markdown** content… 🌚
…
Globally, with patterns
Note:
Locally defined'$schema'
takes precedence over global settings below.
const remarkConfig = {
plugins: [
remarkFrontmatter,
[
remarkLintFrontmatterSchema,
{
schemas: {
/* One schema for many files */
'./content/creative-work.schema.yaml': [
/* Per-file association */
'./content/creative-work/the-shipwreck__global-broken.md',
/* Support glob patterns ———v */
// './content/creative-work/*.md',
// …
// `./` prefix is optional
// 'content/creative-work/foobiz.md',
],
// './content/ghost.schema.yaml': [
// './content/casper.md',
// './content/ether.md',
// ],
},
},
],
],
};
'./foo'
, '/foo'
, 'foo'
, all will work.
It's always relative to your ./.remarkrc.mjs
file, in your workspace root.
CLI usage
Linting whole workspace files (as ./**/*.md
) with remark-cli
:
pnpm remark .
Yields:
Bonus
Validate your schema with JSON meta schema
First, install the YAML for Visual Studio Code extension:
code --install-extension redhat.vscode-yaml
Then, add this to your .vscode/settings.json
:
{
"yaml.schemas": {
"http://json-schema.org/draft-07/schema#": ["content/**/*.schema.yaml"]
}
/* ... */
}
ESLint MDX plugin setup
Will work with the ESLint VS Code extension and the CLI command.
Install the ESLint MDX plugin, the MDX VS Code extension and the ESLint VS Code extension.
Add this dependencies to your project:
pnpm i -D eslint eslint-plugin-mdx \
eslint-plugin-prettier eslint-config-prettier
Add a .eslintrc.cjs
:
/** @type {import("@types/eslint").Linter.Config} */
module.exports = {
overrides: [
{
files: ['*.md', '*.mdx'],
extends: ['plugin:mdx/recommended'],
},
],
};
Add a .remarkrc.yaml
(or JSON, etc.), e.g.:
plugins:
- remark-frontmatter
- - remark-lint-frontmatter-schema
- schemas:
src/schemas/blog-post.schema.yaml:
- content/blog-posts/*.{md,mdx}
# - remark-preset-lint-consistent
# - remark-preset-lint-markdown-style-guide
# - remark-preset-lint-recommended
Result:
Lint with CLI:
pnpm eslint --ext .mdx .
Efforts has been made to have the best output for both remark and ESLint, for IDE extensions and CLIs.
Known issues
- Expected
enum
values suggestions are working with the remark extension, not with the ESLint one. - Similarly, ESLint output will give less details (see screenshot above), and a bit different layout for CLI output, too.
- remark extension seems to load faster, and is more reactive to schema changes.
- As of
eslint-plugin-mdx@2
,.remarkrc.mjs
(ES Module) is not loaded, JSON and YAML configs are fine.
MD / MDX pipeline — Runtime validation
Use it as usual like any remark plugin inside your framework or your custom unified
pipeline.
Custom pipeline
When processing Markdown as single files inside your JS/TS app.
An minimal example is provided in ./demo/pipeline.ts
, you can launch it with pnpm pipeline
from ./demo
.
Schema should be provided programmatically like this:
// …
import remarkFrontmatter from 'remark-frontmatter';
import remarkLintFrontmatterSchema from 'remark-lint-frontmatter-schema';
import type { JSONSchema7 } from 'json-schema';
import { reporter } from 'vfile-reporter';
const mySchema: JSONSchema7 = {
/* … */
};
const output = await unified()
// Your pipeline (basic example)
.use(remarkParse)
// …
.use(remarkFrontmatter)
.use(remarkLintFrontmatterSchema, {
/* Bring your own schema */
embed: mySchema,
})
// …
.use(remarkRehype)
.use(rehypeStringify)
.use(rehypeFormat)
.process(theRawMarkdownLiteral);
/* `path` is for debugging purpose here, as MD literal comes from your app. */
output.path = './the-current-processed-md-file.md';
console.error(reporter([output]));
Yields:
./the-current-processed-md-file.md
1:1 warning Must have required property 'tag' frontmatter-schema remark-lint
⚠ 1 warning
Implementation living example
Checkout Astro Content repository.
<!-- It's a text based, structured content assistant, integrated in Astro, for edition and consumption. --> <!-- file or API based -->Astro Content relies on this library, among others, for providing linting reports.
<!-- You can see **remark-lint-frontmatter-schema** in action, on **[this line, in Astro Content source](outdated)**. -->Important foot-notes for custom pipeline
This is different from static linting, with VS Code extension or CLI.
It will not source .remarkrc
(but you can source it by your own means, if you want).
In fact, it's not aware of your file structure,
nor it will associate or import any schema / Markdown files.
That way, it will integrate easier with your own business logic and existing pipelines.
I found that static linting (during editing) / and runtime validation are two different
uses cases enough to separate them in their setups, but I might converge them partially.
Framework
Warning
WIP. NOT tested yet! It is not a common use case forremark-lint
.
Linting data inside frameworks are generally ignored.
AFAIK,messages
data isn't forwarded to CLI output.
Feel free to open a PR if you have some uses cases in this area that need special care.
Maybe Astro or Astro Content could leverage these linter warnings in the future.
See global patterns schemas
associations for settings reference.
Astro
In astro.config.mjs
// …
export default defineConfig({
// …
remarkPlugins: [
// …
'remark-frontmatter',
['remark-lint-frontmatter-schema', { schemas }],
// …
];
// …
});
Gatsby
In gatsby-config.js
{
// …
plugins: [
// …
{
resolve: 'gatsby-transformer-remark',
options: {
plugins: [
// …
'remark-frontmatter',
['remark-lint-frontmatter-schema', { schemas }],
// …
],
},
},
// …
];
}
Interfaces
export interface Settings {
/**
* Global workspace file associations mapping (for linter extension).
*
* **Example**: `'schemas/thing.schema.yaml': ['content/things/*.md']`
*/
schemas?: Record<string, string[]>;
/**
* Direct schema embedding (for using inside an `unified` transform pipeline).
*
* Format: JSON Schema - draft-2019-09
*
* **Documentation**: https://ajv.js.org/json-schema.html#draft-07
*/
embed?: JSONSchema7;
/**
* **Documentation**: https://ajv.js.org/options.html
*/
ajvOptions?: AjvOptions;
}
export interface FrontmatterSchemaMessage extends VFileMessage {
schema: AjvErrorObject & { url: JSONSchemaReference };
}
Example of a VFileMessage
content you could collect from this lint rule:
[
// …
{
// JS native `Error`
"name": "Markdown YAML frontmatter error (JSON Schema)",
"message": "Keyword: type\nType: string\nSchema path: #/properties/title/type",
// `VFileMessage` (Linter / VS Code…)
"reason": "/clientType: Must be equal to one of the allowed values",
"line": 16,
"column": 13,
"url": "https://github.com/JulianCataldo/remark-lint-frontmatter-schema",
"source": "remark-lint",
"ruleId": "frontmatter-schema",
"position": {
"start": {
"line": 16,
"column": 13
},
"end": {
"line": 16,
"column": 24
}
},
"fatal": false,
"actual": "Individuaaaaaaaal",
"expected": ["Corporate", "Non-profit", "Individual"],
// Condensed string, human readable version of AJV error object
"note": "Keyword: enum\nAllowed values: Corporate, Non-profit, Individual\nSchema path: #/properties/clientType/enum",
// AJV's `ErrorObject`
"schema": {
"url": "https://ajv.js.org/json-schema.html",
"instancePath": "/clientType",
"schemaPath": "#/properties/clientType/enum",
"keyword": "enum",
"params": {
"allowedValues": ["Corporate", "Non-profit", "Individual"]
},
"message": "must be equal to one of the allowed values"
}
}
]
<!-- # Todos -->
Footnotes
100% ESM, including dependencies.
Environments:
- CLI Tool
Remark lint | https://github.com/remarkjs/remark-lint
- IDE Extension (optional)
VS Code
unifiedjs.vscode-remark
https://github.com/remarkjs/vscode-remark
Major dependencies:
ajv
, yaml
, remark
, remark-frontmatter
, unified
, remark-cli
See CHANGELOG.md for release history.
Other projects 👀…
- retext-case-police: Check popular names casing. Example: ⚠️
github
→ ✅GitHub
. - remark-embed: A
remark
plugin for embedding remote / local Markdown or code snippets. - astro-content: A text based, structured content manager, for edition and consumption.
- Web garden: Building blocks for making progressive and future-proof websites.
<div align="center">
Find this project useful?
</div>