sova_meta/
robots_plugin.rs1use crate::defaults::MetaDefaults;
4use crate::robots::{render_robots, RobotsConfig, RobotsDirective, RobotsGroup};
5use crate::sitemap_config::SitemapConfig;
6use sova_core::extend::RouteTable;
7use sova_core::{App, Plugin, Request, Response};
8
9pub struct Robots {
11 cfg: RobotsConfig,
12}
13
14impl Robots {
15 pub fn new() -> Self {
16 Self {
17 cfg: RobotsConfig::default(),
18 }
19 }
20
21 pub fn path(mut self, p: impl Into<String>) -> Self {
22 self.cfg.path = p.into();
23 self
24 }
25
26 pub fn block_all(mut self) -> Self {
27 self.cfg.block_all = true;
28 self
29 }
30
31 pub fn allow(mut self, path: impl Into<String>) -> Self {
32 ensure_star_group(&mut self.cfg)
33 .directives
34 .push(RobotsDirective::Allow(path.into()));
35 self
36 }
37
38 pub fn disallow(mut self, path: impl Into<String>) -> Self {
39 ensure_star_group(&mut self.cfg)
40 .directives
41 .push(RobotsDirective::Disallow(path.into()));
42 self
43 }
44
45 pub fn user_agent<F>(mut self, name: impl Into<String>, f: F) -> Self
46 where
47 F: FnOnce(RobotsUaBuilder) -> RobotsUaBuilder,
48 {
49 let built = f(RobotsUaBuilder {
50 group: RobotsGroup {
51 user_agent: name.into(),
52 directives: Vec::new(),
53 },
54 });
55 self.cfg.groups.push(built.group);
56 self
57 }
58
59 pub fn sitemap(mut self, url: impl Into<String>) -> Self {
60 self.cfg.sitemap_url = Some(url.into());
61 self.cfg.sitemap_from_plugin = false;
62 self
63 }
64
65 pub fn sitemap_from_plugin(mut self, yes: bool) -> Self {
66 self.cfg.sitemap_from_plugin = yes;
67 if !yes {
68 self.cfg.sitemap_url = None;
69 }
70 self
71 }
72
73 pub fn from_noindex(mut self, yes: bool) -> Self {
74 self.cfg.from_noindex = yes;
75 self
76 }
77
78 pub fn crawl_delay(mut self, seconds: f32) -> Self {
79 self.cfg.crawl_delay = Some(seconds);
80 self
81 }
82
83 pub fn raw(mut self, line: impl Into<String>) -> Self {
84 self.cfg.trailing_raw.push(line.into());
85 self
86 }
87}
88
89impl Default for Robots {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95pub struct RobotsUaBuilder {
97 group: RobotsGroup,
98}
99
100impl RobotsUaBuilder {
101 pub fn allow(mut self, path: impl Into<String>) -> Self {
102 self.group
103 .directives
104 .push(RobotsDirective::Allow(path.into()));
105 self
106 }
107
108 pub fn disallow(mut self, path: impl Into<String>) -> Self {
109 self.group
110 .directives
111 .push(RobotsDirective::Disallow(path.into()));
112 self
113 }
114
115 pub fn crawl_delay(mut self, seconds: f32) -> Self {
116 self.group
117 .directives
118 .push(RobotsDirective::CrawlDelay(seconds));
119 self
120 }
121
122 pub fn raw(mut self, line: impl Into<String>) -> Self {
123 self.group
124 .directives
125 .push(RobotsDirective::Raw(line.into()));
126 self
127 }
128}
129
130fn ensure_star_group(cfg: &mut RobotsConfig) -> &mut RobotsGroup {
131 if let Some(i) = cfg.groups.iter().position(|g| g.user_agent == "*") {
132 &mut cfg.groups[i]
133 } else {
134 cfg.groups.insert(
135 0,
136 RobotsGroup {
137 user_agent: "*".into(),
138 directives: Vec::new(),
139 },
140 );
141 &mut cfg.groups[0]
142 }
143}
144
145impl Plugin for Robots {
146 fn id(&self) -> &'static str {
147 "robots"
148 }
149
150 fn meta(&self) -> sova_core::PluginMeta {
151 sova_core::PluginMeta::new("Robots")
152 .description("Serve robots.txt with allow/disallow and Sitemap line")
153 .version(env!("CARGO_PKG_VERSION"))
154 }
155
156 fn install(mut self, app: &mut App) {
157 if let Some(doc) = app.config_doc() {
158 if let Some(section) = doc.section("robots") {
159 if section
160 .get("block_all")
161 .and_then(|v| v.as_bool())
162 .unwrap_or(false)
163 || section.get("block_all").and_then(|v| v.as_str()) == Some("true")
164 {
165 self.cfg.block_all = true;
166 }
167 if let Some(v) = section.get("path").and_then(|v| v.as_str()) {
168 self.cfg.path = v.to_string();
169 }
170 if let Some(v) = section.get("sitemap").and_then(|v| v.as_str()) {
171 self.cfg.sitemap_url = Some(v.to_string());
172 self.cfg.sitemap_from_plugin = false;
173 }
174 if let Some(v) = section.get("from_noindex").and_then(|v| v.as_bool()) {
175 self.cfg.from_noindex = v;
176 }
177 if let Some(arr) = section.get("allow").and_then(|v| v.as_array()) {
178 for item in arr {
179 if let Some(s) = item.as_str() {
180 ensure_star_group(&mut self.cfg)
181 .directives
182 .push(RobotsDirective::Allow(s.to_string()));
183 }
184 }
185 }
186 if let Some(arr) = section.get("disallow").and_then(|v| v.as_array()) {
187 for item in arr {
188 if let Some(s) = item.as_str() {
189 ensure_star_group(&mut self.cfg)
190 .directives
191 .push(RobotsDirective::Disallow(s.to_string()));
192 }
193 }
194 }
195 }
196 if let Some(section) = doc.section("meta") {
198 if section.get("robots").and_then(|v| v.as_str()) == Some("block-all") {
199 self.cfg.block_all = true;
200 }
201 }
202 }
203
204 let mount = self.cfg.path.clone();
205 app.state(self.cfg.clone());
206
207 app.get(mount.as_str(), |req: Request| async move {
208 let cfg = req
209 .try_state::<RobotsConfig>()
210 .map(|c| (*c).clone())
211 .unwrap_or_default();
212 let table = req.try_state::<RouteTable>();
213 let sitemap = req.try_state::<SitemapConfig>();
214 let meta = req.try_state::<MetaDefaults>();
215 let body = render_robots(
216 &cfg,
217 table.as_deref(),
218 sitemap.as_deref(),
219 meta.as_deref(),
220 );
221 Response::text(body).header("content-type", "text/plain; charset=utf-8")
222 });
223 }
224}