use super::{Edits, read_config, register_routes};
use crate::{CliResult, names::humanize, project::Project};
pub fn seo(project: &Project) -> CliResult {
let mut edits = Edits::new(project);
let app = humanize(read_config(&edits)?.worker_name()?);
let localized = edits.read("src/lib.rs")?.is_some_and(|lib| lib.contains("static LOCALES"));
edits.create("src/seo.rs", seo_rs(&app, localized))?;
if let Some(vars) = edits.read(".dev.vars")?
&& !vars.lines().any(|line| line.split('=').next().is_some_and(|key| key.trim() == "APP_URL"))
{
let vars = if vars.is_empty() || vars.ends_with('\n') { vars } else { format!("{vars}\n") };
edits.update(".dev.vars", format!("{vars}APP_URL=http://localhost:8787\n"));
}
register_routes(&mut edits, "seo")?;
let mut report = edits.apply("generate seo")?;
report.next = vec![
"list the public pages in PAGES (src/seo.rs), and their records in `sitemap`".to_owned(),
"ocre dev, then open http://localhost:8787/sitemap.xml and http://localhost:8787/llms.txt".to_owned(),
"production: APP_URL (the public origin) in worker.env of cloudflare.config.ts; add `Sitemap: https://<your host>/sitemap.xml` to public/robots.txt".to_owned(),
];
Ok(report)
}
fn seo_rs(app: &str, localized: bool) -> String {
let (doc, add) = if localized {
(
"//! The app has translations: each page is listed in every locale\n//! (`/<locale><path>`), with `hreflang` alternates.",
" let urls = crate::LOCALES.locale(crate::LOCALES.default_locale()).alternates(&base, page.path);\n \
let urls: Vec<(&str, &str)> = urls.iter().map(|(code, url)| (*code, url.as_str())).collect();\n \
sitemap.add_localized(&urls, None);",
)
} else {
(
"//! Pages are listed as they are; with translations (`ocre g locale`),\n//! use `Sitemap::add_localized` and `i18n.alternates`.",
" sitemap.add(SitemapUrl { loc: format!(\"{base}{}\", page.path), lastmod: None, alternates: vec![] });",
)
};
let link = if localized {
"crate::LOCALES.locale(crate::LOCALES.default_locale()).path(page.path)"
} else {
"page.path.to_owned()"
};
let import = if localized { "Sitemap" } else { "Sitemap, SitemapUrl" };
format!(
r#"//! What search engines and language models read: `/sitemap.xml` and
//! `/llms.txt`, both built from `PAGES`. Generated by `ocre g seo`.
{doc}
//! Pages also need their `<title>`, a description, and in their `<head>`
//! `{{{{ i18n.alternate_links(base_url, path)|safe }}}}` (translated pages) and
//! `{{{{ ocre::seo::json_ld(&data)|safe }}}}` (structured data).
use axum::{{Router, extract::State, routing::get}};
use ocre::{{
Ctx, Result,
seo::{{LlmsTxt, {import}}},
}};
/// A public page, by its path without a locale prefix.
pub struct Page {{
pub path: &'static str,
pub title: &'static str,
/// One sentence, for `llms.txt`.
pub description: &'static str,
}}
/// The public pages, one line each; the sitemap and `llms.txt` list them.
pub const PAGES: &[Page] = &[Page {{ path: "/", title: "{app}", description: "The home page." }}];
pub fn routes() -> Router<Ctx> {{
Router::new().route("/sitemap.xml", get(sitemap)).route("/llms.txt", get(llms))
}}
/// `APP_URL`, the app's public origin (`https://example.com`).
fn base_url(ctx: &Ctx) -> Result<String> {{
Ok(ocre::mail::url(ctx, "/")?.trim_end_matches('/').to_owned())
}}
async fn sitemap(State(ctx): State<Ctx>) -> Result<Sitemap> {{
let base = base_url(&ctx)?;
let mut sitemap = Sitemap::new();
for page in PAGES {{
{add}
}}
// Records with a public page, e.g. (`updated_at` is `YYYY-MM-DD HH:MM:SS`):
// for post in crate::models::post::query().order_desc("id").limit(1000).all(&ctx.db()?).await? {{
// let lastmod = post.updated_at.get(..10).map(str::to_owned);
// sitemap.add(SitemapUrl {{ loc: format!("{{base}}/posts/{{}}", post.id), lastmod, alternates: vec![] }});
// }}
Ok(sitemap)
}}
async fn llms(State(ctx): State<Ctx>) -> Result<LlmsTxt> {{
let base = base_url(&ctx)?;
let mut txt = LlmsTxt::new("{app}", "What {app} is, in one sentence.");
for page in PAGES {{
txt = txt.link("Pages", page.title, &format!("{{base}}{{}}", {link}), page.description);
}}
Ok(txt)
}}
"#
)
}