Skip to content

Instantly share code, notes, and snippets.

@baleboy
Created January 10, 2025 13:23
Show Gist options
  • Select an option

  • Save baleboy/57993c9cc595cd2c80cef98b2af7683d to your computer and use it in GitHub Desktop.

Select an option

Save baleboy/57993c9cc595cd2c80cef98b2af7683d to your computer and use it in GitHub Desktop.
AI generated static site generator
import Foundation
// MARK: - Models and Types
struct Post {
let title: String
let date: Date
let content: String
var htmlFileName: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = dateFormatter.string(from: date)
// Create URL-friendly filename
let safeTitleText = title.lowercased()
.replacingOccurrences(of: " ", with: "-")
.components(separatedBy: CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")))
.joined()
return "\(dateString)-\(safeTitleText).html"
}
}
class StaticSiteGenerator {
private let inputDirectory: URL
private let outputDirectory: URL
private let dateFormatter: DateFormatter
init(inputDirectory: URL, outputDirectory: URL) {
self.inputDirectory = inputDirectory
self.outputDirectory = outputDirectory
self.dateFormatter = DateFormatter()
self.dateFormatter.dateFormat = "yyyy-MM-dd"
}
// MARK: - Main Build Process
func build() throws {
// Create output directory if it doesn't exist
try FileManager.default.createDirectory(at: outputDirectory,
withIntermediateDirectories: true)
// Get all markdown files
let files = try FileManager.default.contentsOfDirectory(at: inputDirectory,
includingPropertiesForKeys: nil)
.filter { $0.pathExtension == "md" }
// Process each file and collect posts
var posts: [Post] = []
for file in files {
if let post = try processMarkdownFile(at: file) {
posts.append(post)
try generatePostHTML(post)
}
}
// Generate index page
try generateIndexHTML(with: posts)
// Copy assets if they exist
try copyAssets()
}
// MARK: - Markdown Processing
private func processMarkdownFile(at url: URL) throws -> Post? {
let content = try String(contentsOf: url)
// Parse frontmatter
guard let (frontmatter, markdown) = parseFrontmatter(from: content) else {
print("Warning: No valid frontmatter found in \(url.lastPathComponent)")
return nil
}
// Extract required metadata
guard let title = frontmatter["title"] as? String,
let dateString = frontmatter["date"] as? String,
let date = dateFormatter.date(from: dateString.replacingOccurrences(of: "\"", with: "")) else {
print("Warning: Missing required frontmatter in \(url.lastPathComponent)")
return nil
}
return Post(title: title, date: date, content: markdown)
}
private func parseFrontmatter(from content: String) -> ([String: Any], String)? {
let components = content.components(separatedBy: "+++")
guard components.count >= 3 else { return nil }
let frontmatterString = components[1].trimmingCharacters(in: .whitespacesAndNewlines)
let markdown = components[2].trimmingCharacters(in: .whitespacesAndNewlines)
// Parse TOML-style frontmatter
var frontmatter: [String: Any] = [:]
let lines = frontmatterString.components(separatedBy: .newlines)
for line in lines {
let parts = line.components(separatedBy: "=").map { $0.trimmingCharacters(in: .whitespaces) }
if parts.count == 2 {
frontmatter[parts[0]] = parts[1]
}
}
return (frontmatter, markdown)
}
// MARK: - HTML Generation
private func generatePostHTML(_ post: Post) throws {
let html = generateHTML(for: post)
let outputFile = outputDirectory.appendingPathComponent(post.htmlFileName)
try html.write(to: outputFile, atomically: true, encoding: .utf8)
}
private func generateHTML(for post: Post) -> String {
// Convert markdown to HTML (basic implementation - you might want to use a proper markdown parser)
let content = convertMarkdownToHTML(post.content)
return """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>\(post.title)</title>
\(generateCSS())
</head>
<body>
<article>
<h1>\(post.title)</h1>
<time datetime="\(dateFormatter.string(from: post.date))">
\(dateFormatter.string(from: post.date))
</time>
<div class="content">
\(content)
</div>
</article>
</body>
</html>
"""
}
private func generateIndexHTML(with posts: [Post]) throws {
let sortedPosts = posts.sorted { $0.date > $1.date }
var postsHTML = ""
for post in sortedPosts {
postsHTML += """
<div class="post-preview">
<h2><a href="\(post.htmlFileName)">\(post.title)</a></h2>
<time datetime="\(dateFormatter.string(from: post.date))">
\(dateFormatter.string(from: post.date))
</time>
</div>
"""
}
let indexHTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Blog</title>
\(generateCSS())
</head>
<body>
<h1>My Blog</h1>
<div class="posts">
\(postsHTML)
</div>
</body>
</html>
"""
let indexFile = outputDirectory.appendingPathComponent("index.html")
try indexHTML.write(to: indexFile, atomically: true, encoding: .utf8)
}
// MARK: - Utilities
private func convertMarkdownToHTML(_ markdown: String) -> String {
// This is a very basic implementation
// You might want to use a proper markdown parser library
var html = markdown
// Convert headers
html = html.replacingOccurrences(of: #"^##\s+(.+)$"#, with: "<h2>$1</h2>", options: .regularExpression, range: nil)
html = html.replacingOccurrences(of: #"^#\s+(.+)$"#, with: "<h1>$1</h1>", options: .regularExpression, range: nil)
// Convert code blocks
html = html.replacingOccurrences(of: "```swift\n([^`]+)\n```", with: "<pre><code class=\"swift\">$1</code></pre>", options: .regularExpression, range: nil)
// Convert links
html = html.replacingOccurrences(of: #"\[([^\]]+)\]\(([^\)]+)\)"#, with: "<a href=\"$2\">$1</a>", options: .regularExpression, range: nil)
// Convert images
html = html.replacingOccurrences(of: #"!\[([^\]]*)\]\(([^\)]+)\)"#, with: "<img src=\"$2\" alt=\"$1\">", options: .regularExpression, range: nil)
return html
}
private func generateCSS() -> String {
return """
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
color: #333;
}
pre {
background: #f6f8fa;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 1rem 0;
}
h1, h2, h3 {
color: #2c3e50;
}
time {
color: #666;
display: block;
margin-bottom: 2rem;
}
.post-preview {
margin-bottom: 2rem;
}
.post-preview h2 {
margin-bottom: 0.5rem;
}
.post-preview a {
color: #2c3e50;
text-decoration: none;
}
.post-preview a:hover {
text-decoration: underline;
}
</style>
"""
}
private func copyAssets() throws {
let assetsDirectory = inputDirectory.appendingPathComponent("assets")
let outputAssetsDirectory = outputDirectory.appendingPathComponent("assets")
if FileManager.default.fileExists(atPath: assetsDirectory.path) {
try FileManager.default.copyItem(at: assetsDirectory, to: outputAssetsDirectory)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment