Skip to main content

sova_meta/
html.rs

1//! Render HTML `<head>` fragment from [`ResolvedMeta`].
2
3use crate::resolve::ResolvedMeta;
4use serde_json::Value;
5
6pub fn render_html(meta: &ResolvedMeta) -> String {
7    let mut out = String::new();
8    if let Some(ref t) = meta.title {
9        out.push_str(&format!("<title>{}</title>\n", escape(t)));
10    }
11    if let Some(ref d) = meta.description {
12        out.push_str(&format!(
13            "<meta name=\"description\" content=\"{}\">\n",
14            escape(d)
15        ));
16    }
17    let robots = if meta.noindex {
18        "noindex, nofollow"
19    } else {
20        "index, follow"
21    };
22    out.push_str(&format!(
23        "<meta name=\"robots\" content=\"{robots}\">\n"
24    ));
25    if let Some(ref c) = meta.canonical {
26        out.push_str(&format!(
27            "<link rel=\"canonical\" href=\"{}\">\n",
28            escape(c)
29        ));
30    }
31    for (lang, href) in &meta.hreflang {
32        out.push_str(&format!(
33            "<link rel=\"alternate\" hreflang=\"{}\" href=\"{}\">\n",
34            escape(lang),
35            escape(href)
36        ));
37    }
38
39    // Open Graph
40    if let Some(ref t) = meta.title {
41        out.push_str(&format!(
42            "<meta property=\"og:title\" content=\"{}\">\n",
43            escape(t)
44        ));
45    }
46    if let Some(ref d) = meta.description {
47        out.push_str(&format!(
48            "<meta property=\"og:description\" content=\"{}\">\n",
49            escape(d)
50        ));
51    }
52    out.push_str(&format!(
53        "<meta property=\"og:type\" content=\"{}\">\n",
54        escape(&meta.og_type)
55    ));
56    if let Some(ref c) = meta.canonical {
57        out.push_str(&format!(
58            "<meta property=\"og:url\" content=\"{}\">\n",
59            escape(c)
60        ));
61    }
62    if let Some(ref img) = meta.image {
63        out.push_str(&format!(
64            "<meta property=\"og:image\" content=\"{}\">\n",
65            escape(img)
66        ));
67    }
68    if let Some(ref published) = meta.published {
69        out.push_str(&format!(
70            "<meta property=\"article:published_time\" content=\"{}\">\n",
71            published.to_rfc3339()
72        ));
73    }
74    if let Some(ref sn) = meta.site_name {
75        out.push_str(&format!(
76            "<meta property=\"og:site_name\" content=\"{}\">\n",
77            escape(sn)
78        ));
79    }
80    if let Some(ref loc) = meta.og_locale {
81        out.push_str(&format!(
82            "<meta property=\"og:locale\" content=\"{}\">\n",
83            escape(loc)
84        ));
85    }
86    for alt in &meta.og_locale_alternate {
87        out.push_str(&format!(
88            "<meta property=\"og:locale:alternate\" content=\"{}\">\n",
89            escape(alt)
90        ));
91    }
92
93    // Twitter
94    let card = if meta.image.is_some() {
95        "summary_large_image"
96    } else {
97        "summary"
98    };
99    out.push_str(&format!(
100        "<meta name=\"twitter:card\" content=\"{card}\">\n"
101    ));
102    if let Some(ref site) = meta.twitter_site {
103        out.push_str(&format!(
104            "<meta name=\"twitter:site\" content=\"{}\">\n",
105            escape(site)
106        ));
107    }
108    if let Some(ref t) = meta.title {
109        out.push_str(&format!(
110            "<meta name=\"twitter:title\" content=\"{}\">\n",
111            escape(t)
112        ));
113    }
114    if let Some(ref d) = meta.description {
115        out.push_str(&format!(
116            "<meta name=\"twitter:description\" content=\"{}\">\n",
117            escape(d)
118        ));
119    }
120    if let Some(ref img) = meta.image {
121        out.push_str(&format!(
122            "<meta name=\"twitter:image\" content=\"{}\">\n",
123            escape(img)
124        ));
125    }
126
127    for block in &meta.jsonld {
128        let with_ctx = ensure_context(block.clone());
129        if let Ok(s) = serde_json::to_string(&with_ctx) {
130            out.push_str("<script type=\"application/ld+json\">");
131            out.push_str(&s);
132            out.push_str("</script>\n");
133        }
134    }
135
136    out
137}
138
139fn ensure_context(mut v: Value) -> Value {
140    if let Value::Object(ref mut map) = v {
141        map.entry("@context".to_string())
142            .or_insert_with(|| Value::String("https://schema.org".into()));
143    }
144    v
145}
146
147fn escape(s: &str) -> String {
148    s.replace('&', "&amp;")
149        .replace('<', "&lt;")
150        .replace('>', "&gt;")
151        .replace('"', "&quot;")
152}