robots.txt Generator — Kotlin source
Build a standards-compliant robots.txt with per-user-agent allow/disallow rules, crawl-delay, and sitemap entries.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// robots-txt-generator — Kotlin 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 kotlin.math.floor
data class RuleGroup(
val userAgents: List<String>, // ["*"] or ["Googlebot", "Bingbot"]
val disallow: List<String> = emptyList(), // "" renders as a bare "Disallow:"
val allow: List<String> = emptyList(),
val crawlDelay: Double? = null, // null = unset; non-finite never emitted
)
data class RobotsConfig(
val groups: List<RuleGroup> = emptyList(),
val sitemaps: List<String> = emptyList(),
)
object 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 fun cleanAgents(list: List<String>): List<String> {
val trimmed = list.map { it.trim() }.filter { it.isNotEmpty() }
return if (trimmed.isEmpty() && list.isNotEmpty()) listOf("*") else trimmed
}
// JS-style number rendering for Crawl-delay: 10.0 -> "10", 1.5 -> "1.5".
private fun numStr(f: Double): String =
if (f == floor(f)) f.toLong().toString() else f.toString()
fun generateRobots(config: RobotsConfig): String {
val out = mutableListOf<String>()
for (g in config.groups) {
val uas = cleanAgents(g.userAgents)
if (uas.isEmpty()) continue
uas.forEach { out += "User-agent: $it" }
for (a in g.allow) {
val p = a.trim()
if (p.isNotEmpty()) out += "Allow: $p"
}
if (g.disallow.isEmpty()) {
out += "Disallow:" // no entries -> the allow-all marker
} else {
g.disallow.forEach { out += "Disallow: $it" } // "" kept verbatim
}
val cd = g.crawlDelay
if (cd != null && cd.isFinite()) out += "Crawl-delay: ${numStr(cd)}"
out += "" // blank line separates groups
}
for (s in config.sitemaps) {
val url = s.trim()
if (url.isNotEmpty()) out += "Sitemap: $url"
}
return out.joinToString("\n").replace(Regex("\n{3,}"), "\n\n").trimEnd() + "\n"
}
private class Acc(val ua: String) {
val disallow = mutableListOf<String>()
val allow = mutableListOf<String>()
var crawlDelay: Double? = null
}
// ParseRobots in the Go twin: strip '#'-comments, split on the FIRST ':'
// (values may contain colons), unknown directives ignored. toDoubleOrNull
// yields null for garbage where JS Number() yields NaN — both are never
// emitted (the TS lib drops non-finite delays).
fun parseRobots(text: String?): RobotsConfig {
val perUA = LinkedHashMap<String, Acc>() // insertion-ordered
val sitemaps = mutableListOf<String>()
var stack = mutableListOf<String>()
var current = listOf<Acc>()
fun flush() { // a rule line applies to every agent of the pending stack
if (stack.isEmpty()) return
current = stack.map { ua -> perUA.getOrPut(ua) { Acc(ua) } }
stack = mutableListOf()
}
for (raw in (text ?: "").split('\n')) {
val line = raw.replace(Regex("#.*$"), "").trim()
if (line.isEmpty()) continue
val colon = line.indexOf(':')
if (colon == -1) continue
val field = line.substring(0, colon).trim().lowercase()
val value = line.substring(colon + 1).trim()
when (field) {
"user-agent" -> {
val ua = value.ifEmpty { "*" }
if (ua !in stack) stack += ua
}
"disallow" -> { flush(); current.forEach { it.disallow += value } }
"allow" -> { flush(); current.forEach { it.allow += value } }
"crawl-delay" -> { flush(); val v = value.toDoubleOrNull(); current.forEach { it.crawlDelay = v } }
"sitemap" -> { flush(); sitemaps += value }
}
}
// Merge agents with identical rule sets into one stacked group.
val groups = mutableListOf<RuleGroup>()
for (acc in perUA.values) {
val twin = groups.find { it.disallow == acc.disallow && it.allow == acc.allow && it.crawlDelay == acc.crawlDelay }
if (twin != null) {
groups[groups.indexOf(twin)] = twin.copy(userAgents = twin.userAgents + acc.ua)
} else {
groups += RuleGroup(listOf(acc.ua), acc.disallow.toList(), acc.allow.toList(), acc.crawlDelay)
}
}
return RobotsConfig(groups, sitemaps)
}
}
// Sample config -> generated body -> parse round-trip check (tool page behavior).
fun main() {
val config = RobotsConfig(
groups = listOf(
RuleGroup(listOf("*"), disallow = listOf("/private/", "/admin/"), allow = listOf("/admin/public/")),
RuleGroup(listOf("GPTBot", "CCBot"), disallow = listOf("/"), crawlDelay = 10.0),
),
sitemaps = listOf("https://example.com/sitemap.xml"),
)
val body = RobotsTxt.generateRobots(config)
print(body)
println("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 →