Markdown Authoring
Author high-quality Markdown documentation with deterministic structure, lint compliance, and CI integration. Use when writing README files, creating docs pages, fixing markdownlint failures, defining style rules, or wiring markdown checks into pre-commit and pipelines. Keywords: markdown, markdownlint, readme, docs, headings, lists, code fences, links, images, lint config, ci, documentation style.
npx skills add github:pantheon-org/tekhne --skill documentation--markdown-authoringWhen to Use
Use this skill when the task involves Markdown content, linting, or documentation standards.
When Not to Use
Do not use this skill for non-Markdown document formats (for example .adoc or .rst) unless conversion is part of the task.
Principles
- Structure first, wording second.
- Keep Markdown lint-clean and deterministic.
- Use code fences with explicit language tags.
- Keep file-level conventions consistent across the repository.
Deterministic Workflow
- Identify document type (README, guide, API doc, changelog).
- Apply template and section order from references.
- Write content with concise headings and actionable examples.
- Run markdownlint locally and fix violations. Re-run until zero errors are reported. If a violation persists: identify the specific rule, fix the source content or apply a narrow, justified inline exception, then re-lint. Do not proceed until lint is clean.
- Lint-fix loop:
lint fails → identify rule → fix source or add justified exception → re-lint → proceed only when clean
- Lint-fix loop:
- Validate CI/pre-commit integration for regression prevention.
Quick Commands
Lint all Markdown files
rumdl check .
Expected result: no errors for staged or target files. This repository lints with rumdl; for a repository that uses markdownlint, see Lint: CLI.
Lint one skill folder
rumdl check skills/documentation/markdown-authoring
Expected result: folder-specific Markdown issues reported or zero errors.
Run repository checks
bunx @biomejs/biome check .
Expected result: non-Markdown formatting/lint issues also caught.
Evaluate this skill quality
pantheon-skill-auditor evaluate documentation/markdown-authoring --json
Expected result: updated dimension scores and grade.
Read a specific reference
./scripts/read-reference.sh syntax-headings
Expected result: displays reference content for the requested topic.
Check document style compliance
bun run check-style "path/to/document.md"
Expected result: style validation report printed to stdout.
Anti-Patterns
NEVER ship Markdown with missing fence language tags
WHY: Untyped code blocks reduce readability and tooling support.
BAD: Use plain triple backticks for code samples.
GOOD: Use typed fences like ```bash or ```ts.
Consequence: Syntax highlighting and lint checks become inconsistent.
NEVER ignore markdownlint violations by disabling broad rules
WHY: Global suppression hides real quality and maintainability issues.
BAD: Turn off multiple rules to make CI green quickly. GOOD: Fix source content or apply narrow, justified exceptions.
Consequence: Documentation quality degrades release-over-release.
NEVER use heading levels out of sequence
WHY: Skipped heading levels break document hierarchy.
BAD: Jump from ## to #### with no ###.
GOOD: Increase heading levels one step at a time.
Consequence: Navigation and accessibility suffer.
Consider adding a table of contents for documents longer than 300 lines. Optionally include anchor links in headings to improve internal navigation.
NEVER mix inconsistent list and table styles in one document
WHY: Style drift makes docs harder to scan and review.
BAD: Alternate list markers and inconsistent table pipe spacing. GOOD: Keep one list style and normalized table formatting.
Consequence: Diff noise increases and review confidence drops.
References
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
API Documentation
REST API Documentation
Endpoint Structure
## GET /api/users
Retrieves a list of users.
### Authentication
Requires Bearer token authentication.
### Request
```http
GET /api/users?page=1&limit=10 HTTP/1.1
Host: api.example.com
Authorization: Bearer {token}
```
### Query Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `page` | integer | No | 1 | Page number |
| `limit` | integer | No | 20 | Items per page (max 100) |
| `sort` | string | No | `created_at` | Sort field |
| `order` | string | No | `desc` | Sort order (`asc` or `desc`) |
### Response
**Success (200 OK)**
```json
{
"data": [
{
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"created_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"pages": 5
}
}
```
**Error (401 Unauthorized)**
```json
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired token"
}
}
```
### Example
```bash
curl -X GET "https://api.example.com/api/users?page=1&limit=10" \
-H "Authorization: Bearer your-token-here"
```
```javascript
const response = await fetch('https://api.example.com/api/users', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
```
POST Endpoint
## POST /api/users
Creates a new user.
### Authentication
Requires Bearer token with `users:write` scope.
### Request
```http
POST /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer {token}
Content-Type: application/json
{
"email": "newuser@example.com",
"name": "Jane Doe",
"role": "user"
}
```
### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `email` | string | Yes | User's email address (must be valid email) |
| `name` | string | Yes | User's full name (2-100 characters) |
| `role` | string | No | User role: `user`, `admin` (default: `user`) |
### Response
**Success (201 Created)**
```json
{
"data": {
"id": "user_456",
"email": "newuser@example.com",
"name": "Jane Doe",
"role": "user",
"created_at": "2024-01-16T14:20:00Z"
}
}
```
**Error (400 Bad Request)**
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{
"field": "email",
"message": "Email already exists"
}
]
}
}
```
### Example
```bash
curl -X POST "https://api.example.com/api/users" \
-H "Authorization: Bearer your-token-here" \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"name": "Jane Doe"
}'
```
Function Documentation
JavaScript/TypeScript
## fetchUser()
Fetches user data by ID.
### Signature
```typescript
function fetchUser(
userId: string,
options?: FetchOptions
): Promise<User>
```
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `userId` | `string` | Yes | The user's unique identifier |
| `options` | `FetchOptions` | No | Optional configuration |
| `options.timeout` | `number` | No | Request timeout in milliseconds (default: 5000) |
| `options.cache` | `boolean` | No | Enable caching (default: true) |
### Returns
`Promise<User>` - Resolves with user object containing:
- `id` (string): User ID
- `email` (string): User email
- `name` (string): User name
- `created_at` (string): ISO 8601 timestamp
### Throws
- `NotFoundError` - If user doesn't exist
- `TimeoutError` - If request times out
- `AuthError` - If authentication fails
### Example
```javascript
try {
const user = await fetchUser('user_123', {
timeout: 3000,
cache: false
});
console.log(user.name);
} catch (error) {
if (error instanceof NotFoundError) {
console.error('User not found');
}
}
```
### See Also
- [`fetchUsers()`](#fetchusers) - Fetch multiple users
- [`updateUser()`](#updateuser) - Update user data
Python
## fetch_user()
Fetches user data by ID.
### Signature
```python
def fetch_user(
user_id: str,
timeout: int = 5000,
cache: bool = True
) -> User:
```
### Parameters
- **user_id** (`str`): The user's unique identifier
- **timeout** (`int`, optional): Request timeout in milliseconds. Default: `5000`
- **cache** (`bool`, optional): Enable caching. Default: `True`
### Returns
`User`: User object with the following attributes:
- `id` (str): User ID
- `email` (str): User email
- `name` (str): User name
- `created_at` (datetime): Creation timestamp
### Raises
- `NotFoundError`: If user doesn't exist
- `TimeoutError`: If request times out
- `AuthError`: If authentication fails
### Example
```python
try:
user = fetch_user('user_123', timeout=3000, cache=False)
print(user.name)
except NotFoundError:
print('User not found')
```
GraphQL Documentation
## Query: user
Fetches a single user by ID.
### Schema
```graphql
type Query {
user(id: ID!): User
}
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
```
### Arguments
| Argument | Type | Required | Description |
|----------|------|----------|-------------|
| `id` | `ID!` | Yes | User's unique identifier |
### Example
```graphql
query GetUser {
user(id: "user_123") {
id
email
name
posts {
id
title
}
}
}
```
### Response
```json
{
"data": {
"user": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"posts": [
{
"id": "post_456",
"title": "My First Post"
}
]
}
}
}
```
CLI Documentation
## Command: deploy
Deploys the application to production.
### Usage
```bash
myapp deploy [options] <environment>
```
### Arguments
| Argument | Description |
|----------|-------------|
| `environment` | Target environment: `staging`, `production` |
### Options
| Option | Alias | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--region` | `-r` | string | `us-east-1` | AWS region |
| `--skip-tests` | - | boolean | `false` | Skip test suite |
| `--verbose` | `-v` | boolean | `false` | Verbose output |
| `--help` | `-h` | boolean | - | Show help |
### Examples
Deploy to production:
```bash
myapp deploy production
```
Deploy to staging in eu-west-1:
```bash
myapp deploy staging --region eu-west-1
```
Deploy with verbose output:
```bash
myapp deploy production --verbose
```
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `AWS_ACCESS_KEY_ID` | Yes | AWS access key |
| `AWS_SECRET_ACCESS_KEY` | Yes | AWS secret key |
| `DEPLOY_WEBHOOK` | No | Webhook URL for notifications |
### Exit Codes
| Code | Description |
|------|-------------|
| 0 | Success |
| 1 | General error |
| 2 | Configuration error |
| 3 | Deployment failed |
Best Practices
Include All Status Codes
Document all possible HTTP responses:
- 200 OK
- 201 Created
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 500 Internal Server Error
Show Complete Examples
Working, copy-pasteable examples:
# This should work as-is
curl -X GET "https://api.example.com/users" \
-H "Authorization: Bearer token" \
-H "Content-Type: application/json"
Document Error Responses
Show error format and all error codes:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [...]
}
}
Use Tables for Parameters
Organized, scannable format: | Parameter | Type | Required | Default | Description |
Include Rate Limits
If applicable:
### Rate Limits
- 1000 requests per hour per API key
- 100 requests per minute per IP address
Version Your API
Show which version supports what:
## GET /api/v2/users
> Added in v2.0. Replaces [`/api/v1/users`](../v1/users.md)
Link Related Endpoints
Help users discover related functionality:
### Related Endpoints
- [`POST /api/users`](#post-users) - Create user
- [`PATCH /api/users/:id`](#patch-users-id) - Update user
- [`DELETE /api/users/:id`](#delete-users-id) - Delete user
Changelog Documentation
Keep a Changelog Format
Follow the Keep a Changelog specification:
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New features that haven't been released yet
### Changed
- Changes in existing functionality
### Deprecated
- Soon-to-be removed features
### Removed
- Features that have been removed
### Fixed
- Bug fixes
### Security
- Vulnerability patches
## [1.0.0] - 2024-01-15
### Added
- Initial release
- User authentication
- Dashboard view
[Unreleased]: https://github.com/user/repo/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/user/repo/releases/tag/v1.0.0
Changelog Sections
Section Order (Standard)
- Added - New features
- Changed - Changes in existing functionality
- Deprecated - Soon-to-be removed features
- Removed - Removed features
- Fixed - Bug fixes
- Security - Security updates
Writing Good Entries
## [1.2.0] - 2024-03-10
### Added
- User profile page with avatar upload (#234)
- Dark mode support for all components (#245)
- Export data to CSV functionality (#256)
### Changed
- Improved search performance by 40% (#267)
- Updated dependency `library-name` from v2.0 to v3.0 (#278)
### Fixed
- Fixed crash when deleting items with special characters (#289)
- Corrected timezone handling in date picker (#290)
Best Practices
- Write for Users - Focus on user-facing changes, not implementation details
- Be Specific - “Added dark mode” not “Added new feature”
- Include Issue/PR Numbers - Link to GitHub issues/PRs
- Group Similar Changes - Keep related changes together
- Use Present Tense - “Add feature” not “Added feature”
- Keep Unreleased Section - Always maintain an [Unreleased] section
- Date Format - Use ISO 8601 format (YYYY-MM-DD)
- Version Links - Include comparison links at the bottom
Common Mistakes
- Don’t include every commit - summarize user-facing changes
- Don’t use vague descriptions like “Various fixes”
- Don’t forget to move Unreleased items when cutting a release
- Don’t skip version numbers in the comparison links
- Don’t mix internal and user-facing changes
Documentation Organization
File Naming Conventions
Use Kebab Case
installation-guide.md
api-reference.md
getting-started.md
troubleshooting-tips.md
Be Descriptive
Good:
authentication-oauth2.md
database-migration-guide.md
deployment-aws.md
Bad:
auth.md
db.md
deploy.md
Use Prefixes for Grouping
api-endpoints.md
api-authentication.md
api-rate-limiting.md
guide-installation.md
guide-configuration.md
guide-deployment.md
Directory Structure
Simple Project
project/
├── README.md
├── LICENSE
├── CONTRIBUTING.md
└── CHANGELOG.md
Medium Project
project/
├── README.md
├── docs/
│ ├── installation.md
│ ├── usage.md
│ ├── api-reference.md
│ └── troubleshooting.md
├── LICENSE
├── CONTRIBUTING.md
└── CHANGELOG.md
Large Project
project/
├── README.md
├── docs/
│ ├── getting-started/
│ │ ├── installation.md
│ │ ├── quick-start.md
│ │ └── configuration.md
│ ├── guides/
│ │ ├── authentication.md
│ │ ├── deployment.md
│ │ └── testing.md
│ ├── api/
│ │ ├── overview.md
│ │ ├── endpoints.md
│ │ └── examples.md
│ ├── reference/
│ │ ├── cli-commands.md
│ │ ├── configuration-options.md
│ │ └── error-codes.md
│ └── contributing/
│ ├── development-setup.md
│ ├── code-style.md
│ └── pull-request-process.md
├── LICENSE
├── CONTRIBUTING.md
└── CHANGELOG.md
Documentation Categories
Getting Started
New user onboarding:
- Installation
- Quick start
- First steps
- Basic concepts
Guides
Task-oriented documentation:
- How to authenticate
- How to deploy
- How to configure X
- How to troubleshoot Y
API Reference
Technical specifications:
- Endpoints
- Methods
- Parameters
- Return values
- Error codes
Explanations
Conceptual documentation:
- Architecture overview
- Design decisions
- Best practices
- Patterns and anti-patterns
Contributing
Developer documentation:
- Development setup
- Code style
- Testing
- Pull request process
Navigation
Table of Contents
Add to main README or docs/README.md:
# Documentation
## Getting Started
- [Installation](getting-started/installation.md)
- [Quick Start](getting-started/quick-start.md)
- [Configuration](getting-started/configuration.md)
## Guides
- [Authentication](guides/authentication.md)
- [Deployment](guides/deployment.md)
- [Testing](guides/testing.md)
## API Reference
- [Overview](api/overview.md)
- [Endpoints](api/endpoints.md)
- [Examples](api/examples.md)
Breadcrumbs
Add to top of each page:
[Home](../README.md) > [Guides](../guides/README.md) > Authentication
# Authentication Guide
Cross-References
Link related pages:
## Next Steps
- Learn about [deployment](deployment.md)
- See [API reference](../api/overview.md)
- Read about [best practices](best-practices.md)
Linking Between Documents
Relative Links
[Installation Guide](installation.md)
[API Reference](api/overview.md)
[Parent README](../README.md)
Anchor Links
[Jump to Configuration](#configuration)
[See Installation Steps](#installation-steps)
Cross-Document Anchors
[See Authentication in API docs](api/overview.md#authentication)
[Configuration options](configuration.md#database-settings)
Documentation Index
Create docs/README.md or docs/index.md:
# Documentation Index
Welcome to the project documentation!
## For New Users
Start here if you're new:
1. [Installation](installation.md)
2. [Quick Start](quick-start.md)
3. [Basic Tutorial](tutorial.md)
## Documentation by Topic
### Setup and Configuration
- [Installation](installation.md)
- [Configuration](configuration.md)
- [Environment Variables](environment.md)
### Usage
- [Quick Start](quick-start.md)
- [Usage Examples](examples.md)
- [API Reference](api-reference.md)
### Deployment
- [Production Deployment](deployment.md)
- [Docker Setup](docker.md)
- [Cloud Deployment](cloud.md)
### Development
- [Development Setup](development.md)
- [Testing](testing.md)
- [Contributing](../CONTRIBUTING.md)
## Need Help?
- [Troubleshooting](troubleshooting.md)
- [FAQ](faq.md)
- [GitHub Issues](https://github.com/user/project/issues)
Best Practices
Organize by User Journey
Match documentation structure to how users will discover features:
- Getting started (first-time users)
- Guides (users accomplishing specific tasks)
- Reference (users looking up details)
- Advanced topics (experienced users)
Use Consistent Naming
Pick a naming convention and stick to it:
- Kebab case for filenames
- Title case for headings
- Descriptive names over short names
Keep Hierarchy Shallow
Maximum 2-3 levels:
docs/
├── getting-started/ # Level 1
│ └── installation.md # Level 2
└── guides/ # Level 1
└── deployment.md # Level 2
Avoid:
docs/
├── advanced/
│ └── topics/
│ └── performance/
│ └── optimization/
│ └── database.md # Too deep!
Provide Multiple Entry Points
- README for overview
- docs/README.md for full index
- Category indexes (guides/README.md)
- Cross-references between related topics
Keep Navigation Visible
- Table of contents in long documents
- Breadcrumbs for context
- “Next steps” at document end
- Clear links back to main index
Anti-Patterns
Flat Structure with Many Files
Bad:
docs/
├── api-auth.md
├── api-endpoints.md
├── api-errors.md
├── guide-deploy-aws.md
├── guide-deploy-docker.md
├── guide-install-linux.md
├── guide-install-mac.md
├── guide-install-windows.md
└── ... (50 more files)
Good:
docs/
├── api/
│ ├── authentication.md
│ ├── endpoints.md
│ └── errors.md
├── deployment/
│ ├── aws.md
│ └── docker.md
└── installation/
├── linux.md
├── mac.md
└── windows.md
Orphaned Documents
Files with no incoming links:
- Add to table of contents
- Link from related documents
- Or remove if not needed
Duplicate Information
Maintain single source of truth:
- Link instead of duplicating
- Use includes/fragments if supported
- Keep one authoritative document per topic
README Structure
Minimal README Template
For simple projects:
# Project Name
Brief one-sentence description.
## Installation
```bash
npm install project-name
Usage
const project = require('project-name');
project.doSomething();
License
MIT
Comprehensive README Template
For complex projects:
# Project Name
[](ci-url)
[](license-url)
Brief description (1-2 sentences) explaining what the project does and why it exists.
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [API Reference](#api-reference)
- [Contributing](#contributing)
- [License](#license)
## Features
- Feature 1: Brief description
- Feature 2: Brief description
- Feature 3: Brief description
## Installation
### Prerequisites
- Node.js 18+
- PostgreSQL 14+
### Quick Start
```bash
git clone https://github.com/user/project.git
cd project
npm install
cp .env.example .env
npm start
Usage
Basic Example
const Project = require('project-name');
const instance = new Project({
option1: 'value1',
option2: 'value2'
});
instance.doSomething();
Advanced Example
// More complex usage example
Configuration
Configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
option1 |
string |
'default' |
Description |
option2 |
number |
100 |
Description |
API Reference
See API Documentation for detailed API reference.
Contributing
See Contributing Guide for contribution guidelines.
License
This project is licensed under the MIT License - see the project LICENSE file for details.
Acknowledgments
- Credit contributors, inspirations, or related projects
Key Sections
Project Title and Badges
# Project Name
[](url)
[](url)
[](url)
[](url)
Description
1-2 paragraphs explaining:
- What the project does
- Why it exists
- Key benefits
- Target audience
Installation
Step-by-step setup instructions:
- Prerequisites with versions
- Installation commands
- Environment setup
- Verification steps
Usage
- Basic examples first
- Progressive complexity
- Real-world use cases
- Common workflows
Configuration
- All available options
- Default values
- Environment variables
- Configuration files
API Reference
- Link to detailed docs
- Or inline for small APIs
- Examples for each endpoint/method
Contributing
- How to contribute
- Development setup
- Testing requirements
- Code style guidelines
License
- License type
- Link to LICENSE file
Best Practices
Start with Why
Explain the problem before the solution:
# DataSync
**Problem:** Keeping multiple databases in sync is complex and error-prone.
**Solution:** DataSync provides real-time bi-directional synchronization with automatic conflict resolution.
Show, Don’t Tell
Use examples instead of descriptions:
Bad:
This library makes API calls easier.
Good:
```javascript
// Before
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
// After
const users = await api.get('/users');
```javascript
Quick Start First
Get users running quickly:
## Quick Start
```bash
npx create-my-app my-project
cd my-project
npm start
Open http://localhost:3000 to see your app.
That’s it! See installation for more options.
Progressive Disclosure
Organize from simple to complex:
- Quick start
- Basic usage
- Advanced features
- API reference
Common README Files
Application README
Focus on:
- Setup and deployment
- Environment configuration
- Running locally
- Testing
- Production deployment
Library/Package README
Focus on:
- Installation
- API documentation
- Code examples
- Browser/Node compatibility
- TypeScript types
Tool/CLI README
Focus on:
- Installation
- Command reference
- Common workflows
- Configuration
- Examples
Anti-Patterns
Too Much, Too Soon
Bad:
# Project
This comprehensive enterprise-grade solution leverages cutting-edge technologies...
[500 lines of detailed technical architecture]
Good:
# Project
A fast, type-safe ORM for Node.js.
```bash
npm install project
const users = await db.users.findMany();
### No Examples
**Bad:**
```markdown
## Usage
Install the package and import it into your project. Configure as needed.
Good:
## Usage
```javascript
import { createClient } from 'project';
const client = createClient({
apiKey: process.env.API_KEY
});
const data = await client.fetch('/users');
```javascript
Outdated Information
- Keep examples tested and working
- Update version numbers
- Remove deprecated features
- Update screenshots
- Test all installation steps
README Checklist
- Clear project title
- One-sentence description
- Installation instructions
- Working code example
- Link to documentation
- License information
- Contact/support information
- All examples tested and working
- No broken links
- No outdated information
Writing Style Guide
General Principles
Be Concise
Bad:
In order to be able to use this feature, you will need to first install the package and then configure it properly before you can actually start using it.
Good:
Install and configure the package before use.
Use Active Voice
Bad:
The file is read by the parser and the data is extracted.
Good:
The parser reads the file and extracts the data.
Address the Reader Directly
Bad:
One should configure the database before starting the application.
Good:
Configure the database before starting the application.
Or:
You should configure the database before starting the application.
Use Present Tense
Bad:
The function will return a promise that will resolve with the data.
Good:
The function returns a promise that resolves with the data.
Audience Considerations
Know Your Audience
Beginners
- Explain concepts thoroughly
- Avoid jargon or define it
- Provide context
- Include more examples
Example:
## Installation
npm (Node Package Manager) is included with Node.js. To install this package, run:
```bash
npm install package-name
This downloads the package and adds it to your project’s dependencies.
Advanced Users
- Be concise
- Use technical terms
- Focus on edge cases
- Link to detailed references
Example:
## Installation
```bash
npm install package-name
Supports Node.js 18+ with ESM and CJS. See the changelog for upgrading from v1.x.
Progressive Disclosure
Start simple, add complexity gradually:
## Basic Usage
```javascript
const result = api.fetch('/users');
Advanced Usage
const result = await api.fetch('/users', {
timeout: 5000,
retries: 3,
cache: true
});
Expert Usage
For fine-grained control, use the low-level API:
const client = api.createClient();
client.configure({ /* ... */ });
const result = await client.request({ /* ... */ });
```javascript
Technical Writing
Be Specific
Bad:
Configure the settings appropriately.
Good:
Set `timeout` to `5000` and `retries` to `3`.
Use Examples
Bad:
The function accepts various options for configuration.
Good:
```javascript
fetchData({
timeout: 5000, // Request timeout in milliseconds
retries: 3, // Number of retry attempts
cache: true // Enable response caching
});
### Define Technical Terms
First use:
```markdown
Use JWT (JSON Web Token) for authentication. JWTs are...
Subsequent uses:
The JWT contains...
Avoid Ambiguity
Bad:
Run the script before deployment.
Good:
Run `npm run build` before deploying to production.
Code Documentation
Inline vs. Block Documentation
Inline Comments
For single lines:
const timeout = 5000; // Connection timeout in milliseconds
Block Comments
For functions, classes, or complex logic:
/**
* Fetches user data from the API.
*
* @param {string} userId - The user's unique identifier
* @param {Object} options - Optional configuration
* @param {number} options.timeout - Request timeout in milliseconds
* @returns {Promise<User>} The user object
* @throws {NotFoundError} If user doesn't exist
*/
async function fetchUser(userId, options = {}) {
// Implementation
}
Command Examples
Show commands with expected output:
```bash
npm test
```
```
> project@1.0.0 test
> jest
PASS tests/api.test.js
✓ should fetch users (45ms)
✓ should handle errors (12ms)
Tests: 2 passed, 2 total
```
Configuration Examples
Show complete, working examples:
```javascript
// config.js
module.exports = {
database: {
host: 'localhost',
port: 5432,
name: 'myapp'
},
server: {
port: 3000,
cors: true
}
};
```
Formatting Guidelines
Headings
- Use sentence case: “Getting started” not “Getting Started”
- Be descriptive: “Configure database connection” not “Setup”
- Use parallel structure in lists of headings
Lists
Start items with same part of speech:
Bad:
- Installing dependencies
- Configuration
- To run the tests
Good:
- Install dependencies
- Configure settings
- Run tests
Emphasis
Use sparingly:
- Bold for important terms or actions
- Italic for emphasis or new terms
Codefor code references
Bad:
**IMPORTANT:** You **MUST** run `npm install` **before** starting!
Good:
Run `npm install` before starting.
Tone
Professional but Friendly
Too Formal:
One must ensure that the appropriate configuration has been applied prior to initialization.
Too Casual:
Just slap in some config and you're good to go!
Just Right:
Configure the application before starting it.
Avoid Unnecessary Words
Wordy:
In order to be able to use this feature...
Concise:
To use this feature...
Wordy:
It is important to note that...
Concise:
Note:
Positive Language
Negative:
Don't forget to configure the database or the application won't work.
Positive:
Configure the database to ensure the application works correctly.
Common Mistakes
Assuming Knowledge
Bad:
Use the CLI to scaffold the project.
Good:
Use the command-line interface (CLI) to generate project files:
```bash
npx create-app my-project
### Vague Instructions
**Bad:**
```markdown
Update the configuration as needed.
Good:
Update `config.json`:
```json
{
"apiKey": "your-api-key-here",
"timeout": 5000
}
### Missing Context
**Bad:**
```markdown
Run the migration:
```bash
npm run migrate
**Good:**
Apply database migrations to create the required tables:
```bash
npm run migrate
This creates the users and posts tables in your database.
Best Practices
- Write for scanability: Use headings, lists, and short paragraphs
- Show, don’t tell: Use examples instead of descriptions
- Be consistent: Use same terms, formatting, and structure
- Test your docs: Follow your own instructions to verify they work
- Keep it updated: Review and update documentation regularly
- Get feedback: Have others review your documentation
- Use templates: Create templates for common documentation types
Checklist
- Concise and clear
- Active voice
- Present tense
- Addresses reader directly
- Appropriate for audience
- Specific and detailed
- Includes working examples
- Technical terms defined
- No ambiguity
- Consistent terminology
- Proper formatting
- Professional tone
- All instructions tested
Markdownlint Programmatic API
Installation
npm install markdownlint
Basic Usage
Callback-Based API
const markdownlint = require('markdownlint');
const options = {
files: ['README.md', 'docs/**/*.md'],
config: {
default: true,
MD013: { line_length: 120 }
}
};
markdownlint(options, (err, result) => {
if (err) {
console.error(err);
return;
}
console.log(result.toString());
// README.md: 23: MD013/line-length Line length [Expected: 120; Actual: 145]
});
Promise-Based API
const { markdownlint } = require('markdownlint/promise');
async function lintFiles() {
const options = {
files: ['README.md', 'docs/**/*.md'],
config: {
default: true,
MD013: { line_length: 120 }
}
};
try {
const result = await markdownlint(options);
console.log(result.toString());
} catch (err) {
console.error(err);
}
}
lintFiles();
Synchronous API
const { markdownlintSync } = require('markdownlint/sync');
const options = {
files: ['README.md'],
config: {
default: true
}
};
const result = markdownlintSync(options);
console.log(result.toString());
Options Object
Basic Options
const options = {
// Files to lint (glob patterns supported)
files: ['**/*.md'],
// Or strings to lint
strings: {
'content1': '# Heading\n\nContent here',
'content2': '# Another\n\nMore content'
},
// Configuration object
config: {
default: true,
MD013: { line_length: 120 }
},
// Custom rules
customRules: [
require('./custom-rule')
],
// Directories to search for rules
markdownItPlugins: [
[require('markdown-it-emoji')]
]
};
Advanced Options
const options = {
files: ['README.md'],
config: {
default: true,
MD013: false
},
// Front matter pattern (RegExp or array of RegExp)
frontMatter: /^---[\s\S]*?---$/,
// Enable/disable inline configuration
noInlineConfig: false,
// Results version for compatibility
resultVersion: 3,
// Handle results
handleRuleFailures: true
};
Working with Results
Result Object Structure
const result = await markdownlint(options);
// result is an object with filename keys:
// {
// 'README.md': [
// {
// lineNumber: 23,
// ruleNames: ['MD013', 'line-length'],
// ruleDescription: 'Line length',
// ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md013',
// errorDetail: 'Expected: 120; Actual: 145',
// errorContext: 'This is a very long line that...',
// errorRange: [1, 145],
// fixInfo: null
// }
// ]
// }
Processing Results
const result = await markdownlint(options);
// Check if any errors found
const hasErrors = Object.keys(result).some(
file => result[file].length > 0
);
if (hasErrors) {
console.error('Linting errors found:');
Object.keys(result).forEach(file => {
const errors = result[file];
if (errors.length > 0) {
console.error(`\n${file}:`);
errors.forEach(error => {
console.error(
` ${error.lineNumber}: ${error.ruleNames[0]} ` +
`${error.ruleDescription} ${error.errorDetail || ''}`
);
});
}
});
process.exit(1);
}
Counting Errors
const result = await markdownlint(options);
let totalErrors = 0;
const errorsByRule = {};
Object.values(result).forEach(fileErrors => {
totalErrors += fileErrors.length;
fileErrors.forEach(error => {
const ruleName = error.ruleNames[0];
errorsByRule[ruleName] = (errorsByRule[ruleName] || 0) + 1;
});
});
console.log(`Total errors: ${totalErrors}`);
console.log('Errors by rule:', errorsByRule);
Linting Strings
Single String
const { markdownlint } = require('markdownlint/promise');
async function lintString(content) {
const result = await markdownlint({
strings: {
'content': content
},
config: {
default: true
}
});
return result.content || [];
}
const errors = await lintString('# Heading\n\nSome content');
Multiple Strings
const result = await markdownlint({
strings: {
'file1': '# First\n\nContent',
'file2': '# Second\n\nMore content',
'file3': '# Third\n\nEven more'
},
config: {
default: true
}
});
// Access results by key
console.log(result.file1);
console.log(result.file2);
console.log(result.file3);
Reading Configuration
Load Config from File
const { readConfig } = require('markdownlint/sync');
// Read .markdownlint.json
const config = readConfigSync('.markdownlint.json');
const result = await markdownlint({
files: ['README.md'],
config: config
});
Merge Configurations
const baseConfig = readConfigSync('.markdownlint.json');
const overrides = {
MD013: { line_length: 150 }
};
const config = { ...baseConfig, ...overrides };
const result = await markdownlint({
files: ['README.md'],
config: config
});
Applying Fixes
Get Fix Information
const result = await markdownlint(options);
Object.keys(result).forEach(file => {
result[file].forEach(error => {
if (error.fixInfo) {
console.log(`${file}:${error.lineNumber} can be auto-fixed`);
}
});
});
Apply Fixes
const { markdownlint } = require('markdownlint/promise');
const { applyFixes } = require('markdownlint');
const fs = require('fs').promises;
async function lintAndFix(filePath) {
// Read file
const content = await fs.readFile(filePath, 'utf8');
// Lint
const result = await markdownlint({
strings: {
[filePath]: content
},
config: {
default: true
}
});
// Apply fixes
const fixed = applyFixes(content, result[filePath]);
// Write back
await fs.writeFile(filePath, fixed, 'utf8');
console.log(`Fixed ${filePath}`);
}
await lintAndFix('README.md');
Bulk Fix Multiple Files
const { markdownlint } = require('markdownlint/promise');
const { applyFixes } = require('markdownlint');
const fs = require('fs').promises;
const glob = require('glob');
async function fixAllFiles(pattern) {
const files = glob.sync(pattern);
for (const file of files) {
const content = await fs.readFile(file, 'utf8');
const result = await markdownlint({
strings: { [file]: content },
config: { default: true }
});
if (result[file] && result[file].length > 0) {
const fixed = applyFixes(content, result[file]);
if (fixed !== content) {
await fs.writeFile(file, fixed, 'utf8');
console.log(`Fixed ${file}`);
}
}
}
}
await fixAllFiles('docs/**/*.md');
Custom Rules
Creating a Custom Rule
// custom-no-lorem.js
module.exports = {
names: ['no-lorem'],
description: 'Disallow lorem ipsum text',
tags: ['test'],
function: (params, onError) => {
params.tokens.forEach(token => {
if (token.type === 'inline') {
const content = token.content.toLowerCase();
if (content.includes('lorem ipsum')) {
onError({
lineNumber: token.lineNumber,
detail: 'Found "lorem ipsum" placeholder text',
context: token.line
});
}
}
});
}
};
Using Custom Rules
const customRule = require('./custom-no-lorem');
const result = await markdownlint({
files: ['README.md'],
config: {
default: true
},
customRules: [customRule]
});
Integration Examples
Express.js API Endpoint
const express = require('express');
const { markdownlint } = require('markdownlint/promise');
const app = express();
app.use(express.json());
app.post('/api/lint', async (req, res) => {
const { content } = req.body;
try {
const result = await markdownlint({
strings: { 'input': content },
config: { default: true }
});
res.json({
errors: result.input || [],
success: (result.input || []).length === 0
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000);
Build Tool Integration
// gulpfile.js
const gulp = require('gulp');
const { markdownlint } = require('markdownlint/promise');
const through2 = require('through2');
function lintMarkdown() {
return gulp.src('**/*.md')
.pipe(through2.obj(async (file, enc, callback) => {
const result = await markdownlint({
strings: {
[file.path]: file.contents.toString()
},
config: { default: true }
});
const errors = result[file.path] || [];
if (errors.length > 0) {
console.error(`${file.path}:`);
errors.forEach(err => {
console.error(` ${err.lineNumber}: ${err.ruleDescription}`);
});
}
callback(null, file);
}));
}
gulp.task('lint:md', lintMarkdown);
Best Practices
- Use Promise API - Cleaner async/await syntax
- Cache Config - Load configuration once and reuse
- Batch Operations - Lint multiple files in one call
- Handle Errors - Always catch and handle errors properly
- Apply Fixes Carefully - Test fixes before writing to disk
- Custom Rules - Create project-specific validation rules
- Report Formatting - Format results for readability
Common Pitfalls
- Async/Sync Mismatch - Don’t mix callback, promise, and sync APIs
- Missing Config - Always provide a config object
- Forgetting to Handle Errors - Check result object for errors
- Not Testing Fixes - Verify fixes don’t break content
- Memory Issues - Don’t load all files at once for large projects
- Config Caching - Don’t reload config on every lint call
Markdownlint CI/CD Integration
GitHub Actions
Basic Workflow
name: Markdown Lint
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint markdown files
run: npm run lint:md
Using markdownlint-cli2-action
name: Markdown Lint
on: [push, pull_request]
jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint markdown
uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: |
**/*.md
#node_modules
With Auto-fix and Commit
name: Markdown Lint & Fix
on:
pull_request:
branches: [main]
jobs:
markdown-lint-fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Fix markdown issues
run: npm run lint:md:fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'docs: auto-fix markdown lint issues'
file_pattern: '*.md'
Matrix Strategy (Multiple Node Versions)
name: Markdown Lint
on: [push, pull_request]
jobs:
markdown-lint:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint:md
GitLab CI
Basic Pipeline
stages:
- test
markdown-lint:
stage: test
image: node:20
before_script:
- npm ci
script:
- npm run lint:md
only:
- merge_requests
- main
- develop
With Caching
markdown-lint:
stage: test
image: node:20
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- npm ci
script:
- npm run lint:md
artifacts:
reports:
junit: markdown-lint-report.xml
when: always
With Auto-fix
markdown-lint-fix:
stage: test
image: node:20
before_script:
- npm ci
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
script:
- npm run lint:md:fix
- |
if [[ -n $(git status -s) ]]; then
git add "*.md"
git commit -m "docs: auto-fix markdown lint issues [skip ci]"
git push "https://oauth2:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:${CI_COMMIT_REF_NAME}
fi
only:
- merge_requests
CircleCI
version: 2.1
executors:
node-executor:
docker:
- image: cimg/node:20.0
jobs:
markdown-lint:
executor: node-executor
steps:
- checkout
- restore_cache:
keys:
- node-deps-{{ checksum "package-lock.json" }}
- run:
name: Install dependencies
command: npm ci
- save_cache:
key: node-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Lint markdown
command: npm run lint:md
workflows:
version: 2
test:
jobs:
- markdown-lint
Jenkins
Declarative Pipeline
pipeline {
agent any
tools {
nodejs 'NodeJS 20'
}
stages {
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Lint Markdown') {
steps {
sh 'npm run lint:md'
}
}
}
post {
always {
cleanWs()
}
failure {
emailext(
subject: "Markdown Lint Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check console output at ${env.BUILD_URL}",
to: "team@example.com"
)
}
}
}
Scripted Pipeline
node {
stage('Checkout') {
checkout scm
}
stage('Install') {
nodejs(nodeJSInstallationName: 'NodeJS 20') {
sh 'npm ci'
}
}
stage('Lint Markdown') {
nodejs(nodeJSInstallationName: 'NodeJS 20') {
sh 'npm run lint:md'
}
}
}
Azure Pipelines
trigger:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Install Node.js'
- script: npm ci
displayName: 'Install dependencies'
- script: npm run lint:md
displayName: 'Lint markdown files'
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/markdown-lint-report.xml'
failTaskOnFailedTests: true
Pre-commit Hooks
Husky + lint-staged
package.json:
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.md": [
"markdownlint-cli2 --fix",
"git add"
]
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"markdownlint-cli2": "^0.12.0"
}
}
.husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
Setup:
npm install -D husky lint-staged
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
pre-commit Framework
.pre-commit-config.yaml:
repos:
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.12.1
hooks:
- id: markdownlint-cli2
args: ['--fix']
Setup:
pip install pre-commit
pre-commit install
Docker Integration
Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run lint:md
docker-compose.yml
version: '3.8'
services:
markdown-lint:
image: node:20-alpine
working_dir: /app
volumes:
- .:/app
command: sh -c "npm ci && npm run lint:md"
Usage:
docker-compose run markdown-lint
Monorepo Integration
Turborepo
turbo.json:
{
"pipeline": {
"lint:md": {
"outputs": [],
"cache": true
}
}
}
package.json (root):
{
"scripts": {
"lint:md": "turbo run lint:md"
}
}
packages/docs/package.json:
{
"scripts": {
"lint:md": "markdownlint-cli2 \"**/*.md\""
}
}
Lerna
lerna.json:
{
"version": "independent",
"npmClient": "npm",
"command": {
"run": {
"stream": true
}
}
}
package.json (root):
{
"scripts": {
"lint:md": "lerna run lint:md"
}
}
Reporting and Metrics
Generate HTML Report
// scripts/markdown-lint-report.js
const { markdownlint } = require('markdownlint/promise');
const fs = require('fs').promises;
async function generateReport() {
const result = await markdownlint({
files: ['**/*.md'],
config: { default: true }
});
let html = '<html><body><h1>Markdown Lint Report</h1>';
Object.keys(result).forEach(file => {
const errors = result[file];
if (errors.length > 0) {
html += `<h2>${file}</h2><ul>`;
errors.forEach(err => {
html += `<li>Line ${err.lineNumber}: ${err.ruleDescription}</li>`;
});
html += '</ul>';
}
});
html += '</body></html>';
await fs.writeFile('markdown-lint-report.html', html);
}
generateReport();
JSON Output for CI
package.json:
{
"scripts": {
"lint:md:json": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\""
}
}
.markdownlint-cli2.jsonc:
{
"outputFormatters": [
["markdownlint-cli2-formatter-json", { "path": "markdown-lint-report.json" }]
]
}
Best Practices
- CI Integration - Run markdown linting on every PR
- Pre-commit Hooks - Catch errors before commit
- Auto-fix in Development - Use
--fixlocally, not in CI - Fail Fast - Exit with error code on lint failures
- Cache Dependencies - Speed up CI with npm/node_modules caching
- Ignore Generated Files - Exclude
node_modules,dist, etc. - Version Lock - Pin markdownlint version in package.json
- Separate Jobs - Keep linting separate from tests/builds
- Report Artifacts - Save lint reports as build artifacts
- Matrix Testing - Test on multiple OS/Node versions if needed
Common Pitfalls
- Missing Ignore Patterns - Forgetting to ignore
node_modules - Wrong Glob Patterns - Using incorrect file patterns
- Config Not Found - Config file not in expected location
- CI Timeout - Linting too many files without exclusions
- No Exit Code Check - Not failing build on lint errors
- Auto-fix in CI - Using
--fixin CI can cause unexpected commits - Cache Poisoning - Not invalidating cache when config changes
- Slow Builds - Not using caching or parallel jobs
Markdownlint CLI Usage
Installation
# markdownlint-cli2 (recommended)
npm install -D markdownlint-cli2
# Original markdownlint-cli
npm install -D markdownlint-cli
Basic Usage
Lint Files
# Lint all markdown files
markdownlint-cli2 "**/*.md"
# Lint specific files
markdownlint-cli2 README.md CONTRIBUTING.md
# Lint with pattern
markdownlint-cli2 "docs/**/*.md"
Fix Issues Automatically
# Fix all auto-fixable issues
markdownlint-cli2 --fix "**/*.md"
# Fix specific files
markdownlint-cli2 --fix README.md
Ignore Patterns
# Ignore node_modules and dist
markdownlint-cli2 "**/*.md" "#node_modules" "#dist"
# Multiple ignore patterns
markdownlint-cli2 "**/*.md" "#node_modules" "#**/vendor/**" "#CHANGELOG.md"
Configuration
Using Config Files
# Uses .markdownlint-cli2.jsonc if present
markdownlint-cli2 "**/*.md"
# Specify config file
markdownlint-cli2 --config custom-config.json "**/*.md"
CLI Options
# Show help
markdownlint-cli2 --help
# Show version
markdownlint-cli2 --version
# Fix issues
markdownlint-cli2 --fix "**/*.md"
# Custom config
markdownlint-cli2 --config .markdownlint.json "**/*.md"
# No config (use defaults)
markdownlint-cli2 --no-config "**/*.md"
Output Formats
Default Output
markdownlint-cli2 "**/*.md"
# Output:
# README.md:23 MD013/line-length Line length [Expected: 80; Actual: 125]
# docs/api.md:45 MD040/fenced-code-language Fenced code blocks should have a language specified
JSON Output
markdownlint-cli2 --config .markdownlint-cli2.jsonc "**/*.md"
.markdownlint-cli2.jsonc:
{
"outputFormatters": [
["markdownlint-cli2-formatter-json"]
]
}
Summary Output
# Summary at end
markdownlint-cli2 "**/*.md"
# Output includes:
# Found 15 error(s) in 5 file(s)
Exit Codes
0- No errors found1- Errors found2- Invalid command-line arguments
Use in CI/CD:
#!/bin/bash
markdownlint-cli2 "**/*.md"
exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "Markdown linting failed!"
exit 1
fi
NPM Scripts Integration
package.json
{
"scripts": {
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\"",
"lint": "npm run lint:md && npm run lint:js",
"format:md": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\""
}
}
Usage:
npm run lint:md
npm run lint:md:fix
CI/CD Integration
GitHub Actions
name: Lint Markdown
on: [push, pull_request]
jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Lint markdown
run: npm run lint:md
GitLab CI
markdown-lint:
image: node:20
stage: test
script:
- npm ci
- npm run lint:md
only:
- merge_requests
- main
Pre-commit Hook
Using Husky + lint-staged:
package.json:
{
"lint-staged": {
"*.md": "markdownlint-cli2 --fix"
}
}
.husky/pre-commit:
#!/bin/sh
npx lint-staged
Common Patterns
Lint All Except Generated Files
markdownlint-cli2 "**/*.md" "#node_modules" "#dist" "#coverage" "#.next"
Lint Only Documentation
markdownlint-cli2 "docs/**/*.md" "README.md" "CONTRIBUTING.md"
Lint and Fix in One Command
markdownlint-cli2 --fix "**/*.md" "#node_modules" || true
The || true prevents the script from failing in CI if there are non-fixable errors.
Check Staged Files Only
# In pre-commit hook
git diff --cached --name-only --diff-filter=ACMR "*.md" | xargs markdownlint-cli2 --fix
Monorepo Usage
Root Configuration
.markdownlint-cli2.jsonc:
{
"globs": [
"**/*.md"
],
"ignores": [
"**/node_modules/**",
"**/dist/**",
"packages/*/CHANGELOG.md"
],
"config": {
"default": true,
"MD013": { "line_length": 120 }
}
}
Package Scripts
package.json (root):
{
"scripts": {
"lint:md": "markdownlint-cli2",
"lint:md:fix": "markdownlint-cli2 --fix"
}
}
packages/app/package.json:
{
"scripts": {
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\""
}
}
Troubleshooting
No Files Found
# Issue: Pattern doesn't match
markdownlint-cli2 "*.md" # Only matches current directory
# Solution: Use recursive pattern
markdownlint-cli2 "**/*.md"
Config Not Found
# Issue: Config file not detected
markdownlint-cli2 "**/*.md"
# Solution: Specify config explicitly
markdownlint-cli2 --config .markdownlint.json "**/*.md"
Too Many Files
# Issue: Argument list too long
markdownlint-cli2 $(find . -name "*.md")
# Solution: Use glob pattern
markdownlint-cli2 "**/*.md"
Performance Issues
# Issue: Slow on large repos
# Solution: Limit scope
markdownlint-cli2 "src/**/*.md" "docs/**/*.md"
# Or: Exclude large directories
markdownlint-cli2 "**/*.md" "#vendor" "#node_modules" "#.git"
Best Practices
- Use Config Files - Store configuration in
.markdownlint-cli2.jsonc - Integrate with CI - Fail builds on linting errors
- Pre-commit Hooks - Catch errors before commit
- Auto-fix When Possible - Use
--fixin development - Ignore Generated Files - Exclude
node_modules,dist, etc. - Version Lock - Pin markdownlint-cli2 version in package.json
- Document Exceptions - Comment why certain files are ignored
- Fail Fast - Don’t use
|| truein CI pipelines
Common Pitfalls
- Wrong Glob Pattern - Use
"**/*.md"not*.md - Missing Quotes - Always quote patterns:
"**/*.md" - Forgetting Ignores - Don’t forget to ignore
node_modules - No Exit Code Check - Always check exit code in scripts
- Slow CI - Exclude unnecessary directories
- Config Not Applied - Verify config file location and name
- Auto-fix in CI - Don’t use
--fixin CI, only in development
Markdownlint Configuration
Installation
# npm
npm install -D markdownlint-cli2
# pnpm
pnpm add -D markdownlint-cli2
# yarn
yarn add -D markdownlint-cli2
Configuration Files
.markdownlint.json
Basic JSON configuration:
{
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json",
"default": true,
"MD003": { "style": "atx" },
"MD004": { "style": "dash" },
"MD007": { "indent": 2 },
"MD013": {
"line_length": 120,
"code_blocks": false,
"tables": false
},
"MD033": {
"allowed_elements": ["br", "img", "details", "summary"]
},
"MD041": true
}
.markdownlint.jsonc
JSONC allows comments:
{
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json",
// Enable all rules by default
"default": true,
// Use ATX-style headings (# Heading)
"MD003": { "style": "atx" },
// Use dashes for unordered lists
"MD004": { "style": "dash" },
// Allow longer lines for code blocks
"MD013": {
"line_length": 120,
"code_blocks": false
}
}
.markdownlint.yaml
YAML configuration:
# Enable all rules by default
default: true
# Heading style
MD003:
style: atx
# Unordered list style
MD004:
style: dash
# Line length
MD013:
line_length: 120
code_blocks: false
tables: false
# Allowed HTML elements
MD033:
allowed_elements:
- br
- img
- details
- summary
.markdownlint.cjs (JavaScript)
module.exports = {
default: true,
MD003: { style: 'atx' },
MD004: { style: 'dash' },
MD013: {
line_length: 120,
code_blocks: false,
tables: false
},
MD033: {
allowed_elements: ['br', 'img', 'details', 'summary']
}
};
Configuration Hierarchy
Markdownlint searches for configuration in this order:
.markdownlint-cli2.jsonc(CLI-specific).markdownlint-cli2.yaml.markdownlint-cli2.cjs.markdownlint.jsonc(markdownlint rules).markdownlint.json.markdownlint.yaml.markdownlint.yml.markdownlint.cjs
Configuration Inheritance
Extending Base Config
{
"extends": "../.markdownlint.json",
"MD013": false
}
Package-based Extends
{
"extends": "markdownlint-config-standard"
}
CLI-Specific Configuration
.markdownlint-cli2.jsonc
{
"config": {
// Markdownlint rules configuration
"default": true,
"MD013": { "line_length": 120 }
},
"globs": [
"**/*.md"
],
"ignores": [
"**/node_modules/**",
"**/dist/**",
"CHANGELOG.md"
],
"fix": false,
"outputFormatters": [
[
"markdownlint-cli2-formatter-default"
]
]
}
Inline Configuration
Disable All Rules for File
<!-- markdownlint-disable-file -->
# This file is not linted
Disable Specific Rules
<!-- markdownlint-disable MD013 MD033 -->
This line can be very long and contain <span>HTML</span>.
<!-- markdownlint-enable MD013 MD033 -->
Disable for Single Line
This is a normal line.
<!-- markdownlint-disable-next-line MD013 -->
This is a very long line that would normally trigger MD013 but is explicitly allowed.
Or:
This is a very long line that would normally trigger MD013 but is explicitly allowed. <!-- markdownlint-disable-line MD013 -->
Capture and Restore
<!-- markdownlint-capture -->
<!-- markdownlint-disable MD013 -->
Long content here...
<!-- markdownlint-restore -->
Normal linting resumes.
Configure Inline
<!-- markdownlint-configure-file { "MD013": { "line_length": 150 } } -->
# Rest of file uses 150 character line length
Common Configuration Patterns
Strict Documentation
{
"default": true,
"MD013": {
"line_length": 80,
"code_blocks": false
},
"MD033": false,
"MD041": true,
"MD046": { "style": "fenced" },
"MD048": { "style": "backtick" }
}
Relaxed Blog Style
{
"default": true,
"MD013": false,
"MD033": {
"allowed_elements": ["br", "img", "iframe", "div", "span"]
},
"MD041": false
}
Technical README
{
"default": true,
"MD013": {
"line_length": 120,
"code_blocks": false,
"tables": false
},
"MD033": {
"allowed_elements": ["br", "details", "summary", "img"]
},
"MD024": { "siblings_only": true },
"MD041": true
}
Monorepo Configuration
Root .markdownlint.json:
{
"default": true,
"MD013": { "line_length": 100 }
}
packages/docs/.markdownlint.json:
{
"extends": "../../.markdownlint.json",
"MD013": { "line_length": 120 }
}
Per-Directory Configuration
project/
├── .markdownlint.json # Base config
├── docs/
│ ├── .markdownlint.json # Overrides for docs
│ └── api.md
└── blog/
├── .markdownlint.json # Overrides for blog
└── post.md
Schema Validation
Always include the schema for IDE support:
{
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json",
"default": true
}
This enables:
- Autocomplete in VS Code and other editors
- Validation of rule names and parameters
- Documentation on hover
Best Practices
- Start with Default - Enable
"default": trueand disable specific rules - Use Schema - Always include
$schemafor IDE support - Document Exceptions - Comment why rules are disabled
- Version Control - Commit configuration files to repo
- Extend Base Config - Use
extendsfor shared configurations - Per-Project Tuning - Adjust line length and HTML rules per project type
- Inline Sparingly - Prefer fixing issues over disabling rules inline
Common Pitfalls
- JSON Syntax Errors - Use JSONC for comments
- Wrong Rule Names - Use schema validation to catch typos
- Conflicting Rules - Some rules conflict (e.g., MD046 and MD031)
- Missing Schema - Without schema, no autocomplete or validation
- Incorrect Paths - In monorepos, verify
extendspaths are correct - Over-configuration - Don’t disable rules without good reason
Markdownlint Rules Reference
Core Rule Categories
Document Structure Rules
MD001 - heading-increment
- Heading levels should increment by one level at a time
- Don’t skip from H1 to H3
MD022 - blanks-around-headings
- Headings should be surrounded by blank lines
MD025 - single-title/single-h1
- Multiple top-level headings in the same document
MD041 - first-line-heading/first-line-h1
- First line in a file should be a top-level heading
Heading Style Rules
MD003 - heading-style
{
"MD003": {
"style": "atx" // or "atx_closed", "setext", "setext_with_atx"
}
}
MD043 - required-headings
- Required heading structure
List Rules
MD004 - ul-style
{
"MD004": {
"style": "dash" // or "asterisk", "plus", "consistent"
}
}
MD005 - list-indent
- Inconsistent indentation for list items at the same level
MD007 - ul-indent
{
"MD007": {
"indent": 2,
"start_indented": false,
"start_indent": 2
}
}
MD029 - ol-prefix
{
"MD029": {
"style": "ordered" // or "one", "zero", "one_or_ordered"
}
}
MD030 - list-marker-space
- Spaces after list markers
MD032 - blanks-around-lists
- Lists should be surrounded by blank lines
Code Block Rules
MD031 - blanks-around-fences
- Fenced code blocks should be surrounded by blank lines
MD040 - fenced-code-language
- Fenced code blocks should have a language specified
MD046 - code-block-style
{
"MD046": {
"style": "fenced" // or "indented", "consistent"
}
}
Line Length Rules
MD013 - line-length
{
"MD013": {
"line_length": 120,
"heading_line_length": 120,
"code_blocks": false,
"tables": false,
"headings": true,
"strict": false,
"stern": false
}
}
Whitespace Rules
MD009 - no-trailing-spaces
{
"MD009": {
"br_spaces": 2,
"list_item_empty_lines": false,
"strict": false
}
}
MD010 - no-hard-tabs
- Hard tabs
MD012 - no-multiple-blanks
- Multiple consecutive blank lines
MD027 - no-multiple-space-blockquote
- Multiple spaces after blockquote symbol
MD028 - no-blanks-blockquote
- Blank line inside blockquote
HTML Rules
MD033 - no-inline-html
{
"MD033": {
"allowed_elements": ["br", "img", "a"]
}
}
Link and Image Rules
MD034 - no-bare-urls
- Bare URL used
MD042 - no-empty-links
- No empty links
MD045 - no-alt-text
- Images should have alternate text (alt text)
MD052 - reference-links-images
- Reference links and images should use a label that is defined
Emphasis Rules
MD036 - no-emphasis-as-heading
- Emphasis used instead of a heading
MD037 - no-space-in-emphasis
- Spaces inside emphasis markers
MD049 - emphasis-style
{
"MD049": {
"style": "underscore" // or "asterisk", "consistent"
}
}
MD050 - strong-style
{
"MD050": {
"style": "asterisk" // or "underscore", "consistent"
}
}
Other Rules
MD014 - commands-show-output
- Dollar signs used before commands without showing output
MD019 - no-multiple-space-atx
- Multiple spaces after hash on atx style heading
MD023 - heading-start-left
- Headings must start at the beginning of the line
MD026 - no-trailing-punctuation
{
"MD026": {
"punctuation": ".,;:!。,;:!"
}
}
MD035 - hr-style
{
"MD035": {
"style": "---" // or "***", "___", "consistent"
}
}
MD044 - proper-names
{
"MD044": {
"names": ["JavaScript", "TypeScript", "GitHub"],
"code_blocks": false,
"html_elements": false
}
}
MD047 - single-trailing-newline
- Files should end with a single newline character
MD048 - code-fence-style
{
"MD048": {
"style": "backtick" // or "tilde", "consistent"
}
}
Common Rule Configurations
Strict Documentation
{
"default": true,
"MD013": { "line_length": 80, "code_blocks": false },
"MD033": false,
"MD041": true
}
Relaxed Blog Style
{
"default": true,
"MD013": false,
"MD033": { "allowed_elements": ["br", "img", "iframe"] },
"MD041": false
}
Technical Documentation
{
"default": true,
"MD013": { "line_length": 120, "code_blocks": false, "tables": false },
"MD033": { "allowed_elements": ["br", "details", "summary"] },
"MD040": true
}
Code Blocks
Fenced Code Blocks
Use triple backticks with language identifier:
```javascript
function greet(name) {
return `Hello, ${name}!`;
}
```
Common Language Identifiers
javascriptorjstypescriptortspythonorpybashorshjsonyamlorymlmarkdownormdhtmlcsssqldockerfilegorustjavac,cpp,csharp
Indented Code Blocks
Indent by 4 spaces or 1 tab:
function greet(name) {
return `Hello, ${name}!`;
}
Note: Fenced code blocks are preferred over indented blocks.
Syntax Highlighting
Language identifiers enable syntax highlighting:
```typescript
interface User {
id: string;
name: string;
email: string;
}
const getUser = async (id: string): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
```
Code Block Metadata
Some renderers support additional metadata:
```javascript {1,3-5}
// Line 1 highlighted
const x = 10;
// Lines 3-5 highlighted
const y = 20;
const z = 30;
```
```javascript filename="app.js"
console.log('Hello, World!');
```
Inline Code
Use single backticks for inline code:
Use the `Array.map()` method to transform arrays.
Best Practices
- Always specify language identifiers for proper syntax highlighting
- Use fenced code blocks over indented blocks
- Keep code examples concise and focused
- Test code examples to ensure they work
- Add comments to explain complex code
- Use inline code for function names, variables, and short snippets
- Add blank lines before and after code blocks
Examples
Good
Install dependencies:
```bash
npm install express
```
Create a simple server:
```javascript
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
```
Bad
Install dependencies:
```
npm install express
```
Create a simple server:
```
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
```
Shell Examples
Use $ prefix for commands, no prefix for output:
```bash
$ npm install
added 142 packages in 5s
$ npm test
PASS test/example.test.js
✓ should pass (2 ms)
```
Configuration Files
Use appropriate language identifiers:
JSON configuration:
```json
{
"name": "my-project",
"version": "1.0.0"
}
```
YAML configuration:
```yaml
name: my-project
version: 1.0.0
```
Linting Rules
- MD031 (blanks-around-fences): Fenced code blocks should be surrounded by blank lines
- MD038 (no-space-in-code): Spaces inside code span elements
- MD040 (fenced-code-language): Fenced code blocks should have a language specified
- MD046 (code-block-style): Code block style (consistent, fenced, indented)
- MD048 (code-fence-style): Code fence style (consistent, backtick, tilde)
Configuration Example
{
"MD031": { "list_items": false },
"MD040": {
"allowed_languages": [],
"language_only": false
},
"MD046": { "style": "fenced" },
"MD048": { "style": "backtick" }
}
Text Formatting
Basic Formatting
Italic
Use single asterisks or underscores:
*italic text*
_italic text_
Bold
Use double asterisks or underscores:
**bold text**
__bold text__
Bold and Italic
Combine them:
***bold and italic***
___bold and italic___
**_bold and italic_**
Strikethrough
Use double tildes:
~~strikethrough text~~
Inline Code
Use single backticks:
Use the `console.log()` function.
For code with backticks inside:
Use ``code with `backticks` inside``.
Best Practices
- Be consistent with italic/bold markers throughout a document
- Use
*for emphasis (italic) and**for strong emphasis (bold) - Use underscores
_only when marking up text within words - Always use inline code for:
- Function names, variable names, method names
- File paths and filenames
- Command-line commands
- Package names
- Configuration keys
Examples
Good
The `getUserById()` function accepts an `id` parameter and returns a **User** object.
Install with `npm install express`.
Edit the `package.json` file in your project root.
Bad
The getUserById() function accepts an id parameter and returns a User object.
Install with npm install express.
Edit the package.json file in your project root.
Linting Rules
- MD036 (no-emphasis-as-heading): Emphasis used instead of a heading
- MD037 (no-space-in-emphasis): Spaces inside emphasis markers
- MD038 (no-space-in-code): Spaces inside code span elements
- MD042 (no-empty-links): No empty links
Configuration Example
{
"MD036": { "punctuation": ".,;:!?。,;:!?" },
"MD037": true,
"MD038": true
}
Markdown Headings
Syntax
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
Rules
- Use only one H1 (
#) per document (typically the title) - Don’t skip heading levels (don’t go from H2 to H4)
- Add blank lines before and after headings
- Use ATX-style headings (
#) not Setext-style (===or---)
Best Practices
- Keep headings concise and descriptive
- Use sentence case (capitalize first word only)
- Don’t end headings with punctuation
- Make headings scannable for quick navigation
Examples
Good
# Project Title
## Installation
### Prerequisites
### Quick Start
## Usage
Bad
# Project Title
##### Prerequisites
## Installation
Linting Rules
- MD001 (heading-increment): Heading levels should only increment by one level at a time
- MD003 (heading-style): Heading style should be consistent (
atx,atx_closed, orsetext) - MD018 (no-missing-space-atx): No space after hash on atx style heading
- MD019 (no-multiple-space-atx): Multiple spaces after hash on atx style heading
- MD020 (no-missing-space-closed-atx): No space inside hashes on closed atx style heading
- MD021 (no-multiple-space-closed-atx): Multiple spaces inside hashes on closed atx style heading
- MD022 (blanks-around-headings): Headings should be surrounded by blank lines
- MD023 (heading-start-left): Headings must start at the beginning of the line
- MD024 (no-duplicate-heading): Multiple headings with the same content
- MD025 (single-h1): Multiple top-level headings in the same document
- MD041 (first-line-heading/first-line-h1): First line in a file should be a top-level heading
- MD043 (required-headings): Required heading structure
Configuration Example
{
"MD003": { "style": "atx" },
"MD022": { "lines_above": 1, "lines_below": 1 },
"MD024": { "siblings_only": true },
"MD025": { "front_matter_title": "" },
"MD041": { "level": 1 }
}
Links and Images
Links
Inline Links
[Link text](https://example.com)
[Link with title](https://example.com "Optional title")
Reference Links
[Link text][reference]
[Another link][ref2]
[reference]: https://example.com
[ref2]: https://example.com/page "Optional title"
Automatic Links
<https://example.com>
<user@example.com>
Internal Links (Anchors)
[Go to Installation](#installation)
[Go to API Reference](#api-reference)
Heading anchors are auto-generated:
- Convert to lowercase
- Replace spaces with hyphens
- Remove special characters
## My Section Title
<!-- Anchor: #my-section-title -->
## API Reference
<!-- Anchor: #api-reference -->
Images
Inline Images


Reference Images
![Alt text][logo]
[logo]: logo.png "Company logo"
Linked Images
[](https://example.com)
Image Size (HTML fallback)
<img src="image.png" alt="Alt text" width="200" height="100">
Best Practices
Links
- Use descriptive link text (avoid “click here” or “here”)
- Use reference links for repeated URLs
- Use automatic links for bare URLs
- Verify all links work (use link checkers)
- Use relative paths for internal documentation links
- Add titles for additional context
Images
- Always provide meaningful alt text
- Use descriptive filenames
- Optimize image sizes for web
- Use relative paths for local images
- Consider accessibility (alt text describes image content)
- Use linked images for clickable graphics
Examples
Good Links
Check the [installation guide](docs/installation.md) for setup instructions.
For more details, see the [official documentation](https://docs.example.com).
Contact us at <support@example.com>.
Bad Links
Click [here](docs/installation.md) for installation.
For more details, click [here](https://docs.example.com).
Contact us at support@example.com.
Good Images


[](https://ci.example.com)
Bad Images


Linting Rules
- MD011 (no-reversed-links): Reversed link syntax
- MD034 (no-bare-urls): Bare URL used
- MD039 (no-space-in-links): Spaces inside link text
- MD042 (no-empty-links): No empty links
- MD045 (no-alt-text): Images should have alternate text (alt text)
- MD052 (reference-links-images): Reference links and images should use a label that is defined
Configuration Example
{
"MD034": true,
"MD042": true,
"MD045": true
}
Lists
Unordered Lists
Use -, *, or + for unordered list items:
- First item
- Second item
- Third item
Nested Lists
Indent by 2 or 4 spaces:
- First level
- Second level
- Third level
- Back to second level
- Back to first level
Ordered Lists
Use numbers followed by a period:
1. First item
2. Second item
3. Third item
Nested Ordered Lists
1. First level
1. Second level
2. Another second level
2. Back to first level
Task Lists
Use - [ ] for unchecked and - [x] for checked:
- [x] Completed task
- [ ] Incomplete task
- [ ] Another incomplete task
Multi-Paragraph List Items
Add blank lines and indent:
- First item
This is a continuation of the first item.
Another paragraph in the first item.
- Second item
Code Blocks in Lists
Indent the code block:
- Install dependencies:
```bash
npm install
-
Run the server:
npm start
## Best Practices
- Be consistent with list markers (`-` recommended for unordered lists)
- Use 2-space or 4-space indentation consistently
- Add blank lines between list items containing multiple paragraphs
- Align nested lists properly
- Use meaningful numbering (don't use `1.` for every item)
## Examples
### Good
```markdown
- Prerequisites:
- Node.js 18+
- PostgreSQL 14+
- Redis 7+
- Installation steps:
1. Clone the repository
2. Install dependencies
3. Configure environment variables
Bad
* Prerequisites:
- Node.js 18+
+ PostgreSQL 14+
* Redis 7+
- Installation steps:
1. Clone the repository
2. Install dependencies
3. Configure environment variables
Linting Rules
- MD004 (ul-style): Unordered list style should be consistent
- MD005 (list-indent): Inconsistent indentation for list items at the same level
- MD006 (ul-start-left): Consider starting bulleted lists at the beginning of the line
- MD007 (ul-indent): Unordered list indentation (default: 2 spaces)
- MD029 (ol-prefix): Ordered list item prefix (one, ordered, one_or_ordered, zero)
- MD030 (list-marker-space): Spaces after list markers (ul_single, ol_single, ul_multi, ol_multi)
- MD032 (blanks-around-lists): Lists should be surrounded by blank lines
Configuration Example
{
"MD004": { "style": "dash" },
"MD007": { "indent": 2, "start_indented": false },
"MD029": { "style": "ordered" },
"MD030": {
"ul_single": 1,
"ol_single": 1,
"ul_multi": 1,
"ol_multi": 1
},
"MD032": { "lines_above": 1, "lines_below": 1 }
}
Blockquotes and Other Elements
Blockquotes
Use > for blockquotes:
> This is a blockquote.
Multi-Line Blockquotes
> This is a blockquote
> that spans multiple lines.
Or use a single >:
> This is a blockquote
that spans multiple lines.
Nested Blockquotes
> First level
>> Second level
>>> Third level
Blockquotes with Other Elements
> ## Heading in Blockquote
>
> This blockquote contains:
>
> - A list item
> - Another item
>
> And a code block:
>
> ```javascript
> console.log('Hello');
> ```
Horizontal Rules
Create with three or more -, *, or _:
---
***
___
Line Breaks
Hard Break
End line with two spaces:
Line one with two spaces at end
Line two
Paragraph Break
Use blank line:
First paragraph
Second paragraph
Escaping Characters
Use backslash to escape special characters:
\* Not italic \*
\# Not a heading
\[Not a link\](url)
Characters that can be escaped:
\ ` * _ { } [ ] ( ) # + - . ! |
Tables
Basic table syntax:
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1 | Cell 2 | Cell 3 |
| Cell 4 | Cell 5 | Cell 6 |
Column Alignment
| Left aligned | Center aligned | Right aligned |
|:-------------|:--------------:|--------------:|
| Left | Center | Right |
Complex Tables
| Feature | Description | Status |
|--------------|--------------------------------|--------|
| Authentication | OAuth 2.0 with JWT | ✅ |
| Authorization | Role-based access control | ✅ |
| Logging | Structured JSON logs | 🚧 |
HTML in Markdown
When to Use HTML
- Complex layouts not supported by Markdown
- Specific styling requirements
- Image sizing
- Embedded content
Common HTML Elements
<div align="center">
<img src="logo.png" alt="Logo" width="200">
</div>
<details>
<summary>Click to expand</summary>
Hidden content here.
</details>
<kbd>Ctrl</kbd> + <kbd>C</kbd>
<sup>Superscript</sup> and <sub>subscript</sub>
Best Practices
Blockquotes
- Use for quotes, callouts, or highlighting important information
- Keep nested quotes to 2-3 levels maximum
- Add attribution for actual quotes
Horizontal Rules
- Use sparingly to separate major sections
- Be consistent with style (prefer
---) - Surround with blank lines
Line Breaks
- Use paragraph breaks (blank lines) for most cases
- Hard breaks for poetry, addresses, or specific formatting needs
- Avoid trailing spaces (can be invisible and cause issues)
Tables
- Keep tables simple (use HTML or images for complex data)
- Align columns for readability in source
- Use column alignment for numeric data
- Consider accessibility (screen readers may struggle with complex tables)
HTML
- Prefer Markdown over HTML when possible
- Validate HTML syntax
- Consider that some Markdown renderers may strip HTML
- Document when HTML is necessary
Examples
Good Blockquote
> **Note:** This feature requires Node.js 18 or higher.
> **Warning:** Deleting this resource cannot be undone.
Good Table
| Command | Description |
|---------|--------------------------|
| `start` | Starts the server |
| `stop` | Stops the server |
| `test` | Runs the test suite |
Good HTML Usage
<details>
<summary>View detailed error log</summary>
```bash
Error: Connection timeout
at connect (db.js:45)
at async main (index.js:12)
```
Linting Rules
- MD009 (no-trailing-spaces): Trailing spaces
- MD010 (no-hard-tabs): Hard tabs
- MD026 (no-trailing-punctuation): Trailing punctuation in heading
- MD027 (no-multiple-space-blockquote): Multiple spaces after blockquote symbol
- MD028 (no-blanks-blockquote): Blank line inside blockquote
- MD033 (no-inline-html): Inline HTML
- MD035 (hr-style): Horizontal rule style
- MD047 (single-trailing-newline): Files should end with a single newline character
Configuration Example
{
"MD009": {
"br_spaces": 2,
"list_item_empty_lines": false,
"strict": false
},
"MD010": {
"code_blocks": true,
"spaces_per_tab": 4
},
"MD026": {
"punctuation": ".,;:!?。,;:!?"
},
"MD033": {
"allowed_elements": ["details", "summary", "kbd", "sup", "sub", "br"]
},
"MD035": {
"style": "consistent"
}
}