1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
use crate::decode_time_str::decode_time_str;
use crate::errors::*;
use crate::page_data::PageData;
use crate::stores::{ImmutableStore, MutableStore};
use crate::template::{States, Template, TemplateMap};
use crate::translations_manager::TranslationsManager;
use crate::translator::Translator;
use crate::Request;
use chrono::{DateTime, Utc};
use sycamore::prelude::SsrNode;
// Note: the reason there are a ton of seemingly useless named lifetimes here is because of [this](https://github.com/rust-lang/rust/issues/63033)
/// Renders a template that uses state generated at build-time. This can't be used for pages that revalidate because their data are
/// stored in a mutable store.
async fn render_build_state(
path_encoded: &str,
immutable_store: &ImmutableStore,
) -> Result<(String, String, Option<String>), ServerError> {
// Get the static HTML
let html = immutable_store
.read(&format!("static/{}.html", path_encoded))
.await?;
let head = immutable_store
.read(&format!("static/{}.head.html", path_encoded))
.await?;
// Get the static JSON
let state = match immutable_store
.read(&format!("static/{}.json", path_encoded))
.await
{
Ok(state) => Some(state),
Err(_) => None,
};
Ok((html, head, state))
}
/// Renders a template that uses state generated at build-time. This is specifically for page that revalidate, because they store data
/// in the mutable store.
async fn render_build_state_for_mutable(
path_encoded: &str,
mutable_store: &impl MutableStore,
) -> Result<(String, String, Option<String>), ServerError> {
// Get the static HTML
let html = mutable_store
.read(&format!("static/{}.html", path_encoded))
.await?;
let head = mutable_store
.read(&format!("static/{}.head.html", path_encoded))
.await?;
// Get the static JSON
let state = match mutable_store
.read(&format!("static/{}.json", path_encoded))
.await
{
Ok(state) => Some(state),
Err(_) => None,
};
Ok((html, head, state))
}
/// Renders a template that generated its state at request-time. Note that revalidation and incremental generation have no impact on
/// SSR-rendered pages. This does everything at request-time, and so doesn't need a mutable or immutable store.
async fn render_request_state(
template: &Template<SsrNode>,
translator: &Translator,
path: &str,
req: Request,
) -> Result<(String, String, Option<String>), ServerError> {
// Generate the initial state (this may generate an error, but there's no file that can't exist)
let state = Some(
template
.get_request_state(path.to_string(), translator.get_locale(), req)
.await?,
);
// Use that to render the static HTML
let html = sycamore::render_to_string(|| {
template.render_for_template(state.clone(), translator, true)
});
let head = template.render_head_str(state.clone(), translator);
Ok((html, head, state))
}
/// Checks if a template that uses incremental generation has already been cached. If the template was prerendered by *build paths*,
/// then it will have already been matched because those are declared verbatim in the render configuration. Therefore, this function
/// only searches for pages that have been cached later, which means it needs a mutable store.
async fn get_incremental_cached(
path_encoded: &str,
mutable_store: &impl MutableStore,
) -> Option<(String, String)> {
let html_res = mutable_store
.read(&format!("static/{}.html", path_encoded))
.await;
// We should only treat it as cached if it can be accessed and if we aren't in development (when everything should constantly reload)
match html_res {
Ok(html) if !cfg!(debug_assertions) => {
// If the HTML exists, the head must as well
let head = mutable_store
.read(&format!("static/{}.head.html", path_encoded))
.await
.unwrap();
Some((html, head))
}
Ok(_) | Err(_) => None,
}
}
/// Checks if a template should revalidate by time. All revalidation timestamps are stored in a mutable store, so that's what this
/// function uses.
async fn should_revalidate(
template: &Template<SsrNode>,
path_encoded: &str,
mutable_store: &impl MutableStore,
) -> Result<bool, ServerError> {
let mut should_revalidate = false;
// If it revalidates after a certain period of time, we needd to check that BEFORE the custom logic
if template.revalidates_with_time() {
// Get the time when it should revalidate (RFC 3339)
// This will be updated, so it's in a mutable store
let datetime_to_revalidate_str = mutable_store
.read(&format!("static/{}.revld.txt", path_encoded))
.await?;
let datetime_to_revalidate = DateTime::parse_from_rfc3339(&datetime_to_revalidate_str)
.map_err(|e| {
let serve_err: ServeError = e.into();
serve_err
})?;
// Get the current time (UTC)
let now = Utc::now();
// If the datetime to revalidate is still in the future, end with `false`
if datetime_to_revalidate > now {
return Ok(false);
}
should_revalidate = true;
}
// Now run the user's custom revalidation logic
if template.revalidates_with_logic() {
should_revalidate = template.should_revalidate().await?;
}
Ok(should_revalidate)
}
/// Revalidates a template. All information about templates that revalidate (timestamp, content, head, and state) is stored in a
/// mutable store, so that's what this function uses.
async fn revalidate(
template: &Template<SsrNode>,
translator: &Translator,
path: &str,
path_encoded: &str,
mutable_store: &impl MutableStore,
) -> Result<(String, String, Option<String>), ServerError> {
// We need to regenerate and cache this page for future usage (until the next revalidation)
let state = Some(
template
.get_build_state(
format!("{}/{}", template.get_path(), path),
translator.get_locale(),
)
.await?,
);
let html = sycamore::render_to_string(|| {
template.render_for_template(state.clone(), translator, true)
});
let head = template.render_head_str(state.clone(), translator);
// Handle revalidation, we need to parse any given time strings into datetimes
// We don't need to worry about revalidation that operates by logic, that's request-time only
if template.revalidates_with_time() {
// IMPORTANT: we set the new revalidation datetime to the interval from NOW, not from the previous one
// So if you're revalidating many pages weekly, they will NOT revalidate simultaneously, even if they're all queried thus
let datetime_to_revalidate = decode_time_str(&template.get_revalidate_interval().unwrap())?;
mutable_store
.write(
&format!("static/{}.revld.txt", path_encoded),
&datetime_to_revalidate,
)
.await?;
}
mutable_store
.write(
&format!("static/{}.json", path_encoded),
&state.clone().unwrap(),
)
.await?;
mutable_store
.write(&format!("static/{}.html", path_encoded), &html)
.await?;
mutable_store
.write(&format!("static/{}.head.html", path_encoded), &head)
.await?;
Ok((html, head, state))
}
/// Internal logic behind `get_page`. The only differences are that this takes a full template rather than just a template name, which
/// can avoid an unnecessary lookup if you already know the template in full (e.g. initial load server-side routing). Because this
/// handles templates with potentially revalidation and incremental generation, it uses both mutable and immutable stores.
// TODO possible further optimizations on this for futures?
pub async fn get_page_for_template(
// This must not contain the locale
raw_path: &str,
locale: &str,
template: &Template<SsrNode>,
// This allows us to differentiate pages for incrementally generated templates that were pre-rendered with build paths (and are in the immutable store) from those generated and cached at runtime (in the mutable store)
was_incremental_match: bool,
req: Request,
(immutable_store, mutable_store): (&ImmutableStore, &impl MutableStore),
translations_manager: &impl TranslationsManager,
) -> Result<PageData, ServerError> {
// Get a translator for this locale (for sanity we hope the manager is caching)
let translator = translations_manager
.get_translator_for_locale(locale.to_string())
.await?;
let mut path = raw_path;
// If the path is empty, we're looking for the special `index` page
if path.is_empty() {
path = "index";
}
// Remove `/` from the path by encoding it as a URL (that's what we store) and add the locale
let path_encoded = format!("{}-{}", locale, urlencoding::encode(path).to_string());
// Only a single string of HTML is needed, and it will be overridden if necessary (priorities system)
let mut html = String::new();
// The same applies for the document metadata
let mut head = String::new();
// Multiple rendering strategies may need to amalgamate different states
let mut states: States = States::new();
// Handle build state (which might use revalidation or incremental)
if template.uses_build_state() || template.is_basic() {
// If the template uses incremental generation, that is its own contained process
if template.uses_incremental() && was_incremental_match {
// This template uses incremental generation, and this page was built and cached at runtime in the mutable store
// Get the cached content if it exists (otherwise `None`)
let html_and_head_opt = get_incremental_cached(&path_encoded, mutable_store).await;
match html_and_head_opt {
// It's cached
Some((html_val, head_val)) => {
// Check if we need to revalidate
if should_revalidate(template, &path_encoded, mutable_store).await? {
let (html_val, head_val, state) =
revalidate(template, &translator, path, &path_encoded, mutable_store)
.await?;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
if html.is_empty() {
html = html_val;
head = head_val;
}
states.build_state = state;
} else {
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
if html.is_empty() {
html = html_val;
head = head_val;
}
// Get the static JSON (if it exists, but it should)
states.build_state = match mutable_store
.read(&format!("static/{}.json", path_encoded))
.await
{
Ok(state) => Some(state),
Err(_) => None,
};
}
}
// It's not cached
// All this uses the mutable store because this will be done at runtime
None => {
// We need to generate and cache this page for future usage
let state = Some(
template
.get_build_state(path.to_string(), locale.to_string())
.await?,
);
let html_val = sycamore::render_to_string(|| {
template.render_for_template(state.clone(), &translator, true)
});
let head_val = template.render_head_str(state.clone(), &translator);
// Handle revalidation, we need to parse any given time strings into datetimes
// We don't need to worry about revalidation that operates by logic, that's request-time only
// Obviously we don't need to revalidate now, we just created it
if template.revalidates_with_time() {
let datetime_to_revalidate =
decode_time_str(&template.get_revalidate_interval().unwrap())?;
// Write that to a static file, we'll update it every time we revalidate
// Note that this runs for every path generated, so it's fully usable with ISR
mutable_store
.write(
&format!("static/{}.revld.txt", path_encoded),
&datetime_to_revalidate,
)
.await?;
}
// Cache all that
mutable_store
.write(
&format!("static/{}.json", path_encoded),
&state.clone().unwrap(),
)
.await?;
// Write that prerendered HTML to a static file
mutable_store
.write(&format!("static/{}.html", path_encoded), &html_val)
.await?;
mutable_store
.write(&format!("static/{}.head.html", path_encoded), &head_val)
.await?;
states.build_state = state;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
if html.is_empty() {
html = html_val;
head = head_val;
}
}
}
} else {
// If we're here, incremental generation is either not used or it's irrelevant because the page was rendered in the immutable store at build time
// Handle if we need to revalidate
// It'll be in the mutable store if we do
if should_revalidate(template, &path_encoded, mutable_store).await? {
let (html_val, head_val, state) =
revalidate(template, &translator, path, &path_encoded, mutable_store).await?;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
if html.is_empty() {
html = html_val;
head = head_val;
}
states.build_state = state;
} else if template.revalidates() {
// The template does revalidate, but it doesn't need to revalidate now
// Nonetheless, its data will be the mutable store
let (html_val, head_val, state) =
render_build_state_for_mutable(&path_encoded, mutable_store).await?;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
if html.is_empty() {
html = html_val;
head = head_val;
}
states.build_state = state;
} else {
// If we don't need to revalidate and this isn't an incrementally generated template, everything is immutable
let (html_val, head_val, state) =
render_build_state(&path_encoded, immutable_store).await?;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
if html.is_empty() {
html = html_val;
head = head_val;
}
states.build_state = state;
}
}
}
// Handle request state
if template.uses_request_state() {
let (html_val, head_val, state) =
render_request_state(template, &translator, path, req).await?;
// Request-time HTML always overrides anything generated at build-time or incrementally (this has more information)
html = html_val;
head = head_val;
states.request_state = state;
}
// Amalgamate the states
// If the user has defined custom logic for this, we'll defer to that
// Otherwise we go as with HTML, request trumps build
// Of course, if only one state was defined, we'll just use that regardless (so `None` prioritization is impossible)
// If this is the case, the build content will still be served, and then it's up to the client to hydrate it with the new amalgamated state
let state: Option<String>;
if !states.both_defined() {
state = states.get_defined()?;
} else if template.can_amalgamate_states() {
state = template.amalgamate_states(states)?;
} else {
state = states.request_state;
}
// Combine everything into one JSON object
let res = PageData {
content: html,
state,
head,
};
Ok(res)
}
/// Gets the HTML/JSON data for the given page path. This will call SSG/SSR/etc., whatever is needed for that page. Note that HTML
/// generated at request-time will **always** replace anything generated at build-time, incrementally, revalidated, etc.
pub async fn get_page(
// This must not contain the locale
raw_path: &str,
locale: &str,
(template_name, was_incremental_match): (&str, bool),
req: Request,
templates: &TemplateMap<SsrNode>,
(immutable_store, mutable_store): (&ImmutableStore, &impl MutableStore),
translations_manager: &impl TranslationsManager,
) -> Result<PageData, ServerError> {
let mut path = raw_path;
// If the path is empty, we're looking for the special `index` page
if path.is_empty() {
path = "index";
}
// Get the template to use
let template = templates.get(template_name);
let template = match template {
Some(template) => template,
// This shouldn't happen because the client should already have performed checks against the render config, but it's handled anyway
None => {
return Err(ServeError::PageNotFound {
path: path.to_string(),
}
.into())
}
};
let res = get_page_for_template(
raw_path,
locale,
template,
was_incremental_match,
req,
(immutable_store, mutable_store),
translations_manager,
)
.await?;
Ok(res)
}
