robots.txt Generator — Swift source
Build a standards-compliant robots.txt with per-user-agent allow/disallow rules, crawl-delay, and sitemap entries.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// robots-txt-generator — Swift port: standards-compliant robots.txt generator + parser.
//
// Display port of the CosmoDev Robots.txt Generator tool — same contract as
// cli/robots-txt-generator/robots-txt-generator.go (the live Go twin) and
// src/lib/robotsTxt.ts (canonical TypeScript). Pure + deterministic; stdlib
// only. A group carries a LIST of user-agents (the REP spec's stacked
// User-agent lines); parsing aggregates repeated blocks per agent, then
// re-merges agents whose accumulated rules are identical into one stacked
// group, exactly like the TS lib.
import Foundation
public struct RuleGroup: Equatable {
/// `["*"]` or `["Googlebot", "Bingbot"]` — one REP group.
public var userAgents: [String]
/// Paths to disallow; `""` renders as a bare `Disallow:`.
public var disallow: [String]
/// Paths to allow.
public var allow: [String]
/// Seconds between requests; `nil` = unset, non-finite never emitted.
public var crawlDelay: Double?
public init(userAgents: [String], disallow: [String] = [], allow: [String] = [], crawlDelay: Double? = nil) {
self.userAgents = userAgents
self.disallow = disallow
self.allow = allow
self.crawlDelay = crawlDelay
}
}
public struct RobotsConfig: Equatable {
/// Ordered rule groups.
public var groups: [RuleGroup]
/// Sitemap URLs, emitted after the groups.
public var sitemaps: [String]
public init(groups: [RuleGroup] = [], sitemaps: [String] = []) {
self.groups = groups
self.sitemaps = sitemaps
}
}
public enum RobotsTxt {
// cleanAgents in the TS lib: trim + drop blanks. A non-empty list that
// trims away entirely degrades to ["*"]; an empty list means "no group yet".
private static func cleanAgents(_ list: [String]) -> [String] {
let trimmed = list.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }
return trimmed.isEmpty && !list.isEmpty ? ["*"] : trimmed
}
// JS-style number rendering for Crawl-delay: 10.0 -> "10", 1.5 -> "1.5".
private static func numStr(_ f: Double) -> String {
f == f.rounded() ? String(Int64(f)) : String(f)
}
public static func generateRobots(_ config: RobotsConfig) -> String {
var out: [String] = []
for g in config.groups {
let uas = cleanAgents(g.userAgents)
if uas.isEmpty { continue }
uas.forEach { out.append("User-agent: \($0)") }
for a in g.allow {
let p = a.trimmingCharacters(in: .whitespacesAndNewlines)
if !p.isEmpty { out.append("Allow: \(p)") }
}
if g.disallow.isEmpty {
out.append("Disallow:") // no entries -> the allow-all marker
} else {
g.disallow.forEach { out.append("Disallow: \($0)") } // "" kept verbatim
}
if let cd = g.crawlDelay, cd.isFinite {
out.append("Crawl-delay: \(numStr(cd))")
}
out.append("") // blank line separates groups
}
for s in config.sitemaps {
let url = s.trimmingCharacters(in: .whitespacesAndNewlines)
if !url.isEmpty { out.append("Sitemap: \(url)") }
}
let joined = out.joined(separator: "\n")
return joined.replacingOccurrences(of: #"\n{3,}"#, with: "\n\n", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines) + "\n"
}
private final class Acc {
let ua: String
var disallow: [String] = []
var allow: [String] = []
var crawlDelay: Double?
init(_ ua: String) { self.ua = ua }
}
// ParseRobots in the Go twin: strip '#'-comments, split on the FIRST ':'
// (values may contain colons), unknown directives ignored. Double(value)
// yields nil for garbage where JS Number() yields NaN — both are never
// emitted (the TS lib drops non-finite delays).
public static func parseRobots(_ text: String?) -> RobotsConfig {
var perUA: [(ua: String, acc: Acc)] = [] // insertion-ordered
var sitemaps: [String] = []
var stack: [String] = []
var current: [Acc] = []
func flush() { // a rule line applies to every agent of the pending stack
guard !stack.isEmpty else { return }
current = stack.map { ua in
if let hit = perUA.first(where: { $0.ua == ua }) { return hit.acc }
let acc = Acc(ua)
perUA.append((ua, acc))
return acc
}
stack = []
}
for raw in (text ?? "").split(separator: "\n", omittingEmptySubsequences: false).map(String.init) {
let stripped = raw.replacingOccurrences(of: #"[#].*$"#, with: "", options: .regularExpression)
let line = stripped.trimmingCharacters(in: .whitespaces)
if line.isEmpty { continue }
guard let colon = line.firstIndex(of: ":") else { continue }
let field = line[..<colon].trimmingCharacters(in: .whitespaces).lowercased()
let value = String(line[line.index(after: colon)...]).trimmingCharacters(in: .whitespaces)
switch field {
case "user-agent":
let ua = value.isEmpty ? "*" : value
if !stack.contains(ua) { stack.append(ua) }
case "disallow": flush(); current.forEach { $0.disallow.append(value) }
case "allow": flush(); current.forEach { $0.allow.append(value) }
case "crawl-delay": flush(); current.forEach { $0.crawlDelay = Double(value) }
case "sitemap": flush(); sitemaps.append(value)
default: break
}
}
// Merge agents with identical rule sets into one stacked group.
var groups: [RuleGroup] = []
for entry in perUA {
let acc = entry.acc
if let i = groups.firstIndex(where: {
$0.disallow == acc.disallow && $0.allow == acc.allow && $0.crawlDelay == acc.crawlDelay
}) {
groups[i].userAgents.append(entry.ua)
} else {
groups.append(RuleGroup(userAgents: [entry.ua], disallow: acc.disallow, allow: acc.allow, crawlDelay: acc.crawlDelay))
}
}
return RobotsConfig(groups: groups, sitemaps: sitemaps)
}
}
// Sample config -> generated body -> parse round-trip check (tool page behavior).
let config = RobotsConfig(
groups: [
RuleGroup(userAgents: ["*"], disallow: ["/private/", "/admin/"], allow: ["/admin/public/"]),
RuleGroup(userAgents: ["GPTBot", "CCBot"], disallow: ["/"], crawlDelay: 10),
],
sitemaps: ["https://example.com/sitemap.xml"]
)
let body = RobotsTxt.generateRobots(config)
print(body)
print("round-trip stable: \(RobotsTxt.generateRobots(RobotsTxt.parseRobots(body)) == body)")
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →