Create content for your site from existing sources.
Static site generators are great for blog posts you write by hand — but most of a site's content already lives somewhere else: a YouTube channel, RSS feeds, a newsletter archive, an old CMS export. Contribute turns those source items into clean markdown files with YAML front matter, ready to drop into your generator's content folder.
It does this with one small, reusable shape so you don't write a bespoke importer per source. You write two tiny conformances per source; the fetch + extract + write pipeline stays the same.
Scope: Contribute does not fetch from APIs. You bring the already-decoded source models — use SyndiKit for RSS, a client for the YouTube API, your CMS's export, etc. Contribute's job starts there: source model → markdown file.
brightdigit.com uses Contribute to import its podcast episodes, videos, and newsletters into a Swift static site generator.
Two protocols do the per-source work, and a third binds them to a single source model.
| Protocol | Responsibility |
|---|---|
FrontMatterTranslator |
Map a source item's fields onto your site's (Encodable) front matter. |
MarkdownExtractor |
Render a source item's body into markdown, using an injected HTML→markdown function. |
ContentType |
Bind one SourceType to its translator and extractor — and get a generic write(...) for free. |
public protocol FrontMatterTranslator {
associatedtype SourceType
associatedtype FrontMatterType: Encodable
init()
func frontMatter(from source: SourceType) -> FrontMatterType
}
public protocol MarkdownExtractor {
associatedtype SourceType
init()
func markdown(
from source: SourceType,
using htmlToMarkdown: @escaping (String) throws -> String
) throws -> String
}
public protocol ContentType {
associatedtype SourceType
associatedtype MarkdownExtractorType: MarkdownExtractor
where MarkdownExtractorType.SourceType == SourceType
associatedtype FrontMatterTranslatorType: FrontMatterTranslator
where FrontMatterTranslatorType.SourceType == SourceType
}SourceType is your model — Contribute never defines it and never fetches it. That seam is what keeps the library source-agnostic.
Add Contribute to your Package.swift:
dependencies: [
.package(url: "https://github.com/brightdigit/Contribute.git", from: "1.0.0-alpha.1")
]Then add it to a target:
.target(
name: "MySite",
dependencies: [.product(name: "Contribute", package: "Contribute")]
)Say you've already fetched an RSS feed into an array of your own RSSItem values and want one markdown file per entry.
import Contribute
import Foundation
// 1. Your site's front matter — anything Encodable.
struct PostFrontMatter: Encodable {
let title: String
let date: Date
let tags: [String]
}
// 2. Your already-decoded source model. Conform to HTMLSource to reuse the
// built-in FilteredHTMLMarkdownExtractor for the body.
struct RSSItem: HTMLSource {
let title: String
let published: Date
let categories: [String]
let html: String // satisfies HTMLSource
}
// 3. A ContentType binds the source to its translator + extractor.
enum RSSContent: ContentType {
typealias SourceType = RSSItem
typealias FrontMatterTranslatorType = FrontMatter
typealias MarkdownExtractorType = FilteredHTMLMarkdownExtractor<RSSItem>
struct FrontMatter: FrontMatterTranslator {
func frontMatter(from item: RSSItem) -> PostFrontMatter {
PostFrontMatter(
title: item.title,
date: item.published,
tags: item.categories
)
}
}
}
// 4. Write every item to disk. The pipeline is generic over the trio.
let htmlToMarkdown = SwiftSoupMarkdownGenerator().markdown(fromHTML:)
try RSSContent.write(
from: items, // [RSSItem] you fetched
atContentPathURL: URL(fileURLWithPath: "Content/posts"),
fileNameWithoutExtension: { $0.title.lowercased().replacingOccurrences(of: " ", with: "-") },
using: htmlToMarkdown,
options: .init(shouldOverwriteExisting: true, includeMissingPrevious: false)
)Each file is written as YAML front matter (encoded with Yams) followed by the extracted markdown body:
---
title: My Episode Title
date: 2026-06-29T12:00:00Z
tags:
- swift
- podcast
---
The body, converted from HTML to clean markdown.Every source is the same shape with different field mappings. Want YouTube, Mailchimp, or a WordPress export? Decode it into your own model, then write a ContentType with a FrontMatterTranslator (and a MarkdownExtractor if the body isn't plain HTMLSource). The write(...) call site never changes.
enum YouTubeContent: ContentType {
typealias SourceType = YouTubeVideo
typealias FrontMatterTranslatorType = FrontMatter
typealias MarkdownExtractorType = FilteredHTMLMarkdownExtractor<YouTubeVideo>
struct FrontMatter: FrontMatterTranslator {
func frontMatter(from video: YouTubeVideo) -> PostFrontMatter {
PostFrontMatter(title: video.title, date: video.publishedAt, tags: video.tags)
}
}
}SwiftSoupMarkdownGenerator— a SwiftSoup-backed HTML→markdown converter (built on swift-markdown). Pass itsmarkdown(fromHTML:)as theusing:argument.PassthroughMarkdownGenerator— use.sharedwhen your source body is already markdown.FilteredHTMLMarkdownExtractor<Source: HTMLSource>— a ready-madeMarkdownExtractorfor any source that exposesvar html: String.FrontMatterYAMLExporter— encodes yourEncodablefront matter to YAML.FileNameGenerator/ContentURLGenerator— control the destination file name/path per source.MarkdownContentBuilderOptions—shouldOverwriteExisting(overwrite existing files) andincludeMissingPrevious(controls pruning of items that have disappeared from the source).
ContentType.write(...) is generic over the trio and identical for every source. For each item it:
- translates front matter (
FrontMatterTranslator→ YAML viaFrontMatterYAMLExporter), - extracts the markdown body (
MarkdownExtractor, using the injectedhtmlToMarkdown), - joins them and writes the file at a destination derived from
ContentURLGenerator.
Options handle overwriting existing files and pruning entries that have dropped out of the source feed.
- Swift 6.4+ (the package builds in Swift 6 language mode with complete strict concurrency checking)
- macOS 12+, iOS 13+, tvOS 13+, watchOS 6+
MIT © BrightDigit
