crates/gunnar-public-site/src/pages.rs
//! Every file the site serves, built once per refresh into one map.
//!
//! GUN-GUEST (owner 2026-10-30): no HTML page of the site's own is left. A repository's
//! address is the console's (its document, with the page's content pre-rendered into it:
//! [`crate::shell`]); what is built here is robots.txt, the sitemap, llms.txt,
//! llms-full.txt, each public repository's readme.md, tree.txt and issues.md, IndexNow's key
//! file, and the console's own landing at `/` with the search tags in its head.
use std::collections::HashMap;
use std::sync::Arc;
use crate::{head, issues, robots, sitemap, text, Config, Page, RepoRef, RepoSnapshot, SignedIn};
pub const TEXT: &str = "text/plain; charset=utf-8";
pub const MARKDOWN: &str = "text/markdown; charset=utf-8";
pub const HTML: &str = "text/html; charset=utf-8";
pub const XML: &str = "application/xml; charset=utf-8";
fn put(pages: &mut HashMap<String, Page>, path: String, content_type: &'static str, body: String, signed_in: SignedIn) {
pages.insert(path, Page { content_type, body: Arc::from(body), signed_in });
}
/// Build every file from the public repositories (already filtered to public).
pub fn build(config: &Config, repos: &[(RepoRef, Arc<RepoSnapshot>)], kept: &issues::Kept, pages: &mut HashMap<String, Page>) {
// `/`: the console's own landing with the search tags in its head, a
// visitor's; a signed-in person is handed the console. No landing, no `/`.
if let Some(landing) = &config.landing {
put(pages, "/".into(), HTML, head::home(config, landing), SignedIn::Console);
}
put(pages, "/robots.txt".into(), TEXT, robots::robots_txt(config), SignedIn::Same);
// IndexNow's key file, only where IndexNow is switched on.
if let Some(ix) = &config.indexnow {
put(pages, format!("/{}.txt", ix.key), TEXT, ix.key.clone(), SignedIn::Same);
}
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or_default();
put(pages, "/sitemap.xml".into(), XML, sitemap::sitemap_xml(config, repos, kept, now), SignedIn::Same);
put(pages, "/llms.txt".into(), TEXT, text::llms_txt(config, repos), SignedIn::Same);
put(pages, "/llms-full.txt".into(), MARKDOWN, text::llms_full_txt(config, repos), SignedIn::Same);
for (repo, snap) in repos {
put(pages, text::readme_md_path(repo), MARKDOWN, text::readme_md(config, repo, snap), SignedIn::Same);
put(pages, text::tree_txt_path(repo), TEXT, text::tree_txt(repo, snap), SignedIn::Same);
let list: &[crate::Issue] = kept.get(&issues::list_path(repo)).map(|(_, l)| l.as_slice()).unwrap_or_default();
put(pages, issues::archive_path(repo), MARKDOWN, issues::archive_md(config, repo, list), SignedIn::Same);
}
}