typstify_generator/
robots.rs1use std::{fs::File, io::Write, path::Path};
6
7use thiserror::Error;
8use tracing::info;
9use typstify_core::Config;
10
11#[derive(Debug, Error)]
13pub enum RobotsError {
14 #[error("IO error: {0}")]
16 Io(#[from] std::io::Error),
17}
18
19pub type Result<T> = std::result::Result<T, RobotsError>;
21
22#[derive(Debug)]
24pub struct RobotsGenerator<'a> {
25 config: &'a Config,
26}
27
28impl<'a> RobotsGenerator<'a> {
29 #[must_use]
31 pub fn new(config: &'a Config) -> Self {
32 Self { config }
33 }
34
35 pub fn generate(&self, output_dir: &Path) -> Result<()> {
37 if !self.config.robots.enabled {
38 return Ok(());
39 }
40
41 info!("generating robots.txt");
42
43 let path = output_dir.join("robots.txt");
44 let mut file = File::create(path)?;
45
46 writeln!(file, "User-agent: *")?;
47
48 for path in &self.config.robots.disallow {
49 writeln!(file, "Disallow: {path}")?;
50 }
51
52 for path in &self.config.robots.allow {
53 writeln!(file, "Allow: {path}")?;
54 }
55
56 let sitemap_url = format!("{}/sitemap.xml", self.config.base_url());
58 writeln!(file, "Sitemap: {sitemap_url}")?;
59
60 Ok(())
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use std::{collections::HashMap, fs};
67
68 use typstify_core::test_fixtures::test_config;
69
70 use super::*;
71
72 #[test]
73 fn test_disabled_produces_no_file() {
74 let mut config = test_config();
75 config.robots.enabled = false;
76
77 let dir = tempfile::tempdir().expect("create temp dir");
78 let generator = RobotsGenerator::new(&config);
79 generator.generate(dir.path()).unwrap();
80
81 assert!(!dir.path().join("robots.txt").exists());
82 }
83
84 #[test]
85 fn test_enabled_with_disallow_allow_paths() {
86 let mut config = test_config();
87 config.robots.disallow = vec!["/private/".to_string(), "/admin/".to_string()];
88 config.robots.allow = vec!["/public/".to_string()];
89
90 let dir = tempfile::tempdir().expect("create temp dir");
91 let generator = RobotsGenerator::new(&config);
92 generator.generate(dir.path()).unwrap();
93
94 let content = fs::read_to_string(dir.path().join("robots.txt")).unwrap();
95
96 assert!(content.starts_with("User-agent: *\n"));
97 assert!(content.contains("Disallow: /private/\n"));
98 assert!(content.contains("Disallow: /admin/\n"));
99 assert!(content.contains("Allow: /public/\n"));
100 assert!(content.contains("Sitemap: https://example.com/sitemap.xml\n"));
101 }
102
103 #[test]
104 fn test_sitemap_includes_base_path() {
105 let config = Config::from_parts(
106 typstify_core::config::SiteConfig {
107 title: "Test Site".to_string(),
108 host: "https://example.com".to_string(),
109 base_path: "/typstify".to_string(),
110 default_language: "en".to_string(),
111 description: None,
112 author: None,
113 },
114 typstify_core::config::BuildConfig::default(),
115 typstify_core::config::SearchConfig::default(),
116 typstify_core::config::RssConfig::default(),
117 typstify_core::config::RobotsConfig::default(),
118 typstify_core::config::TaxonomyConfig::default(),
119 HashMap::new(),
120 );
121
122 let dir = tempfile::tempdir().expect("create temp dir");
123 let generator = RobotsGenerator::new(&config);
124 generator.generate(dir.path()).unwrap();
125
126 let content = fs::read_to_string(dir.path().join("robots.txt")).unwrap();
127
128 assert!(content.contains("Sitemap: https://example.com/typstify/sitemap.xml\n"));
129 }
130}