#![allow(clippy::unwrap_used, clippy::expect_used)]
use anyhow::{Context, Result};
use http_handle::Server;
use ssg::{
cmd::SsgConfig,
content::validate_with_schema,
execute_build_pipeline,
plugin::{PluginContext, PluginManager},
Paths,
};
use std::{fs, path::PathBuf, time::Instant};
fn main() -> Result<()> {
let base_dir = PathBuf::from("examples").join("docs");
let content_dir = base_dir.join("content");
let output_dir = base_dir.join("build");
let template_dir = PathBuf::from("examples").join("templates");
let site_dir = base_dir.join("public");
fs::create_dir_all(&content_dir)?;
fs::create_dir_all(&output_dir)?;
fs::create_dir_all(&template_dir)?;
fs::create_dir_all(&site_dir)?;
let content_dir = fs::canonicalize(&content_dir)?;
let output_dir = fs::canonicalize(&output_dir)?;
let template_dir = fs::canonicalize(&template_dir)?;
let site_dir = fs::canonicalize(&site_dir)?;
let schema_path = base_dir.join("content.schema.toml");
println!("Validating content schemas...");
match validate_with_schema(&content_dir, &schema_path) {
Ok(()) => println!("Schema validation: all pages valid, 0 errors"),
Err(e) => {
eprintln!("Schema validation failed: {e}");
return Err(e);
}
}
let config = SsgConfig::builder()
.site_name("polaris-docs".to_string())
.base_url("http://127.0.0.1:3003".to_string())
.content_dir(content_dir.clone())
.output_dir(output_dir.clone())
.template_dir(template_dir.clone())
.site_title("Polaris Documentation".to_string())
.site_description(
"Documentation template for any developer tool, library, or API"
.to_string(),
)
.language("en-GB".to_string())
.build()
.context("Failed to build configuration")?;
let paths = Paths {
content: content_dir.clone(),
build: output_dir.clone(),
site: site_dir.clone(),
template: template_dir.clone(),
};
let ctx = PluginContext::with_config(
&config.content_dir,
&config.output_dir,
&paths.site,
&config.template_dir,
config.clone(),
);
let mut plugins = PluginManager::new();
plugins.register(ssg::shortcodes::ShortcodePlugin);
#[cfg(feature = "templates")]
plugins.register(ssg::template_plugin::TemplatePlugin::from_template_dir(
&config.template_dir,
));
plugins.register(ssg::postprocess::SitemapFixPlugin);
plugins.register(ssg::postprocess::NewsSitemapFixPlugin);
plugins.register(ssg::postprocess::RssAggregatePlugin);
plugins.register(ssg::postprocess::AtomFeedPlugin);
plugins.register(ssg::postprocess::ManifestFixPlugin);
plugins.register(ssg::postprocess::HtmlFixPlugin);
plugins.register(ssg::highlight::HighlightPlugin::default());
plugins.register(ssg::seo::SeoPlugin);
plugins.register(ssg::seo::JsonLdPlugin::from_site(
&config.base_url,
&config.site_name,
));
plugins.register(ssg::seo::CanonicalPlugin::new(config.base_url.clone()));
plugins.register(ssg::seo::RobotsPlugin::new(config.base_url.clone()));
plugins.register(ssg::search::SearchPlugin);
plugins.register(ssg::accessibility::AccessibilityPlugin);
#[cfg(feature = "image-optimization")]
plugins.register(ssg::image_plugin::ImageOptimizationPlugin::default());
plugins.register(ssg::ai::AiPlugin);
plugins.register(ssg::llm::LlmPlugin::new(ssg::llm::LlmConfig::default()));
plugins.register(ssg::taxonomy::TaxonomyPlugin);
plugins.register(ssg::pagination::PaginationPlugin::default());
plugins.register(ssg::drafts::DraftPlugin::new(false));
plugins.register(ssg::assets::FingerprintPlugin);
plugins.register(ssg::plugins::MinifyPlugin);
println!("\nBuilding documentation portal...");
let start = Instant::now();
execute_build_pipeline(
&plugins,
&ctx,
&config.output_dir,
&config.content_dir,
&paths.site,
&config.template_dir,
false,
)?;
let elapsed = start.elapsed();
println!(" \u{26a1} Built in {elapsed:.0?}");
hide_language_icon(&site_dir)?;
let page_count = fs::read_dir(&site_dir).map_or(0, |entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "html"))
.count()
});
println!("\n=== Build Summary ===");
println!(" Pages built: {page_count}");
let search_path = site_dir.join("search-index.json");
if search_path.exists() {
let size = fs::metadata(&search_path)?.len();
println!(" Search index: {size} bytes");
} else {
println!(" Search index: not found");
}
let rss_exists = site_dir.join("rss.xml").exists();
let atom_exists = site_dir.join("atom.xml").exists();
println!(
" RSS feed: {}",
if rss_exists { "rss.xml" } else { "not found" }
);
println!(
" Atom feed: {}",
if atom_exists { "atom.xml" } else { "not found" }
);
println!("====================\n");
let doc_root = site_dir
.to_str()
.context("Failed to convert site path to string")?
.to_string();
let server = Server::builder()
.address("127.0.0.1:3003")
.document_root(doc_root.as_str())
.custom_header("Permissions-Policy", "browsing-topics=()")
.build()
.map_err(|e| anyhow::anyhow!("{e}"))?;
server.start().context("Failed to start dev server")?;
Ok(())
}
fn hide_language_icon(site_dir: &std::path::Path) -> Result<()> {
const MARKER: &str = "/* ssg-single-locale: hide lang */";
const STYLE: &str =
"<style>/* ssg-single-locale: hide lang */.lang-btn,.lang-dropdown,.mobile-lang{display:none!important}</style>";
fn walk(dir: &std::path::Path, marker: &str, style: &str) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
walk(&path, marker, style)?;
} else if path.extension().is_some_and(|e| e == "html") {
let html = fs::read_to_string(&path)?;
if html.contains(marker) {
continue;
}
if let Some(pos) = html.find("</head>") {
let new_html =
format!("{}{}{}", &html[..pos], style, &html[pos..]);
fs::write(&path, new_html)?;
}
}
}
Ok(())
}
walk(site_dir, MARKER, STYLE)
}