use super::options::{LeptosOptions, PKG_SUBDIR};
pub(super) struct PrefixContext<'a> {
pub opts: &'a LeptosOptions,
pub initial_theme: Option<&'a str>,
pub request_path: &'a str,
}
pub(super) fn build_html_prefix(ctx: &PrefixContext<'_>) -> String {
let seo = &ctx.opts.seo_defaults;
let theme_class = ctx.initial_theme.unwrap_or("light");
let lang = seo.locale.as_deref().unwrap_or("en");
let mut head_extras = String::new();
if let Some(desc) = &seo.description {
head_extras.push_str(&format!(
"<meta name=\"description\" content=\"{}\">",
html_escape(desc)
));
}
if let Some(robots) = &seo.robots {
head_extras.push_str(&format!("<meta name=\"robots\" content=\"{}\">", html_escape(robots)));
}
if let Some(base) = &seo.canonical_base {
let trimmed = base.trim_end_matches('/');
head_extras.push_str(&format!(
"<link rel=\"canonical\" href=\"{}{}\">",
html_escape(trimmed),
html_escape(ctx.request_path)
));
}
if let Some(handle) = &seo.twitter_handle {
head_extras.push_str(&format!(
"<meta name=\"twitter:site\" content=\"{}\">",
html_escape(handle)
));
}
head_extras.push_str("<meta name=\"twitter:card\" content=\"summary_large_image\">");
if let Some(img) = &seo.og_image {
head_extras.push_str(&format!(
"<meta property=\"og:image\" content=\"{}\">",
html_escape(img)
));
head_extras.push_str(&format!(
"<meta name=\"twitter:image\" content=\"{}\">",
html_escape(img)
));
}
if let Some(logo) = &seo.og_logo {
head_extras.push_str(&format!(
"<meta property=\"og:logo\" content=\"{}\">",
html_escape(logo)
));
}
if let Some(default_title) = &seo.default_title {
head_extras.push_str(&format!(
"<meta property=\"og:title\" content=\"{}\">",
html_escape(default_title)
));
}
if let Some(desc) = &seo.description {
head_extras.push_str(&format!(
"<meta property=\"og:description\" content=\"{}\">",
html_escape(desc)
));
}
head_extras.push_str("<meta property=\"og:type\" content=\"website\">");
if let Some(base) = &seo.canonical_base {
let trimmed = base.trim_end_matches('/');
head_extras.push_str(&format!(
"<meta property=\"og:url\" content=\"{}{}\">",
html_escape(trimmed),
html_escape(ctx.request_path)
));
}
if let Some(site_name) = seo.site_name.as_ref().or(seo.default_title.as_ref()) {
head_extras.push_str(&format!(
"<meta property=\"og:site_name\" content=\"{}\">",
html_escape(site_name)
));
}
if let Some(locale) = &seo.locale {
head_extras.push_str(&format!(
"<meta property=\"og:locale\" content=\"{}\">",
html_escape(&og_locale(locale))
));
}
let asset_links = build_asset_links(ctx.opts);
format!(
"<!DOCTYPE html><html lang=\"{lang}\" class=\"{theme_class}\"><head>\
<meta charset=\"UTF-8\">\
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\
{head_extras}{asset_links}<!--HEAD--></head><body>"
)
}
pub(super) fn build_html_suffix(opts: &LeptosOptions) -> String {
let mut buf = String::new();
if opts.include_hydration_script && !opts.output_name.is_empty() {
buf.push_str(&format!(
"<script type=\"module\">import init,{{hydrate}} from \"/{pkg}/{out}.js\";\
init({{module_or_path:\"/{pkg}/{out}_bg.wasm\"}}).then(()=>hydrate());</script>",
pkg = PKG_SUBDIR,
out = opts.output_name,
));
}
buf.push_str("</body></html>");
buf
}
fn build_asset_links(opts: &LeptosOptions) -> String {
if opts.output_name.is_empty() {
return String::new();
}
let pkg = PKG_SUBDIR;
let out = &opts.output_name;
let css = format!("<link rel=\"stylesheet\" href=\"/{pkg}/{out}.css\">");
if opts.include_hydration_script {
format!(
"{css}\
<link rel=\"modulepreload\" href=\"/{pkg}/{out}.js\">\
<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" crossorigin href=\"/{pkg}/{out}_bg.wasm\">",
)
} else {
css
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn og_locale(lang: &str) -> String {
let trimmed = lang.trim();
if let Some((primary, region)) = trimmed.split_once('-').or_else(|| trimmed.split_once('_')) {
return format!("{}_{}", primary.to_ascii_lowercase(), region.to_ascii_uppercase());
}
let lower = trimmed.to_ascii_lowercase();
let region = match lower.as_str() {
"en" => "US",
"ru" => "RU",
"de" => "DE",
"fr" => "FR",
"es" => "ES",
"it" => "IT",
"pt" => "PT",
"ja" => "JP",
"ko" => "KR",
"zh" => "CN",
"uk" => "UA",
"pl" => "PL",
"nl" => "NL",
"sv" => "SE",
"tr" => "TR",
_ => return lower,
};
format!("{lower}_{region}")
}
#[cfg(test)]
mod tests {
use super::og_locale;
#[test]
fn og_locale_expands_known_primary() {
assert_eq!(og_locale("ru"), "ru_RU");
assert_eq!(og_locale("en"), "en_US");
assert_eq!(og_locale("pt"), "pt_PT");
}
#[test]
fn og_locale_preserves_explicit_region() {
assert_eq!(og_locale("pt-BR"), "pt_BR");
assert_eq!(og_locale("en_GB"), "en_GB");
assert_eq!(og_locale("ZH-tw"), "zh_TW");
}
#[test]
fn og_locale_passes_unknown_through() {
assert_eq!(og_locale("xx"), "xx");
}
}