Skip to main content

sova_meta/
plugin.rs

1//! Plugin builder and install (head / SEO only).
2
3use crate::check::register_meta_check;
4use crate::defaults::{MetaDefaults, TrailingSlash};
5use crate::headers::install_headers_middleware;
6use crate::page::MetaPage;
7use crate::slash::install_slash_middleware;
8use sova_core::{App, Plugin};
9
10/// Outbound document-meta plugin.
11pub struct Meta {
12    defaults: MetaDefaults,
13}
14
15impl Meta {
16    pub fn new() -> Self {
17        Self {
18            defaults: MetaDefaults::default(),
19        }
20    }
21
22    pub fn site_name(mut self, name: impl Into<String>) -> Self {
23        self.defaults.site_name = Some(name.into());
24        self
25    }
26
27    pub fn title_template(mut self, t: impl Into<String>) -> Self {
28        self.defaults.title_template = Some(t.into());
29        self
30    }
31
32    pub fn default_image(mut self, img: impl Into<String>) -> Self {
33        self.defaults.default_image = Some(img.into());
34        self
35    }
36
37    pub fn twitter_site(mut self, s: impl Into<String>) -> Self {
38        self.defaults.twitter_site = Some(s.into());
39        self
40    }
41
42    pub fn public_url(mut self, u: impl Into<String>) -> Self {
43        self.defaults.public_url = Some(u.into());
44        self
45    }
46
47    pub fn trailing_slash(mut self, p: TrailingSlash) -> Self {
48        self.defaults.trailing_slash = p;
49        self
50    }
51
52    /// Soft `check`: missing title/description warn via tracing instead of failing.
53    pub fn soft_check(mut self) -> Self {
54        self.defaults.check_strict = false;
55        self
56    }
57
58    pub fn page() -> MetaPage {
59        MetaPage::new()
60    }
61
62    pub fn noindex() -> MetaPage {
63        MetaPage::new().noindex()
64    }
65}
66
67impl Default for Meta {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl Plugin for Meta {
74    fn id(&self) -> &'static str {
75        "meta"
76    }
77
78    fn meta(&self) -> sova_core::PluginMeta {
79        sova_core::PluginMeta::new("Meta")
80            .description("Document meta, OG/Twitter, JSON-LD, and head inject")
81            .version(env!("CARGO_PKG_VERSION"))
82    }
83
84    fn install(mut self, app: &mut App) {
85        // Unset-fill from `[meta]` — explicit builder values win.
86        if let Some(doc) = app.config_doc() {
87            if let Some(section) = doc.section("meta") {
88                if section
89                    .get("robots")
90                    .and_then(|v| v.as_str())
91                    == Some("block-all")
92                {
93                    self.defaults.robots_block_all = true;
94                }
95                if self.defaults.public_url.is_none() {
96                    if let Some(v) = section.get("public_url").and_then(|v| v.as_str()) {
97                        self.defaults.public_url = Some(v.to_string());
98                    }
99                }
100                if self.defaults.site_name.is_none() {
101                    if let Some(v) = section.get("site_name").and_then(|v| v.as_str()) {
102                        self.defaults.site_name = Some(v.to_string());
103                    }
104                }
105                if self.defaults.title_template.is_none() {
106                    if let Some(v) = section.get("title_template").and_then(|v| v.as_str()) {
107                        self.defaults.title_template = Some(v.to_string());
108                    }
109                }
110                if self.defaults.default_image.is_none() {
111                    if let Some(v) = section.get("default_image").and_then(|v| v.as_str()) {
112                        self.defaults.default_image = Some(v.to_string());
113                    }
114                }
115                if section
116                    .get("check")
117                    .and_then(|v| v.as_str())
118                    == Some("soft")
119                {
120                    self.defaults.check_strict = false;
121                }
122            }
123        }
124
125        let defaults = self.defaults.clone();
126        app.state(defaults);
127
128        install_slash_middleware(app);
129        install_headers_middleware(app);
130
131        #[cfg(feature = "templates")]
132        {
133            use crate::html::render_html;
134            use crate::resolve_meta;
135            use minijinja::Value;
136            use sova_core::Request;
137            sova_templates::register_per_request(app, "meta", |req: &Request| {
138                let html = render_html(&resolve_meta(req));
139                Value::from_function(move || html.clone())
140            });
141        }
142
143        register_meta_check(app);
144    }
145}