A clean MkDocs blog setup with Jac syntax highlighting and interactive, runnable Jac code blocks.
We welcome contributions from anyone in the community! If you'd like to write a blog post, we'd love to hear from you. Topics can include anything relevant to:
- Jac & Jaseci -- tutorials, tips, deep dives, project showcases
- AI & Machine Learning -- techniques, tools, research, opinions
- Cool Open Source Tools -- discoveries, comparisons, how-tos
- Open Source News -- notable releases, ecosystem updates, community happenings
- Anything Nerdy -- if it's insightful, creative, or just plain fun for a technical audience, we're interested
We'll review all submissions and happily accept anything that's insightful or otherwise enjoyable to the community. Don't be shy -- submit a PR!
- Fork the repo and create a new branch
- Add your post as a markdown file in
docs/blog/posts/:touch docs/blog/posts/my-awesome-post.md
- Add frontmatter to the top of your post.
draft: trueis required — a PR check will reject any new post without it. This is the safety rail that keeps an accidental merge from publishing a post. An editor flips it live later via the scheduling workflows.--- date: 2026-03-12 authors: - your_author_id categories: - Your Category slug: my-awesome-post draft: true ---
- Write your post in markdown (see Adding Jac Code Blocks for interactive code examples). We also love Mermaid diagrams -- use
```mermaidcode blocks to add flowcharts, sequence diagrams, and more. For custom graphics, we prefer SVGs since they scale nicely and keep the site looking crisp - If you need images (e.g., screenshots of what you built), place them in
docs/assets/and keep file sizes reasonable -- aim for under 100KB per image when possible. Compress PNGs/JPGs before committing - Open a pull request
Note on publish timing: an editor decides when your post goes live. Your post must include
draft: true(a PR check enforces this); the editor removes it via the scheduling workflows once a publish time is decided. If you have a target date in mind, mention it in the PR description. See Editorial Scheduling for how it works.
Posts are not published the instant they merge. An editor schedules each post for a specific UTC publish time. The source of truth is docs/blog/.schedule.yml, and there are three ways for editors to drive it -- none require cloning the repo:
1. Slash-commands on the PR. Right in the PR conversation:
/schedule 2026-06-01T14:00:00Z
/schedule 2026-06-01T14:00:00Z waiting on marketing
/hold
/cancel
/publish-now
/hide outdated benchmarks, will revisit (draft in place — reversible)
/unlist author asked to retract (move to unlisted/)
/archive superseded by 2026 retrospective (move to archived_posts/)
These work both in a PR conversation and on the auto-created scheduling issue (see the release flow below). The post's slug is inferred from the PR's changed files, or from the scheduling issue itself. To target a different post from any thread, append slug=<name>.
2. "Schedule a post" workflow. Under the repo's Actions tab, run the Schedule a post workflow. It's a form with action (add / hold / cancel / publish-now), slug, publish_at, and notes inputs. Use this for posts that have already merged.
3. "Take down a post" workflow. Also under the Actions tab. Pulls a live post off the site with a destination choice:
| Destination | What it does | Use when |
|---|---|---|
draft |
Sets draft: true in the post's frontmatter, file stays in docs/blog/posts/ |
Temporary hide. Easy to re-publish with /publish-now. |
unlist |
Moves the file to docs/blog/unlisted/ and adds draft: true defensively |
You may revisit it but don't want it anywhere near the live site. |
archive |
Moves the file to docs/blog/archived_posts/ |
Permanently retired (outdated, superseded by another post). |
The blog plugin only indexes docs/blog/posts/, so unlist and archive make the post invisible by location, not by frontmatter — there's no way it can accidentally render.
4. "List schedulable posts" workflow. Run it (no inputs) to dump a markdown table of every post with its draft state and active schedule entry — handy when you need to look up a slug.
5. Hourly auto-publisher. A cron workflow runs at five-past every hour, finds entries whose publish_at has passed, removes draft: true from each post's frontmatter, and commits. That commit triggers the existing deploy. Precision is ~1 hour (GitHub-hosted cron is best-effort and can lag by 10–20 minutes under load).
On a first publish the post's date: is rewritten to the actual publish day, so readers see when the post went live. On a re-publish (a previously-live post that was /hided and is going back up), the original date: is preserved — RSS feeds, bookmarks, and "originally published on" attributions stay stable.
Every scheduling action -- add, hold, cancel, manual publish, auto-publish -- is a real commit on main made by github-actions[bot], so git log docs/blog/.schedule.yml is the full editorial history.
Anyone with write access on the repo can run these. The slash-command workflow additionally checks author_association so random PR commenters can't trigger it.
When a post PR passes CI, the bot opens a scheduling issue (labelled blog-schedule) and links it on the PR. That issue — not the PR — is where editors schedule the post, so scheduling is decoupled from the merge:
- Author opens a PR adding
docs/blog/posts/<slug>.mdwithdraft: true. - CI (Lint new posts) checks the draft flag. On success the bot opens a scheduling issue (+ a docs-reference issue in the Jac docs repo if configured) and posts a sticky comment on the PR.
- Reviewer approves and merges — the merge is gated on CI + review, not on scheduling.
- Editor comments
/schedule <ISO8601 UTC>on the scheduling issue (the same slash-commands as above; they take effect once the post is onmain). - Auto-publisher flips the post live at the scheduled time, closes the scheduling issue with the final URL, and the commit to
maintriggers the deploy.
A post's URL is permanent and slug-only — https://blogs.jaseci.org/blog/posts/<slug> — and the Jac app shows an in-page "coming soon" state for that URL until the post is live, then the same link serves the article. The scheduling issue stays open until the post publishes, so a merged-but-unscheduled post is never silently forgotten.
Full details — triggers, tokens, idempotency markers, deploy coupling, and the one-time GitHub App setup — are in .github/RELEASE_FLOW.md.
If you're a new contributor, add yourself to docs/blog/.authors.yml:
authors:
# ... existing authors ...
your_author_id:
name: Your Name
description: A short bio about yourself
avatar: https://avatars.githubusercontent.com/u/YOUR_GITHUB_USER_ID?v=4To get your GitHub avatar URL, just replace YOUR_GITHUB_USER_ID with your numeric GitHub user ID. You can find your ID by visiting https://api.github.com/users/YOUR_GITHUB_USERNAME -- look for the id field in the response. This will make your profile picture show up nicely alongside your posts.
- Jac Syntax Highlighting: Beautiful syntax highlighting for Jac code using custom Pygments and Monaco lexers
- Interactive Code Blocks: Run Jac code directly in the browser using Pyodide (WebAssembly)
- Clean Design: Built with MkDocs Material theme
- Easy to Use: Simple markdown-based content creation
- Fast: Static site generation for optimal performance
- Python 3.8 or higher
- pip (Python package installer)
- Git
-
Clone this repository (if you haven't already):
cd ~/blog
-
Install all dependencies and the Jac syntax highlighter:
pip install -e .This will install all required dependencies (mkdocs-material, pymdown-extensions, pygments, mkdocs-video, starlette, uvicorn) and register the Jac syntax highlighter.
To start the development server with interactive code execution:
python scripts/mkdocs_serve.pyThis will start a server at http://127.0.0.1:8000 with the necessary CORS headers for Pyodide to work.
Note: The custom server (
mkdocs_serve.py) is required for runnable code blocks to work properly because it sets up the CORS headers needed for SharedArrayBuffer support.
Alternatively, for basic preview without runnable code blocks:
mkdocs serveTo build the static site:
mkdocs buildThe built site will be in the site/ directory.
Deploy to GitHub Pages:
mkdocs gh-deploy-
Create a new markdown file in
docs/posts/:touch docs/posts/my-new-post.md
-
Add the post to the navigation in
mkdocs.yml:nav: - Posts: - My New Post: posts/my-new-post.md
For code that just needs syntax highlighting:
```jac
with entry {
print("Hello, World!");
}
```For code that users can edit and run in the browser:
<div class="code-block">
```jac
with entry {
print("Hello, World!");
}
```
</div>This will add a "Run" button that executes the code in the browser.
You can customize the buttons shown:
- Run only (default):
<div class="code-block"> - Run and Serve:
<div class="code-block run-serve"> - Serve only:
<div class="code-block serve-only">
Example:
<div class="code-block run-serve">
```jac
with entry {
print("This has both Run and Serve buttons!");
}
```
</div>~/blog/
├── docs/ # Documentation source files
│ ├── index.md # Homepage
│ ├── about.md # About page
│ ├── posts/ # Blog posts
│ │ └── welcome.md # Example post
│ ├── js/ # JavaScript files
│ │ ├── jac.monarch.js # Monaco Editor Jac syntax
│ │ ├── run-code.js # Interactive code execution
│ │ └── pyodide-worker.js # Pyodide web worker
│ ├── playground/ # Playground resources
│ │ ├── language-configuration.json
│ │ └── jaclang.zip # (Generated by build hook)
│ ├── extra.css # Custom CSS styling
│ └── assets/ # Images and other assets
├── scripts/ # Build and serve scripts
│ ├── handle_jac_compile_data.py # Build hook for Jac compiler
│ └── mkdocs_serve.py # Custom dev server
├── overrides/ # Theme overrides (optional)
├── jac_syntax_highlighter.py # Pygments lexer for Jac
├── mkdocs.yml # MkDocs configuration
└── README.md # This file
The blog uses two lexers for syntax highlighting:
- Pygments Lexer (
jac_syntax_highlighter.py): Used for server-side static code highlighting during build - Monaco Monarch Lexer (
docs/js/jac.monarch.js): Used for client-side syntax highlighting in the interactive code editor
The runnable code blocks use:
- Pyodide: A Python runtime compiled to WebAssembly that runs in the browser
- Monaco Editor: The same code editor that powers VS Code
- Web Workers: For isolated code execution without blocking the UI
- SharedArrayBuffer: For synchronous input handling (requires special CORS headers)
When you click "Run":
- The code is loaded into Monaco Editor
- A web worker initializes Pyodide and loads the Jac compiler
- The code is executed in the browser
- Output is streamed back to the page in real-time
Edit the palette section in mkdocs.yml:
theme:
palette:
scheme: slate # Use 'default' for light mode
primary: black # Primary color
accent: orange # Accent colorEdit the extra.social section in mkdocs.yml:
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/yourusername
- icon: fontawesome/brands/twitter
link: https://twitter.com/yourusernameAdd your custom styles to docs/extra.css.
- Make sure you're using the custom server:
python scripts/mkdocs_serve.py - The custom server sets CORS headers required for SharedArrayBuffer
- Check browser console for errors
- Ensure
jac_syntax_highlighter.pyis installed properly - Try rebuilding:
mkdocs build --clean
- Make sure you have the Jac compiler installed
- The hook tries to create
docs/playground/jaclang.zipfrom your Jac installation - If you don't have Jac installed, comment out the hook in
mkdocs.yml
Core dependencies:
mkdocs-material: Material theme for MkDocspymdown-extensions: Markdown extensions for code highlightingpygments: Syntax highlighting librarystarlette: ASGI framework for custom serveruvicorn: ASGI server
Optional dependencies:
mkdocs-video: Video embedding support
[Add your license here]
- Built with MkDocs and Material for MkDocs
- Interactive code execution powered by Pyodide
- Code editor powered by Monaco Editor
- Based on the excellent documentation setup from the Jaseci project