Publishing a Static Site
AS Notes includes a built-in HTML conversion tool that turns your markdown notes into a static website. It supports filtering, layouts, themes, asset pipelines, retina images, SEO metadata, sitemaps, and RSS feeds. You can deploy to GitHub Pages, Netlify, Vercel, Cloudflare Pages, or any static hosting.
This documentation is itself a working example -- it was written in AS Notes and published using the same tool.
How It Works
The publish package recursively scans a folder of markdown files and converts them to HTML. Subdirectories are walked automatically, so notes organised in folders like notes/, journals/, and pages/ are all discovered. The templates and node_modules directories are excluded by default.
Wikilinks between pages are automatically resolved to the correct HTML links. A navigation sidebar is generated from all public pages. Only pages you mark as public are published.
Output is flat: all HTML files are written to the output root regardless of source subdirectory depth. Filenames are slugified to clean, URL-friendly kebab-case: notes/My Page.md becomes my-page.html. This matches how wikilinks work in AS Notes where filenames are globally unique.
Each page is wrapped in a layout template with class names you can style any way you like. Unstyled output is clean, readable HTML with zero dependencies.
Prerequisites
- Node.js 20 or later
- Your notes in a folder of
.mdfiles
Installation
There are two ways to run the converter:
npm (recommended for CI/CD)
Install the CLI globally or use npx:
npx asnotes-publish --config ./asnotes-publish.json
Or install globally:
npm install -g asnotes-publish
asnotes-publish --config ./asnotes-publish.json
This is the recommended approach for CI/CD pipelines (GitHub Actions, Netlify, Vercel, Cloudflare Pages).
Build from source
Alternatively, the publish package in the AS Notes repository can be built from source:
cd publish
npm install
npm run build
node dist/convert.js --config ../asnotes-publish.json
VS Code Extension
The converter is also bundled into the AS Notes VS Code extension. Use the command palette (Ctrl+Shift+P) and run AS Notes: Publish to HTML. This prompts for an output directory and runs the conversion with your configured settings. No separate installation required.
Quick Start
npm run convert -- --input /path/to/notes --output /path/to/site --default-public
This converts all markdown files to HTML. The output folder is wiped and regenerated on every run.
Preview locally:
npx serve /path/to/site
Front Matter
Control publishing behaviour per-page using YAML front matter at the top of any markdown file:
---
public: true
title: My Page Title
order: 1
description: A short description for SEO
layout: docs
assets: true
retina: true
draft: false
date: 2025-03-23
---
All fields are optional. Here is what each field does:
| Field | Type | Description |
|---|---|---|
public |
boolean | Page is included in output. Required unless --default-public is used |
title |
string | Page title for <title> tag and nav. Defaults to filename |
order |
number | Nav sort order. Lower numbers appear first. Unordered pages sort alphabetically after ordered ones |
description |
string | Injected as <meta name="description"> for SEO |
layout |
string | Per-page layout override (docs, blog, minimal, or custom) |
assets |
boolean | Enable asset copying for this page. Required unless --default-assets is used |
retina |
boolean | Apply retina styling to all images on this page |
draft |
boolean | Exclude from output unless --include-drafts is passed |
date |
string | Date for blog-style display and RSS feed ordering |
Slash Commands for Front Matter
In VS Code, type / in any markdown file to access these slash commands:
- /Public -- toggle
public: true/falsein front matter - /Layout -- cycle through
docs,blog,minimallayouts - /Retina -- toggle
retina: true/false - /Assets -- toggle
assets: true/false
These commands create a front matter block if one doesn't exist, or update existing fields.
Public / Private Filtering
By default, only pages with public: true in their front matter are converted. All other pages are skipped.
---
public: true
---
# This page will be published
Pages without front matter or without public: true are skipped silently.
Encrypted Files
Files ending in .enc.md (AS Notes encrypted files) are always excluded from publishing. This is a hardcoded safety measure -- encrypted notes are never included in the output regardless of --default-public or per-file front matter settings.
Excluding Directories
The converter recursively scans all subdirectories. The templates and node_modules directories are excluded by default. To exclude additional directories, use --exclude:
npm run convert -- --input ./notes --output ./site --default-public --exclude drafts --exclude archive
Exclude matches directory names at any depth in the tree.
Default Public Mode
Pass --default-public to invert the behaviour: all pages are published unless they have public: false:
npm run convert -- --input ./notes --output ./site --default-public
Dead Links
When a public page links to a non-public page via wikilink, the link is rendered as a dead link (the href is still present but points nowhere). The converter logs a warning for each dead link so you can fix them.
Drafts
Pages with draft: true are excluded from the output. To include them (e.g. for a preview build), pass --include-drafts:
npm run convert -- --input ./notes --output ./site --default-public --include-drafts
Home Page (Index)
Every site needs a page at /. If your notes include a file called index.md, it becomes the home page automatically. The index page appears first in navigation and displays as "Home".
If no index.md exists among your public pages, the converter auto-generates a simple index page that lists links to all published pages. The console will show:
[info] No index.md found - generating page index
To create a custom home page, add index.md to your notes root with public: true:
---
public: true
title: Welcome
---
# Welcome to my site
This is my home page.
Asset Pipeline
Images and files referenced in your markdown are automatically discovered and copied to the output directory.
Enabling Asset Copying
Asset copying is opt-in for safety. You have two options:
Per-page: Add assets: true to the page's front matter:
---
public: true
assets: true
---

