Skip to main content

Working with Markdown

Scully uses markdown files to generate static content pages. This guide covers everything you need to know about working with markdown in Scully.

Markdown Basics

File Structure

Every markdown file has two parts:
  1. Frontmatter (YAML metadata at the top)
  2. Content (Markdown body)
---
title: My Blog Post
description: A short description
published: true
---

# My Blog Post

This is the content of the blog post.

Frontmatter

Frontmatter is YAML metadata between --- delimiters. Scully uses this data for routing and metadata.

Required Properties

---
title: My Post Title        # Post title
description: Brief summary  # Post description
published: true            # Whether post is live
---

Optional Properties

Add any custom properties you need:
---
title: Advanced TypeScript Tips
description: Master TypeScript advanced features
published: true

# Custom metadata
author: Jane Doe
date: 2024-03-15
updated: 2024-03-20
tags: [typescript, javascript, programming]
category: tutorials
featuredImage: /assets/images/typescript.jpg
readingTime: 8 min
seoKeywords: [typescript, types, generics]
canonicalUrl: https://example.com/typescript-tips
lang: en
---

Accessing Frontmatter Data

All frontmatter properties are available in your Angular components:
import { Component } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-blog-post',
  template: `
    <article *ngIf="post$ | async as post">
      <h1>{{ post.title }}</h1>
      <div class="meta">
        <span *ngIf="post.author">By {{ post.author }}</span>
        <time *ngIf="post.date">{{ post.date | date }}</time>
        <span *ngIf="post.readingTime">{{ post.readingTime }}</span>
      </div>
      <img *ngIf="post.featuredImage" [src]="post.featuredImage" [alt]="post.title">
      <scully-content></scully-content>
      <div class="tags">
        <span *ngFor="let tag of post.tags">{{ tag }}</span>
      </div>
    </article>
  `
})
export class BlogPostComponent {
  post$: Observable<ScullyRoute>;

  constructor(private scully: ScullyRoutesService) {
    this.post$ = this.scully.getCurrent();
  }
}

ScullyRoute Interface

The ScullyRoute interface includes all frontmatter plus Scully properties:
export interface ScullyRoute {
  route: string;           // Generated route path
  title?: string;          // From frontmatter
  slugs?: string[];        // Unpublished slugs array
  published?: boolean;     // Publish status
  slug?: string;          // Custom slug override
  sourceFile?: string;    // Original markdown file path
  lang?: string;          // Language code
  [prop: string]: any;    // Any custom properties
}

Markdown Syntax

Headers

# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6

Text Formatting

