A Rudimentary Blog in Swift with Build Tool Plugins

April 19, 2025


Swift is fun to write in. Wouldn't it be fun to write a markdown.md file, put it in the Sources folder, push the change, and almost have a live blog post?

It would, and it doesn't take much code at all!

For a primer on how to get past the Xcode ceremonies, see Apple's Create Swift Package Plugins.

With that out of the way, here's how to collect all .md files from your app sources:

@main
struct MarkdownHTML: BuildToolPlugin {
  func createBuildCommands(
    context: PackagePlugin.PluginContext,
    target: any PackagePlugin.Target
  ) async throws -> [PackagePlugin.Command] {
    enum MarkdownHTMLError: Error {
      case failedToReadInputDirectory
    }
    guard let target = target as? SourceModuleTarget else {
      Diagnostics.emit(.error, "🔴 GenerateSwiftMarkdown failed")
      throw MarkdownHTMLError.failedToReadInputDirectory
    }
    return try target.sourceFiles(withSuffix: "md").map { file in
      let base = file.url.deletingPathExtension().lastPathComponent
      let input = file.url
      let output = context.pluginWorkDirectoryURL.appending(path: "\(base).swift")

      return .buildCommand(
        displayName: "Generating HTML from Markdown \(base)",
        executable: try context.tool(named: "MarkdownHTMLExec").url,
        arguments: [input.path, output.path],
        inputFiles: [input],
        outputFiles: [output]
      )
    }
  }
}

And here's the executable that runs Ink to parse Markdown into HTML:

let fileManager = FileManager.default
let arguments = ProcessInfo.processInfo.arguments
if arguments.count < 3 {
  throw MarkdownHTMLExec.insufficientArguments
}
let (input, output) = (arguments[1], arguments[2])

let markdown = try String(contentsOfFile: input)
let parser = MarkdownParser()
let html = parser.html(from: markdown)

...

What's left is to grab that variable and render it. That's it!

While this post focused on the plugin part, the complete source code of this website is open source.