JohnSundell/Publish
A static site generator for Swift developers
Stale — last commit 2y ago
worst of 4 axeslast commit was 2y ago; no CI workflows detected
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
last commit was 2y ago; no CI workflows detected
- ✓38+ active contributors
- ✓Distributed ownership (top contributor 49% of recent commits)
- ✓MIT licensed
Show 3 more →Show less
- ✓Tests present
- ⚠Stale — last commit 2y ago
- ⚠No CI workflows detected
What would change the summary?
- →Use as dependency Mixed → Healthy if: 1 commit in the last 365 days
- →Deploy as-is Mixed → Healthy if: 1 commit in the last 180 days
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Forkable" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/johnsundell/publish)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/johnsundell/publish on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: JohnSundell/Publish
Generated by RepoPilot · 2026-05-10 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/JohnSundell/Publish shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
WAIT — Stale — last commit 2y ago
- 38+ active contributors
- Distributed ownership (top contributor 49% of recent commits)
- MIT licensed
- Tests present
- ⚠ Stale — last commit 2y ago
- ⚠ No CI workflows detected
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live JohnSundell/Publish
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/JohnSundell/Publish.
What it runs against: a local clone of JohnSundell/Publish — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in JohnSundell/Publish | Confirms the artifact applies here, not a fork |
| 2 | License is still MIT | Catches relicense before you depend on it |
| 3 | Default branch master exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 700 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of JohnSundell/Publish. If you don't
# have one yet, run these first:
#
# git clone https://github.com/JohnSundell/Publish.git
# cd Publish
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of JohnSundell/Publish and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "JohnSundell/Publish(\\.git)?\\b" \\
&& ok "origin remote is JohnSundell/Publish" \\
|| miss "origin remote is not JohnSundell/Publish (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
&& ok "license is MIT" \\
|| miss "license drift — was MIT at generation time"
# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
&& ok "default branch master exists" \\
|| miss "default branch master no longer exists"
# 4. Critical files exist
test -f "Sources/Publish/API/Website.swift" \\
&& ok "Sources/Publish/API/Website.swift" \\
|| miss "missing critical file: Sources/Publish/API/Website.swift"
test -f "Sources/Publish/Internal/PublishingPipeline.swift" \\
&& ok "Sources/Publish/Internal/PublishingPipeline.swift" \\
|| miss "missing critical file: Sources/Publish/Internal/PublishingPipeline.swift"
test -f "Sources/Publish/API/HTMLFactory.swift" \\
&& ok "Sources/Publish/API/HTMLFactory.swift" \\
|| miss "missing critical file: Sources/Publish/API/HTMLFactory.swift"
test -f "Sources/Publish/Internal/MarkdownContentFactory.swift" \\
&& ok "Sources/Publish/Internal/MarkdownContentFactory.swift" \\
|| miss "missing critical file: Sources/Publish/Internal/MarkdownContentFactory.swift"
test -f "Sources/PublishCLICore/CLI.swift" \\
&& ok "Sources/PublishCLICore/CLI.swift" \\
|| miss "missing critical file: Sources/PublishCLICore/CLI.swift"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 700 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~670d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/JohnSundell/Publish"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
Publish is a static site generator written in Swift that allows developers to build entire websites using type-safe Swift code as configuration. Instead of YAML/TOML frontmatter and separate markdown files, websites are defined as Swift packages with custom Website structs, section enums, and item metadata types, then rendered to HTML/RSS/sitemaps via a fluent API. It powers swiftbysundell.com and includes built-in themes (Foundation), plugin support, and deployment methods. Single Swift package with monolithic structure: Sources/Publish/API/ contains ~50 core types (Item, Page, Section, Tag, Content, HTMLFactory, Plugin, PublishingContext, etc.) that define the domain model and rendering pipeline. Resources/FoundationTheme/ holds the built-in CSS theme. Websites using Publish import this library and define their own Website struct + Swift package configuration.
👥Who it's for
Swift developers and content creators who want to build blogs, documentation sites, or podcasts using Swift package configuration instead of traditional static site generators. Specifically suits developers comfortable with type systems and Swift-based tooling who want their site structure enforced by the compiler.
🌱Maturity & risk
Actively maintained and production-ready. The repo shows substantial Swift codebase (~276KB), ships with real themes and plugins, and is used on a live production site (swiftbysundell.com). The single-maintainer model (JohnSundell) and lack of visible recent commit metadata in the provided snapshot are mild concerns, but the established usage and thorough documentation indicate stability.
Single-maintainer project with no visible CI configuration in the file list—typical of older Swift projects before GitHub Actions maturity. Dependency surface is unclear from Package.swift not being fully shown. The abstraction-heavy design (ContentProtocol, HTMLFactory, PlotModifiers) could make onboarding steeper than traditional generators. No test files visible in the top 60, which is a red flag for a publishing tool handling user content.
Active areas of work
No recent commit metadata provided in the snapshot, so activity status is unclear. The Documentation/ folder contains HowTo guides on syntax highlighting (highlight.js, Pygments, Splash), Disqus comments, custom metadata, nested items, and date formatting—suggesting the project has been feature-complete for a while and focuses on documentation and plugin support.
🚀Get running
git clone https://github.com/JohnSundell/Publish.git
cd Publish
swift build
Then explore Sources/Publish/API/ to understand the core types, or check Documentation/ for usage guides.
Daily commands:
This is a library, not an app. To see it work: create a new Swift package importing Publish, define a Website struct, call .publish(withTheme:). For local development: swift build in repo root. For integration: swift package resolve && swift build in a consuming site package.
🗺️Map of the codebase
Sources/Publish/API/Website.swift— Core Website protocol that defines the contract all sites must implement; foundational to understanding Publish's type-safe architectureSources/Publish/Internal/PublishingPipeline.swift— Orchestrates the entire generation workflow; essential to understand how publishing steps are composed and executedSources/Publish/API/HTMLFactory.swift— Protocol defining how HTML is generated; central to theme rendering and customizationSources/Publish/Internal/MarkdownContentFactory.swift— Handles parsing markdown files into content; critical for content ingestion pipelineSources/PublishCLICore/CLI.swift— Main CLI entry point for project generation, building, and deployment commandsSources/Publish/API/PublishingContext.swift— Context object passed through publishing pipeline containing site state, sections, and metadataSources/Publish/API/Theme.swift— Theme abstraction allowing websites to define HTML generation logic; key to extensibility
🛠️How to make changes
Add a new content section to your site
- Add a new enum case to your Website's SectionID type (
YourWebsite.swift (in your generated website package)) - Implement HTMLFactory to render pages for that section (implement methods like
makeIndexHTML,makeSectionHTML) (Sources/Publish/API/HTMLFactory.swift (protocol reference)) - Create markdown files in
Content/SectionName/directory (Content/ (in your website package)) - The PublishingPipeline will automatically discover, parse, and generate HTML for the new section (
Sources/Publish/Internal/PublishingPipeline.swift (reference))
Create a custom theme
- Create a new Swift package or add a new Theme extension (
Sources/Publish/API/Theme.swift (implements this protocol)) - Implement HTMLFactory conformance with all required HTML generation methods (
Sources/Publish/API/HTMLFactory.swift (protocol to implement)) - Use Plot (Publish's HTML DSL) to generate HTML; optionally add CSS in Resources/ (
Sources/Publish/API/PlotComponents.swift (available components reference)) - In your Website.swift, set
.withTheme(.yourCustomTheme)during publishing (YourWebsite.swift (in website package))
Add custom metadata to items (e.g., reading time, categories)
- Extend Item's Metadata with new fields (use associated types if needed) (
Sources/Publish/API/Item.swift (modify or subclass)) - Add corresponding fields to your markdown frontmatter (
Content/YourSection/post.md (in website package)) - Update MarkdownMetadataDecoder if using custom decodable types (
Sources/Publish/Internal/MarkdownMetadataDecoder.swift) - Reference the new metadata in your HTMLFactory implementation (
Sources/Publish/API/HTMLFactory.swift (in your theme))
Add a custom publishing step (e.g., minify HTML, generate search index)
- Create a function returning
PublishingStep<YourSiteType>(Sources/Publish/API/PublishingStep.swift (protocol reference)) - Within the step, mutate the PublishingContext to transform generated files or content (
Sources/Publish/API/PublishingContext.swift (the context you receive)) - Add the step to your Website's publishing sequence via
.addPublishingStep(yourCustomStep)(YourWebsite.swift (in website package))
🔧Why these technologies
- Swift & Swift Package Manager — Type-safe, compile-time verification of site configuration; leverages Swift ecosystem for plugins and extensions
- Plot (HTML DSL) — Type-safe HTML generation within Swift; prevents malformed markup and enables compile-time validation
- Markdown + YAML Frontmatter — Simple, portable content format familiar to developers; YAML allows rich metadata without custom parsers
- Protocol-oriented design (HTMLFactory, Theme, Plugin) — Enables flexible, pluggable architecture; users can inject custom behavior without forking Publish
⚖️Trade-offs already made
-
Static-first (no server-side rendering at runtime)
- Why: Simplicity, performance, and SEO; eliminates runtime dependencies
- Consequence: Dynamic content requires JavaScript on the client or pre-generation of variants at build time
-
Swift-only configuration (no TOML/YAML config files)
- Why: Type safety, IDE autocomplete, and native Swift ecosystem integration
- Consequence: Requires Swift knowledge; steeper learning curve for non-Swift developers
-
Single-pass publishing pipeline
- Why: Simpler mental model and faster builds for most sites
- Consequence: Cannot easily generate content that depends on previously generated content
🪤Traps & gotchas
No test suite visible in file list—contributing to core types has no regression safety net. PlotComponents.swift and PlotEnvironmentKeys.swift suggest deep Plot library integration; breaking changes in Plot versions could silently break rendering without tests. No CI config visible, so builds may not be validated across Swift versions. The abstraction layers (ContentProtocol, HTMLFactory protocol) mean a small typo in a theme can fail at render time rather than compile time if the protocol is loose. Documentation assumes familiarity with Swift package structure and Codable metadata serialization.
🏗️Architecture
💡Concepts to learn
- Protocol-oriented theme composition (HTMLFactory protocol) — Themes in Publish are implementations of the HTMLFactory protocol rather than template files—understanding this protocol-based architecture is essential to customizing how your site renders
- Static site generation pipeline (PublishingStep chains) — Publish uses composable async steps to transform content from markdown → AST → rendered HTML → deployed files; grasping this pipeline is necessary to add custom processing (e.g., image optimization, analytics injection)
- Type-safe metadata (Codable + custom ItemMetadata protocol) — Unlike YAML frontmatter in other generators, Publish enforces metadata types at compile time via Swift structs conforming to Codable—this prevents runtime parsing errors but requires understanding Swift serialization
- Plugin system (PublishingContext mutation) — Plugins intercept the PublishingContext at various stages to mutate content, add RSS properties, or deploy artifacts—learning to write plugins is the primary way to extend Publish beyond built-in features
- ContentProtocol abstraction (Item vs Page unification) — Both Item (timestamped, tagged content) and Page (static content) conform to ContentProtocol, allowing one HTMLFactory to render both—this unification pattern is key to reducing boilerplate but can be confusing if you don't understand the protocol hierarchy
- RSS feed generation from type-safe config (RSSFeedConfiguration + ItemRSSProperties) — Publish auto-generates RSS feeds from your content using RSSFeedConfiguration; understanding ItemRSSProperties lets you customize feed appearance per-item without manual XML
- Plot HTML DSL (type-safe HTML generation) — Publish renders HTML via Plot, a Swift DSL that prevents malformed HTML at compile time—unlike string templates, invalid tags are caught before generation, but requires learning Plot's component API
🔗Related repos
apple/swift-markdown— Official Swift markdown parsing library that Publish likely uses or could use for parsing .md files into Content objectsJohnSundell/Plot— John Sundell's type-safe HTML/RSS DSL library—Publish depends on this for generating HTML without string concatenationgetHarmonized/harmonized— Alternative static site generator for Swift, competes with Publish but uses different theme/plugin approachSwiftGithubActions/setup-swift-action— Enables CI/CD for Publish-based sites using GitHub Actions to rebuild on push—practical companion for deploying Publish sitesJohnSundell/Splash— Syntax highlighter by the same author; Publish integrates with it as documented inSyntaxHighlighting/using-splash.md
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add unit tests for MarkdownMetadataDecoder.swift
The MarkdownMetadataDecoder is a critical internal component for parsing YAML-style metadata from markdown files, but there are no visible test files in the repo structure. This decoder handles custom metadata values (documented in Documentation/HowTo/custom-markdown-metadata-values.md) and would benefit from comprehensive unit tests covering edge cases like malformed metadata, special characters, nested structures, and type conversions.
- [ ] Create Tests/Publish/Internal/MarkdownMetadataDecoderTests.swift
- [ ] Add test cases for valid metadata parsing with various types (String, Int, Date, custom types)
- [ ] Add test cases for error handling (malformed YAML, missing required fields, type mismatches)
- [ ] Add test cases for edge cases (empty metadata, special characters, Unicode)
- [ ] Verify integration with MarkdownFileHandler.swift for end-to-end scenarios
Add GitHub Actions CI/CD workflow for Swift Package Manager validation
The repo shows Swift 5.5+ support with SPM compatibility badge, but there's no visible CI workflow file (.github/workflows/) to validate builds, run tests, and verify package integrity across macOS and Linux. This is essential for a widely-used package to catch regressions early and ensure contributor PRs meet quality standards.
- [ ] Create .github/workflows/swift-ci.yml with Swift build and test jobs
- [ ] Configure matrix for macOS and Ubuntu Linux runners
- [ ] Add 'swift build' and 'swift test' steps
- [ ] Add SwiftLint or similar static analysis if appropriate
- [ ] Ensure workflow runs on push to main and all pull requests
Document PodcastFeedGenerator and podcast-specific features with working example
The codebase includes sophisticated podcast support (PodcastEpisodeMetadata.swift, PodcastFeedConfiguration.swift, PodcastFeedGenerator.swift, PodcastCompatibleWebsiteItemMetadata.swift), but there's no corresponding how-to guide in Documentation/HowTo/. Given the complexity of podcast RSS generation and iTunes/Apple Podcasts requirements, new contributors lack guidance on this feature.
- [ ] Create Documentation/HowTo/creating-a-podcast-feed.md
- [ ] Document required metadata fields (author, episode duration, iTunes image requirements, explicit flag)
- [ ] Include step-by-step example showing PodcastFeedConfiguration setup
- [ ] Explain how PodcastEpisodeMetadata integrates with Item metadata
- [ ] Add section on validating generated feed against Apple Podcasts specifications
- [ ] Link to PodcastFeedConfiguration.swift and PodcastAuthor.swift API reference
🌿Good first issues
- Add a test suite: Create
Tests/PublishTests/with at least 10 unit tests covering PublishingContext initialization, ContentProtocol conformance (Item vs Page), HTMLFactory mock implementations, and Plugin composition. Start withSources/Publish/API/SectionMap.swiftwhich has no visible tests. - Document the plugin system with a concrete example: The repo has
Plugin.swiftbut no example plugin in Documentation/. CreateDocumentation/HowTo/creating-a-custom-plugin.mdshowing how to build a plugin that adds a 'last modified' footer to all items usingPublishingStep. - Add missing metadata validation docs:
Sources/Publish/API/CustomMetadataValue.swift(or equivalent) is referenced by HowTo guides but not shown in the file list. Document the Codable requirements and common pitfalls for custom metadata types with a worked example for a 'reading time' field on blog items.
⭐Top contributors
Click to expand
Top contributors
- @JohnSundell — 42 commits
- @MaxDesiatov — 3 commits
- @uhooi — 2 commits
- @bensyverson — 2 commits
- @alexito4 — 2 commits
📝Recent commits
Click to expand
Recent commits
58e9430— Foundation theme: Add margin to tags (#133) (uhooi)fb0e911— Merge pull request #143 from yonihemi/public_async (JohnSundell)d0ac447— Merge pull request #144 from ramzesenok/master (JohnSundell)815b497— Fixed typo (Roman Mirzoyan)c184931— Make async versions ofpublishpublic (yonihemi)36c60df— README: Add paragraph explaining that Homebrew support isn't officially maintained (JohnSundell)1c8ad00— README: Update Swift version badge to 5.5 (JohnSundell)2beedc7— CLI: Always use Python 3 (#132) (JohnSundell)a0d30bc— Add initial support for Swift Concurrency (#117) (JohnSundell)f8c386f— Bump Publish from 0.7.0 to 0.8.0 (#128) (uhooi)
🔒Security observations
The Publish static site generator has a moderate security posture. The main concerns are around XSS prevention in HTML generation, metadata deserialization safety, and file path validation. As a build-time tool (not a runtime server), the attack surface is limited. However, if deployed content is user-controlled or the tool processes untrusted input, proper sanitization is critical. No hardcoded secrets were detected in the file structure. Dependency analysis is incomplete without the Package.resolved content. Regular security audits and dependency updates are recommended.
- Medium · Potential XSS Risk in HTML Generation —
Sources/Publish/Internal/HTMLGenerator.swift, Sources/Publish/API/PlotComponents.swift. The codebase includes HTMLGenerator.swift and PlotComponents.swift which handle HTML generation. Without explicit evidence of output encoding, there's a risk of Cross-Site Scripting (XSS) if user-provided markdown or metadata is rendered without proper sanitization. Fix: Ensure all user-provided content (from markdown, metadata, and configuration) is properly HTML-escaped before rendering. Verify that the Plot library (dependency) properly escapes content by default. - Medium · Markdown Metadata Deserialization —
Sources/Publish/Internal/MarkdownMetadataDecoder.swift. MarkdownMetadataDecoder.swift deserializes metadata from markdown files. If deserialization is not properly validated, this could lead to arbitrary code execution or data corruption attacks. Fix: Implement strict schema validation for metadata. Use allowlisting for accepted metadata keys and types. Avoid using unsafe deserialization methods. - Medium · File System Access Without Validation —
Sources/Publish/Internal/File+SwiftPackageFolder.swift, Sources/Publish/Internal/Folder+Group.swift, Sources/Publish/Internal/MarkdownFileHandler.swift. Multiple file handling utilities exist (File+SwiftPackageFolder.swift, Folder+Group.swift, MarkdownFileHandler.swift) that read and write files. Path traversal vulnerabilities could occur if user-controlled paths are not properly validated. Fix: Implement strict path validation to prevent directory traversal attacks. Canonicalize all file paths and ensure they remain within expected directories. Use whitelisting for allowed paths. - Low · Missing Dependency Lock File Details —
Package.resolved. Package.resolved exists but its content is not provided for analysis. Dependency vulnerabilities cannot be fully assessed without reviewing specific versions and known CVEs. Fix: Regularly audit dependencies using Swift Package Manager security tools. Review Package.resolved for outdated or vulnerable dependency versions. Consider using tools like 'swift package describe' to identify vulnerable dependencies. - Low · Potential Information Disclosure via Error Messages —
Sources/Publish/Internal/FileIOError.swift, Sources/Publish/Internal/ContentError.swift, Sources/Publish/Internal/PodcastError.swift. Multiple error types exist (FileIOError.swift, ContentError.swift, PodcastError.swift, ShellOutError+PublishingErrorConvertible.swift). If these errors are exposed to users/logs without proper filtering, they may leak sensitive system information. Fix: Sanitize error messages before display to end users. Log detailed errors securely server-side only. Provide generic error messages to users to avoid information disclosure. - Low · Shell Command Execution Risk —
Sources/Publish/Internal/ShellOutError+PublishingErrorConvertible.swift, Sources/PublishCLICore/WebsiteDeployer.swift. ShellOutError handling suggests shell command execution occurs (likely via ShellOut library). If user input is passed to shell commands without proper escaping, command injection is possible. Fix: Avoid shell execution where possible. If required, use properly escaped parameters and avoid string concatenation. Use array-based argument passing if the underlying library supports it.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.