Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-public-site/src/sitemap.rs

//! **`/sitemap.xml`** (S-3): the public pages and every PUBLIC repository,
//! each with its `lastmod`, rebuilt with the rest of the site on the timer. The
//! addresses are the console's (GUN-GUEST): a repository, its issues, and its
//! documentation (each Markdown file outside a hidden directory) at any depth.

use std::collections::BTreeSet;
use std::fmt::Write as _;
use std::sync::Arc;

use crate::head::{esc, ymd};
use crate::{issues, robots, text, Config, RepoRef, RepoSnapshot};

/// The documentation files one repository lists at most.
const MAX_DOCS_PER_REPO: usize = 500;

/// A documentation file: Markdown, outside any hidden directory (`.nornir/`, `.github/`).
fn is_doc(path: &str) -> bool {
    let lower = path.to_ascii_lowercase();
    (lower.ends_with(".md") || lower.ends_with(".markdown")) && !path.split('/').any(|seg| seg.starts_with('.'))
}

/// The file. `now` is the build's own time, the home's lastmod when no
/// repository is newer.
pub fn sitemap_xml(config: &Config, repos: &[(RepoRef, Arc<RepoSnapshot>)], kept: &issues::Kept, now_unix: i64) -> String {
    let base = &config.base_url;
    let newest = repos.iter().filter_map(|(_, s)| s.head.as_ref().map(|c| c.at_unix)).max();
    let mut s = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
    let mut url = |loc: String, lastmod: Option<i64>| {
        // A namespace whose name starts like a console path (`/accessories`
        // under `Disallow: /access`) is left out, so the sitemap never lists
        // what robots.txt tells a crawler to stay out of.
        let path = loc.strip_prefix(base.as_str()).unwrap_or(&loc);
        if robots::DISALLOWED.iter().any(|d| path.starts_with(d)) {
            return;
        }
        let _ = write!(s, "<url><loc>{}</loc>", esc(&loc));
        if let Some(t) = lastmod {
            let _ = write!(s, "<lastmod>{}</lastmod>", ymd(t));
        }
        s.push_str("</url>\n");
    };
    url(format!("{base}/"), Some(newest.unwrap_or(now_unix)));
    if config.signup {
        url(format!("{base}/signup"), None);
    }
    let namespaces: BTreeSet<&str> = repos.iter().map(|(r, _)| r.namespace.as_str()).collect();
    for ns in namespaces {
        let at = repos.iter().filter(|(r, _)| r.namespace == ns).filter_map(|(_, s)| s.head.as_ref().map(|c| c.at_unix)).max();
        url(format!("{base}/{ns}"), at);
    }
    for (repo, snap) in repos {
        let at = snap.head.as_ref().map(|c| c.at_unix);
        url(text::repo_url(config, repo), at);
        for doc in snap.tree.iter().filter(|e| !e.dir && is_doc(&e.path)).take(MAX_DOCS_PER_REPO) {
            url(format!("{}/blob/{}", text::repo_url(config, repo), text::pct_encode_path(&doc.path)), at);
        }
        // PUB-ISSUES: the issue list and each issue, by when it last moved.
        let list = kept.get(&issues::list_path(repo)).map(|(_, l)| l.as_slice()).unwrap_or_default();
        url(format!("{base}{}", issues::list_path(repo)), list.iter().map(crate::Issue::last_at_unix).max());
        for issue in list {
            url(format!("{base}{}", issues::issue_path(repo, issue.number)), Some(issue.last_at_unix()));
        }
    }
    s.push_str("</urlset>\n");
    s
}