**Bold text**
*Italic text*
***Bold and italic***
~~Strikethrough~~
`Inline code`
[Link text](https://example.com)
[Link with title](https://example.com "Title")
[Reference link][ref]

[ref]: https://example.com

Images

![Alt text](/path/to/image.jpg)
![Alt text](/path/to/image.jpg "Image title")

Lists

# Unordered list
- Item 1
- Item 2
  - Nested item
  - Another nested item
- Item 3

# Ordered list
1. First item
2. Second item
3. Third item

Code Blocks

With syntax highlighting:
```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<h1>Hello</h1>'
})
export class AppComponent {}
```

Blockquotes

> This is a blockquote
> It can span multiple lines
>
> And have multiple paragraphs

Tables

| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   | Cell 3   |
| Cell 4   | Cell 5   | Cell 6   |

Horizontal Rules

---
***
___

Syntax Highlighting

Configuration

1

Enable in Scully Config

scully.config.ts
import { setPluginConfig } from '@scullyio/scully';

setPluginConfig('md', { enableSyntaxHighlighting: true });
2

Import Language Components

Import Prism.js languages you need:
scully.config.ts
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-yaml';
import 'prismjs/components/prism-docker';
3

Import Theme

Add a Prism theme to your global styles:
src/styles.css
/* Choose a theme from node_modules/prismjs/themes/ */
@import '~prismjs/themes/prism-tomorrow.css';
/* or */
@import '~prismjs/themes/prism-okaidia.css';
/* or */
@import '~prismjs/themes/prism-twilight.css';

Supported Languages

Prism.js supports many languages. Common ones include:
  • typescript, javascript, jsx, tsx
  • html, css, scss, less
  • bash, shell, powershell
  • json, yaml, xml
  • markdown, sql, graphql
  • python, java, c, cpp, csharp
  • docker, nginx, git

Usage in Markdown

Specify the language after the opening backticks:
```typescript
const greeting: string = 'Hello, Scully!';
console.log(greeting);
```

```bash
npm install @scullyio/scully
ng build
npx scully
```

```json
{
  "name": "my-app",
  "version": "1.0.0"
}
```

Custom Language Support

If a language isn’t available, Scully will warn you:
Language 'rust' is not available in the default Prism.js setup.
If you want support for this, add it to your scully.config.ts:

  import 'prismjs/components/prism-rust'

Markdown Plugin Configuration

The markdown plugin uses marked.js with these settings:
import { setPluginConfig } from '@scullyio/scully';

setPluginConfig('md', {
  enableSyntaxHighlighting: true
});

// Marked.js is configured internally with:
// pedantic: false
// gfm: true (GitHub Flavored Markdown)
// breaks: false
// sanitize: false
// smartLists: true
// smartypants: false
// xhtml: false

File Organization

project-root/
├── blog/
│   ├── 2024-01-15-first-post.md
│   ├── 2024-02-20-second-post.md
│   └── 2024-03-10-third-post.md
├── docs/
│   ├── getting-started/
│   │   ├── installation.md
│   │   └── configuration.md
│   ├── guides/
│   │   ├── routing.md
│   │   └── plugins.md
│   └── api/
│       ├── core.md
│       └── utils.md
└── scully.config.ts

Date-Prefixed Files

Prefix files with dates for chronological ordering:
blog/
├── 2024-01-15-getting-started.md
├── 2024-02-01-advanced-features.md
└── 2024-03-01-best-practices.md

Custom Slugs

Override the default filename-based slug:
---
title: How to Use Angular with Scully
slug: angular-scully-guide
---
Route becomes /blog/angular-scully-guide instead of /blog/how-to-use-angular-with-scully.

Draft Posts

Unpublished Posts

Set published: false to create a draft:
---
title: Work in Progress
published: false
---
Scully generates an anonymous slug:
---
title: Work in Progress
published: false
slugs:
  - ___UNPUBLISHED___kao8mvda_pmldPr7aN7owPpStZiuDXFZ1ILfpcv5Z
---
Access at:
http://localhost:1668/blog/___UNPUBLISHED___kao8mvda_pmldPr7aN7owPpStZiuDXFZ1ILfpcv5Z

Publishing

To publish:
  1. Set published: true
  2. Remove slugs array
  3. Rebuild: npx scully

Filtering Unpublished Posts

Filter out drafts in your listing:
this.posts$ = this.scully.available$.pipe(
  map(routes => 
    routes
      .filter(route => route.route.startsWith('/blog/'))
      .filter(route => route.published !== false)  // Exclude drafts
  )
);

Advanced Techniques

Table of Contents

Generate TOC from headers:
import { registerPlugin } from '@scullyio/scully';

registerPlugin('postProcessByDom', 'toc', async (dom) => {
  const { window: { document } } = dom;
  
  const headings = document.querySelectorAll('h2, h3');
  const toc = document.createElement('nav');
  toc.className = 'toc';
  
  const ul = document.createElement('ul');
  headings.forEach(heading => {
    const li = document.createElement('li');
    const a = document.createElement('a');
    a.href = `#${heading.id}`;
    a.textContent = heading.textContent;
    li.appendChild(a);
    ul.appendChild(li);
  });
  
  toc.appendChild(ul);
  const content = document.querySelector('scully-content');
  content?.parentNode?.insertBefore(toc, content);
  
  return dom;
});

Reading Time

Calculate estimated reading time:
import { registerPlugin } from '@scullyio/scully';

registerPlugin('routeProcess', 'reading-time', async (routes) => {
  return routes.map(route => {
    if (route.sourceFile) {
      const fs = require('fs');
      const content = fs.readFileSync(route.sourceFile, 'utf-8');
      const words = content.split(/\s+/).length;
      const readingTime = Math.ceil(words / 200); // 200 words per minute
      return { ...route, readingTime: `${readingTime} min read` };
    }
    return route;
  });
});

Custom Frontmatter Validation

Validate required frontmatter fields:
import { registerPlugin, logWarn } from '@scullyio/scully';

registerPlugin('routeProcess', 'validate-frontmatter', async (routes) => {
  const required = ['title', 'description', 'author', 'date'];
  
  routes.forEach(route => {
    if (route.route.startsWith('/blog/')) {
      required.forEach(field => {
        if (!route[field]) {
          logWarn(`Missing required field "${field}" in ${route.sourceFile}`);
        }
      });
    }
  });
  
  return routes;
});

Troubleshooting

Problem: Frontmatter data not available in componentSolutions:
  • Ensure frontmatter is valid YAML
  • Check for proper --- delimiters
  • Rebuild with npx scully
  • Verify no extra spaces or tabs
  • Check for special characters (escape if needed)
Problem: Code blocks not highlightedSolutions:
  • Enable: setPluginConfig('md', { enableSyntaxHighlighting: true })
  • Import language: import 'prismjs/components/prism-<language>'
  • Import theme in styles.css
  • Use proper code fence syntax with language
  • Clear browser cache
Problem: Raw markdown visible on pageSolutions:
  • Ensure <scully-content> in template
  • Import ScullyLibModule in component module
  • Run npx scully to generate pages
  • Check route is configured in scully.config.ts
  • Verify markdown files are in correct folder
Problem: Images broken in markdownSolutions:
  • Use absolute paths from assets: /assets/images/pic.jpg
  • Or relative to markdown file: ./images/pic.jpg
  • Ensure images are in Angular assets folder
  • Check angular.json includes images in assets
  • Verify image paths are correct after build

Best Practices

  • Use meaningful frontmatter with consistent structure
  • Add descriptions and metadata for SEO
  • Keep markdown files organized in logical folders
  • Use date prefixes for chronological content
  • Include published flag for draft management
  • Add custom metadata (author, date, tags) consistently
  • Enable syntax highlighting for code examples
  • Test with unpublished slugs before publishing
  • Use semantic heading hierarchy (h1 → h2 → h3)
  • Optimize images before adding to content

Next Steps

Create a Blog

Set up a complete blog with markdown

Configuration

Configure Scully for your needs

Plugins

Extend Scully with plugins

Deployment

Deploy your static site

Build docs developers (and LLMs) love