Create content for your site from Mailchimp newsletters.
Your newsletter archive is already a body of writing — it just lives inside Mailchimp instead of on your site. ContributeMailchimp turns sent Mailchimp campaigns into Markdown files with YAML front matter, ready to drop into a static site generator's content folder.
It is the Mailchimp adapter for Contribute, the
source-model-to-Markdown pipeline behind brightdigit.com. Contribute is deliberately
source-agnostic: it defines the shape of an import — a ContentType binding a source model to a
front-matter translator and a Markdown extractor — but it never talks to an API. This package
fills in the Mailchimp-specific half:
- it fetches campaigns and their archive HTML with Spinetail, the BrightDigit Mailchimp API client, and
- it models a newsletter issue as the trio Contribute expects —
Newsletter(theContentType),Newsletter.Source,Newsletter.FrontMatterTranslator, andNewsletter.MarkdownExtractor— so Contribute's genericwrite(...)does the rest.
brightdigit.com uses it to import its newsletter archive alongside its podcast episodes and
videos.
Add ContributeMailchimp to your Package.swift:
dependencies: [
.package(url: "https://github.com/brightdigit/ContributeMailchimp.git", from: "1.0.0-alpha.1")
]Then add it to a target:
.target(
name: "MySite",
dependencies: [.product(name: "ContributeMailchimp", package: "ContributeMailchimp")]
)Newsletter.sources(from:listID:selectingWith:processedWith:) lists a Mailchimp list's sent
campaigns and hands the whole list to your select closure. Filtering and issue numbering
happen there, where global ordering is available. Each campaign you return is then fetched and
converted concurrently.
import Contribute
import ContributeMailchimp
import Foundation
import Spinetail
let client = try MailchimpClient(apiKey: apiKey)
let htmlToMarkdown = SwiftSoupMarkdownGenerator().markdown(fromHTML:)
let newsletters = try await Newsletter.sources(
from: client,
listID: listID,
selectingWith: { campaigns in
// `campaigns` is [Spinetail.Campaign] — every sent campaign on the list.
campaigns.enumerated().compactMap { index, campaign in
guard
let campaignID = campaign.id,
let title = campaign.title,
let sendTime = campaign.sendTime,
let archiveURL = campaign.longArchiveURL.flatMap(URL.init(string:))
else { return nil }
return Newsletter.Source.Campaign(
slug: title.slugify(),
issueNo: index + 1,
campaignID: campaignID,
longArchiveURL: archiveURL,
featuredImageURL: campaign.socialCardImageURL.flatMap(URL.init(string:)),
title: title,
subjectLine: campaign.subjectLine ?? title,
previewText: campaign.previewText,
sendTime: sendTime
)
}
},
processedWith: htmlToMarkdown
)
.sorted { $0.issueNo < $1.issueNo }Hand the sources to Contribute's generic writer — the call site is identical to any other Contribute source:
try Newsletter.write(
from: newsletters,
atContentPathURL: URL(fileURLWithPath: "Content/newsletters"),
fileNameWithoutExtension: { "\($0.issueNo)-\($0.slug)" },
using: htmlToMarkdown,
options: MarkdownContentBuilderOptions(
shouldOverwriteExisting: false,
includeMissingPrevious: false
)
)Each file is YAML front matter followed by the newsletter body:
---
issueNo: 42
campaignID: 1a2b3c4d5e
featuredImage: https://mcusercontent.com/…/header.png
longArchiveURL: https://us1.campaign-archive.com/?u=…&id=…
newsletterTitle: Empower Apps Newsletter
title: What's new in Swift 6.4
date: 2026-06-29 12:00
description: A short preview line.
---
The newsletter body, converted from the campaign's archive HTML.Mailchimp's storage layer occasionally serves a permanently cached 503 for a single campaign's
archive content. Newsletter.sources(...) logs that campaign to standard error and skips it
rather than aborting, so one bad campaign never costs you the whole archive.
- Swift 6.4
- macOS 15+, iOS 16+, tvOS 16+, watchOS 9+
- Ubuntu 24.04 (Noble) and other Swift 6.4 Linux distributions; Windows and Android are covered in CI
MIT © BrightDigit