Browse docs
PortalMSP Portal

Markdown features

MSP keeps Markdown readable while adding small, progressively enhanced helpers for documentation authors.

Images and accordions

Portal sync flow A simple diagram showing a source repository syncing documentation into MSP Portal. Source repository docs/ + metadata MSP sync step copy + normalize MSP Portal search + navigation

Click an image to open it at full size. Native details elements provide accessible accordions without a dependency:

Show the authoring notes

The image zoom, copy buttons, and previews work without changing the source repository's Markdown ownership.

Code blocks

Fenced code blocks show their language and include copy and preview actions. Add preview after html for an instant sandboxed preview:

HTML
<div class="demo-card">
  <strong>Live preview</strong>
  <p>This HTML is rendered in an isolated frame.</p>
</div>
<style>
  .demo-card { padding: 1rem; border: 2px solid #f2b632; border-radius: 8px; font-family: sans-serif; }
</style>

A saved JSFiddle can be embedded with a jsfiddle fenced block containing its URL:

External links automatically open in a new tab:

See the Astro documentation for more about the static site generator.

YouTube links turn into embedded video players. Just write a normal markdown link:

File embeds

Embed a source file's content directly in the page using embed: followed by the language:

JavaScript Open raw
import fs from &#39;node:fs/promises';
import path from &#39;node:path';
import { execFile } from &#39;node:child_process';
import { promisify } from &#39;node:util';
import YAML from &#39;yaml';

const execFileAsync = promisify(execFile);
const root = process.cwd();
const tmpDir = path.join(root, &#39;.tmp');
const clonesDir = path.join(tmpDir, &#39;clones');
const aggregatedDir = path.join(tmpDir, &#39;aggregated');
const publicSourcesDir = path.join(root, &#39;public', '_sources');
const generatedDir = path.join(root, &#39;src', 'generated');

await fs.mkdir(clonesDir, { recursive: true });
await fs.mkdir(aggregatedDir, { recursive: true });
await fs.mkdir(publicSourcesDir, { recursive: true });
await fs.mkdir(generatedDir, { recursive: true });

await cleanDir(aggregatedDir);
await cleanDir(publicSourcesDir);

const registry = YAML.parse(await fs.readFile(path.join(root, &#39;sources.yml'), 'utf8')) ?? {};
const sources = Array.isArray(registry.sources) ? registry.sources : [];
const synced = [];

for (const source of sources) {
  const repoDir = source.localPath
    ? path.resolve(root, source.localPath)
    : await cloneRepo(source, clonesDir);

  const metadataPath = path.join(repoDir, &#39;.docs-source.yml');
  const metadata = YAML.parse(await fs.readFile(metadataPath, &#39;utf8'));
  const docsPath = metadata.docs_path || &#39;docs';
  const docsDir = path.join(repoDir, docsPath);
  const targetDir = path.join(aggregatedDir, metadata.id);
  const publicDir = path.join(publicSourcesDir, metadata.id);

  await copyDir(docsDir, targetDir);
  await copyDir(docsDir, publicDir);

  synced.push({
    id: metadata.id,
    name: metadata.name || metadata.id,
    description: metadata.description || &#39;',
    category: metadata.category || &#39;General',
    tags: metadata.tags || [],
    navigation: metadata.navigation || [],
    repo: source.repo,
    repoUrl: source.repoUrl || `https:16
    defaultBranch: source.defaultBranch || &17
    docsPath,
    sourceDir: path.relative(root, targetDir).replace(/\\/g, &18
    publicDir: path.relative(root, publicDir).replace(/\\/g, &19
    local: Boolean(source.localPath)
  });
}

await fs.writeFile(path.join(generatedDir, &20
console.log(`Synced ${synced.length} source(s)`);

async function cloneRepo(source, parentDir) {
  const name = source.repo.replace(/[\/]/g, &21
  const dir = path.join(parentDir, name);
  await fs.rm(dir, { recursive: true, force: true });

  const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || &22
  const url = token
    ? `https://x-access-token:${token}@github.com/${source.repo}.git`
    : `https://github.com/${source.repo}.git`;

  await execFileAsync(&#39;git', ['clone', '--depth', '1', '--branch', source.defaultBranch || 'main', url, dir], {
    cwd: parentDir
  });

  return dir;
}

async function cleanDir(dir) {
  await fs.rm(dir, { recursive: true, force: true });
  await fs.mkdir(dir, { recursive: true });
}

async function copyDir(from, to) {
  await fs.mkdir(to, { recursive: true });
  const entries = await fs.readdir(from, { withFileTypes: true });
  for (const entry of entries) {
    const src = path.join(from, entry.name);
    const dest = path.join(to, entry.name);
    if (entry.isDirectory()) {
      await copyDir(src, dest);
    } else {
      await fs.copyFile(src, dest);
    }
  }
}
YAML Open raw
sources:
  - repo: Leonard-Data/firstmate-docs
    defaultBranch: main

SVG images in the project are rendered inline so you can style them with CSS:

Portal sync flow A simple diagram showing a source repository syncing documentation into MSP Portal. Source repository docs/ + metadata MSP sync step copy + normalize MSP Portal search + navigation

MSP keeps documentation with its source repository and publishes one searchable library.
Global search

Search the library

Results pull from titles, descriptions, headings, page content, source names, and category labels.