Global: Pass --default-assets to enable asset copying for all public pages:
npm run convert -- --input ./notes --output ./site --default-public --default-assets
With --default-assets, pages can opt out with assets: false.
How It Works
The converter scans rendered HTML for local <img src="..."> references. Each reference is resolved relative to the page's source location within the input directory. Since all HTML pages are output to a flat directory, the converter rewrites src attributes so that asset paths resolve correctly in the published output.
For example, if pages/My Page.md references ../assets/images/photo.png, the converter:
- Resolves the path relative to the page's source directory (
pages/) - Copies the file to the output directory as
assets/images/photo.png - Rewrites the
<img src>in the HTML toassets/images/photo.png
Absolute URLs (http://, https://) and data URIs are ignored.
If a referenced file is missing, a warning is logged.
Manual Assets
You can also copy specific files with --asset:
npm run convert -- --input ./notes --output ./site --asset /path/to/favicon.ico
Stylesheets
CDN Stylesheets
Pass --stylesheet with a URL:
npm run convert -- \
--input ./notes --output ./site \
--stylesheet https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown-light.css
Local Stylesheets
Local file paths passed to --stylesheet are automatically copied to the output directory. No separate --asset flag is needed:
npm run convert -- \
--input ./notes --output ./site \
--stylesheet /path/to/my-styles.css
The file is copied to the output and referenced by filename in each page's <head>.
Combining Stylesheets
Pass --stylesheet multiple times. Tags are injected in the order specified:
npm run convert -- \
--input ./notes --output ./site \
--stylesheet https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown-light.css \
--stylesheet /path/to/layout.css
Layouts
Layouts control the HTML structure of each page. Three built-in layouts are available, and you can define your own.
Built-in Layouts
Select a layout with --layout:
| Layout | Description |
|---|---|
docs |
Navigation sidebar + content area with markdown-body class (default) |
blog |
Navigation + blog-style article with date display |
minimal |
Content only, no navigation |
npm run convert -- --input ./notes --output ./site --layout blog
Per-page Layout Override
Set layout: in front matter to override the global layout for a specific page:
---
public: true
layout: minimal
---
Custom Layouts
The setup wizard (Step 8) offers to create a layouts directory containing editable copies of all three built-in layouts. You can modify these files to customise the HTML structure of your site.
Point the converter at your layouts directory with --layouts:
npm run convert -- --input ./notes --output ./site --layouts ./layouts --layout my-layout
The converter looks for my-layout.html in the layouts directory first, then falls back to the includes directory, then to built-in layouts.
You can also create entirely new layout files in the layouts directory and reference them by name (without the .html extension) in --layout or per-page layout: front matter.
Template tokens:
| Token | Replaced with |
|---|---|
{{title}} |
Page title (escaped) |
{{header}} |
Header partial HTML (from header.html in includes dir) |
{{nav}} |
Navigation HTML |
{{content}} |
Rendered markdown body |
|
<link> tags for stylesheets |
|
<meta name="description"> tag |
|
<time> element (if page has date: front matter) |
|
Table of contents HTML |
|
Footer partial HTML (from footer.html in includes dir) |
|
Base URL path prefix |
Example custom layout:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
</head>
<body>
{{header}}
<header>{{nav}}</header>
<main>
{{content}}
</main>
</body>
</html>
Header and Footer Partials
Add a consistent header and footer across all pages by creating header.html and footer.html in your includes directory.
The converter looks for these files when --includes is set (or includes is set in the config file). If a partial exists, its contents are wrapped in a <header> or <footer> element and injected into the page. If a partial does not exist, an HTML comment (<!-- header --> or <!-- footer -->) is emitted as a placeholder.
Partials support and {{title}} tokens so you can create dynamic links.
Example header.html:
<div class="site-header">
<a class="site-title" href="/">My Notes</a>
<span class="header-sep">|</span>
<a href="https://github.com/me">GitHub</a>
<a href="mailto:me@example.com">Contact</a>
</div>
Example footer.html:
<div class="site-footer">
<p>© 2026 My Name. Built with <a href="https://www.asnotes.io">AS Notes</a>.</p>
</div>
Both built-in and custom layouts include {{header}} and
tokens, so partials work with any layout.
When using the VS Code publish wizard, the includes directory step (step 8) offers three options: create default header.html and footer.html files, browse for an existing includes directory, or skip. Edit the generated files to add your own navigation, branding, and contact information.
Custom Navigation
By default, the converter auto-generates a sidebar navigation listing all published pages in order. To take full control of navigation content and structure, create a nav.md file at the root of your input directory.
nav.md is rendered as markdown with full wikilink support, so you can use headings, horizontal rules, lists, and links to organise your navigation however you like:
## Guides
- [[Getting Started]]
- [[Publishing a Static Site]]
---
## Reference
- [[Settings]]
- [[Wikilinks]]
The rendered HTML replaces the auto-generated <nav class="site-nav"> element on every page.
How it works:
- If
nav.mdexists in the input directory root, it is used as the navigation for all pages - If
nav.mddoes not exist, the auto-generated navigation is used (withnav-currenthighlighting) nav.mdis not published as a standalone page and does not appear in the sitemap- Wikilinks in
nav.mdare resolved to their HTML page slugs, with--base-urlapplied if set - The
nav-currentCSS class is not applied when using custom navigation (since the content is user-controlled)
Multi-site support: Since nav.md lives in the input directory, each site configuration can have its own navigation when using multi-site publishing.
Themes
Built-in CSS themes provide ready-made styling without writing CSS:
npm run convert -- --input ./notes --output ./site --theme default
Available themes:
| Theme | Description |
|---|---|
default |
Light theme with GitHub-inspired typography |
dark |
Dark theme with dark background |
Themes are injected as the first stylesheet, so you can override them with additional --stylesheet flags.
Retina Images
Display high-resolution images at half their natural dimensions for crisp rendering on retina displays.
When retina is enabled, the converter:
- Adds a
retinaCSS class to the<img>tag (enablesimage-rendering: crisp-edges) - Reads the image file to determine its intrinsic width
- Sets a
widthattribute to half that value, so a 1600px source image displays at 800px
The {.retina} marker is stripped from the rendered alt text.
Supported formats for automatic width detection: PNG, JPEG, GIF, WebP, and BMP. If the image file is not found or the format is not recognised, the retina class is still applied but no width is set.
Three Levels of Control
Per-image: Append {.retina} to the image alt text in markdown:

Per-page: Add retina: true to the page's front matter. All images on that page are treated as retina:
---
public: true
retina: true
---
Global: Pass --retina to apply retina sizing to all images across the site:
npm run convert -- --input ./notes --output ./site --retina
SEO Features
Page Descriptions
Add description: to front matter for SEO metadata:
---
public: true
description: Learn how to publish AS Notes as a static website
---
This injects <meta name="description" content="..."> in the page <head>.
Slug URLs
Output filenames are automatically slugified to clean, URL-friendly kebab-case. This produces shorter, more readable URLs that are better for SEO and sharing:
| Source file | Output file |
|---|---|
Getting Started.md |
getting-started.html |
My Cool Page.md |
my-cool-page.html |
Page & Notes.md |
page-notes.html |
index.md |
index.html |
All wikilinks, navigation, sitemap, and RSS feed hrefs use the slugified filenames automatically.
Base URL
When deploying to a subdirectory (e.g. https://example.com/docs/), set the base URL prefix:
npm run convert -- --input ./notes --output ./site --base-url /docs
This prefixes all navigation links and asset references with /docs/.
Sitemap
A sitemap.xml is automatically generated in the output directory containing all public pages. If pages have a date: field, it is included as <lastmod>.
RSS / Atom Feed
An RSS feed (feed.xml) is generated for pages with a date: field, sorted newest-first. This enables blog-style subscriptions.
Table of Contents
Every page automatically gets a table of contents generated from its h2, h3, and h4 headings. The TOC is rendered as a collapsible <details> element at the top of the content area.
Headings automatically receive id attributes for deep linking.
HTML Structure Reference
Here is the HTML structure the docs layout produces:
<body>
<header>
<!-- contents of header.html, or an HTML comment if not set -->
</header>
<nav class="site-nav">
<ul>
<li><a href="index.html">Home</a></li>
<li class="nav-current"><a href="my-page.html">My Page</a></li>
</ul>
</nav>
<article class="markdown-body">
<nav class="toc">
<details open>
<summary>Contents</summary>
<ul>
<li><a href="#section">Section</a></li>
</ul>
</details>
</nav>
<!-- rendered markdown -->
</article>
<footer>
<!-- contents of footer.html, or an HTML comment if not set -->
</footer>
</body>
Key classes:
| Class | Element | Notes |
|---|---|---|
site-header |
<div> |
Default header partial wrapper (customise in header.html) |
site-footer |
<div> |
Default footer partial wrapper (customise in footer.html) |
site-nav |
<nav> |
Sidebar navigation (auto-generated or from nav.md) |
nav-current |
<li> |
Currently active page |
markdown-body |
<article> |
Content area (docs layout) -- compatible with github-markdown-css |
blog-post |
<article> |
Content area (blog layout) |
toc |
<nav> |
Table of contents |
page-date |
<time> |
Date display (blog layout) |
retina |
<img> |
Retina image (auto width set to half intrinsic size) |
missing-page |
<p> |
Placeholder page for unresolved wikilinks |
CLI Reference
asnotes-publish --input <dir> --output <dir> [options]
asnotes-publish --config <file> [options]
Options:
--config <file> Load settings from a JSON config file
--stylesheet <url|file> Add a stylesheet (repeatable)
--asset <file> Copy a file to output (repeatable)
--default-public Treat all pages as public unless public: false
--default-assets Copy referenced assets unless assets: false
--layout <name> Layout template: docs, blog, minimal (default: docs)
--layouts <path> Directory containing editable layout templates
--includes <path> Directory for custom headers and footers
--theme <name> Built-in CSS theme: default, dark
--retina Enable retina image sizing globally
--base-url <prefix> URL path prefix for links and assets
--include-drafts Include pages with draft: true
--exclude <dirname> Exclude a directory from scanning (repeatable)
When --config is used, --input defaults to the config file's directory.
CLI flags override config file values.
Default excluded directories: templates, node_modules
Config File
Publish settings are stored in a JSON config file at the root of your notes directory (next to .asnotes/). Both the VS Code extension and the CLI read from this file.
The default filename is asnotes-publish.json. When publishing from a subdirectory, the filename includes the directory name: asnotes-publish.<dirname>.json (e.g. asnotes-publish.docs-src.json). See Multi-Site Publishing below.
Schema
{
"inputDir": "",
"defaultPublic": true,
"defaultAssets": true,
"layout": "docs",
"layouts": "./layouts",
"includes": "./includes",
"theme": "default",
"baseUrl": "/my-repo",
"retina": false,
"includeDrafts": false,
"stylesheets": [
"https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown-light.css"
],
"exclude": ["drafts", "archive"],
"outputDir": "./site"
}
The wizard writes all fields with their defaults so you can discover every available option by reading the file.
| Field | Type | Default | Description |
|---|---|---|---|
inputDir |
string | "" |
Input directory relative to config file. Empty string means notes root |
defaultPublic |
boolean | false |
Publish all pages unless public: false |
defaultAssets |
boolean | false |
Copy referenced assets unless assets: false |
layout |
string | "docs" |
Layout template: docs, blog, minimal |
layouts |
string | "" |
Directory containing editable layout templates |
includes |
string | "" |
Directory for custom headers and footers |
theme |
string | "" |
Built-in CSS theme: default, dark |
baseUrl |
string | "" |
URL path prefix for links and assets |
retina |
boolean | false |
Enable retina image sizing globally |
includeDrafts |
boolean | false |
Include pages with draft: true |
stylesheets |
string[] | [] |
Stylesheet URLs or local file paths |
exclude |
string[] | [] |
Additional directory names to exclude |
outputDir |
string | "" |
Output directory (relative to config file or absolute) |
Using --config
Pass --config to the CLI to load settings from the file:
asnotes-publish --config ./asnotes-publish.json
When --config is used:
--inputdefaults toinputDirfrom the config (resolved relative to the config file), then falls back to the config file's parent directory--outputdefaults to theoutputDirvalue in the config (resolved relative to the config file)- CLI flags override any config file values
This means a minimal CI/CD invocation with a config file is:
npx asnotes-publish --config ./asnotes-publish.json
You can override individual settings:
npx asnotes-publish --config ./asnotes-publish.json --include-drafts --output ./preview
Multi-Site Publishing
You can publish multiple sites from the same workspace by creating separate config files. Each config file targets a different input directory.
The config filename is derived from the input directory name:
| Input directory | Config filename |
|---|---|
| Notes root | asnotes-publish.json |
./docs-src |
asnotes-publish.docs-src.json |
./blog |
asnotes-publish.blog.json |
./pages |
asnotes-publish.pages.json |
For example, a workspace with both documentation and a blog might have:
my-notes/
.asnotes/
asnotes-publish.docs-src.json # publishes docs-src/ to site/
asnotes-publish.blog.json # publishes blog/ to blog-site/
docs-src/
pages/
Getting Started.md
...
blog/
2025-01-01 First Post.md
...
Build each site separately:
npx asnotes-publish --config ./asnotes-publish.docs-src.json
npx asnotes-publish --config ./asnotes-publish.blog.json
The VS Code extension discovers all config files automatically and shows a picker when multiple exist.
Settings to CLI Flag Mapping
If you prefer CLI flags over a config file, here is the mapping:
| Config field | CLI flag |
|---|---|
inputDir |
--input <dir> |
defaultPublic |
--default-public |
defaultAssets |
--default-assets |
layout |
--layout <name> |
layouts |
--layouts <path> |
includes |
--includes <path> |
theme |
--theme <name> |
baseUrl |
--base-url <prefix> |
retina |
--retina |
includeDrafts |
--include-drafts |
stylesheets |
--stylesheet <url> (repeatable) |
exclude |
--exclude <dirname> (repeatable) |
outputDir |
--output <dir> |
VS Code Integration
Publish Command
Use the command palette (Ctrl+Shift+P) and run AS Notes: Publish to HTML.
If no publish config exists, a setup wizard walks you through:
- Input directory -- notes root or a subdirectory
- Default public -- publish all pages by default?
- Default assets -- copy referenced images and files?
- Layout -- docs, blog, or minimal
- Theme -- default, dark, or none
- Base URL -- path prefix for deployed site
- Output directory -- where to write the HTML
- Layouts directory -- create default editable layouts, browse for an existing directory, or skip
- Includes directory -- create default includes, browse for an existing directory, or skip
The wizard saves your choices to the appropriate config file (e.g. asnotes-publish.json or asnotes-publish.docs-src.json). All fields are written with defaults so you can discover every option by reading the JSON file.
On subsequent runs with a single config, the converter uses the saved settings immediately -- no wizard, no prompts. With multiple config files, a picker lets you choose which site to publish or create a new configuration.
Configure Publish Settings
To change your publish settings without building, run AS Notes: Configure Publish Settings from the command palette. This shows a picker of existing configs to edit or creates a new one. If you change the input directory, the config file is renamed automatically to match.
You can also edit config files directly -- they are standard JSON.
## Publishing to GitHub Pages
### 1. Enable GitHub Pages
In your repository, go to **Settings > Pages** and set the source to **GitHub Actions**.
### 2. Add a CI Workflow
Create `.github/workflows/pages.yml`:
```yaml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npx asnotes-publish --config ./asnotes-publish.json --base-url /${{ github.event.repository.name }}
- uses: actions/upload-pages-artifact@v3
with:
path: site
- id: deployment
uses: actions/deploy-pages@v4
Adjust --config path and any flag overrides to match your repository layout. The --base-url override is needed for GitHub Pages subdirectory deployment. All other settings come from asnotes-publish.json.
Alternatively, you can use explicit flags instead of a config file:
- run: >
npx asnotes-publish
--input ./notes
--output ./site
--default-public
--default-assets
--theme default
--base-url /${{ github.event.repository.name }}
Custom Domain
To serve from a custom domain (e.g. docs.example.com):
-
Add a DNS
CNAMErecord pointing to<your-github-username>.github.io -
Add a step after the convert step to write the
CNAMEfile (the converter wipes output on each run):- run: echo "docs.example.com" > ./site/CNAME -
Enter the domain in Settings > Pages > Custom domain and enable Enforce HTTPS
-
Remove
--base-urlsince you're serving from the domain root
Publishing to Netlify
1. Create a netlify.toml
Add this to your repository root:
[build]
command = "npx asnotes-publish --config ./asnotes-publish.json"
publish = "site"
[build.environment]
NODE_VERSION = "20"
2. Connect Your Repository
- Log in to Netlify and click Add new site > Import an existing project
- Connect your Git provider and select your repository
- Netlify auto-detects the
netlify.tomlsettings - Click Deploy site
Netlify automatically builds and deploys on every push. No --base-url is needed since Netlify serves from the domain root.
Custom Domain
In Site settings > Domain management, add your custom domain and follow Netlify's DNS instructions.
Publishing to Vercel
1. Configure Vercel
Create vercel.json in your repository root:
{
"buildCommand": "npx asnotes-publish --config ./asnotes-publish.json",
"outputDirectory": "site",
"framework": null
}
2. Connect Your Repository
- Log in to Vercel and click Add New > Project
- Import your Git repository
- Vercel reads
vercel.jsonfor build settings - Click Deploy
Vercel deploys on every push and provides a preview URL for each branch.
Publishing to Cloudflare Pages
1. Connect Your Repository
- Log in to the Cloudflare dashboard and go to Compute > Workers & Pages
- Click Create application > Pages > Connect to Git
- Select your repository
2. Configure Build Settings
In Build configuration, set the following:
| Setting | Value |
|---|---|
| Build command | npx @appsoftwareltd/asnotes-publish --config ./asnotes-publish.json |
| Build output directory | The outputDir value from your config file (e.g. site) |
| Root directory | The directory containing your config file (leave empty if at repo root) |
Under Variables and secrets, add:
| Variable | Value |
|---|---|
NODE_VERSION |
20 |
Root Directory
The Root directory setting controls the working directory for the build. Cloudflare changes to this directory before running the build command. All relative paths in your config file (inputDir, outputDir, layouts, includes, themes) resolve from there.
If your config file is at the repository root, leave Root directory empty. If your config file lives in a subdirectory, set Root directory to that subdirectory.
Example: config at repo root
my-notes/
asnotes-publish.json # inputDir: "./notes", outputDir: "./site"
notes/
Getting Started.md
| Setting | Value |
|---|---|
| Root directory | (empty) |
| Build command | npx @appsoftwareltd/asnotes-publish --config ./asnotes-publish.json |
| Build output directory | site |
Example: config in a subdirectory
my-repo/
docs-src/
asnotes-publish.blog.json # inputDir: "./blog", outputDir: "./blog-publish"
blog/
Welcome Post.md
| Setting | Value |
|---|---|
| Root directory | /docs-src |
| Build command | npx @appsoftwareltd/asnotes-publish --config ./asnotes-publish.blog.json |
| Build output directory | blog-publish |
The --config path and Build output directory are both relative to the Root directory. If Root directory is wrong, the build will fail because Cloudflare cannot find the config file or its referenced directories.
Click Save and Deploy. Cloudflare Pages deploys on every push with automatic preview deployments for branches. No --base-url is needed since Cloudflare Pages serves from the domain root.
Writing a Custom Layout CSS
This documentation site uses a two-column grid layout. Save this as layout.css:
*, *::before, *::after { box-sizing: border-box; }
body {
display: grid;
grid-template-columns: 220px 1fr;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
nav.site-nav {
background: #f6f8fa;
border-right: 1px solid #d0d7de;
padding: 1.5rem 1rem;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
nav.site-nav ul { list-style: none; margin: 0; padding: 0; }
nav.site-nav a {
display: block;
padding: 0.3rem 0.6rem;
border-radius: 6px;
text-decoration: none;
color: #24292f;
font-size: 0.875rem;
}
nav.site-nav a:hover { background: #eaeef2; color: #0550ae; }
nav.site-nav .nav-current a { background: #ddf4ff; color: #0550ae; font-weight: 600; }
article.markdown-body { padding: 2rem 3rem; max-width: 900px; }
@media (max-width: 700px) {
body { grid-template-columns: 1fr; }
nav.site-nav { position: relative; height: auto; border-right: none; border-bottom: 1px solid #d0d7de; }
article.markdown-body { padding: 1.5rem; }
}
Then convert:
npm run convert -- \
--input ./notes --output ./site \
--default-public --default-assets \
--stylesheet /path/to/layout.css