` in your markup (double or single quotes, `root_id` replaced by what this property is set to).
$crate::define_app_root!($($root_selector)?);
- /// Gets the config manager to use. This allows the user to conveniently test production managers in development. If nothing is
- /// given, the filesystem will be used.
- $crate::define_get_config_manager!($($config_manager)?);
+ /// Gets the immutable store to use. This allows the user to conveniently change the path of distribution artifacts.
+ $crate::define_get_immutable_store!($($dist_path)?);
+ /// Gets the mutable store to use. This allows the user to conveniently substitute the default filesystem store for another
+ /// one in development and production.
+ $crate::define_get_mutable_store!($($mutable_store)?);
/// Gets the translations manager to use. This allows the user to conveniently test production managers in development. If
/// nothing is given, the filesystem will be used.
diff --git a/packages/perseus/src/router.rs b/packages/perseus/src/router.rs
index 21d743c98e..147cf894b1 100644
--- a/packages/perseus/src/router.rs
+++ b/packages/perseus/src/router.rs
@@ -4,21 +4,26 @@ use crate::TemplateMap;
use std::collections::HashMap;
use sycamore::prelude::GenericNode;
-/// Determines the template to use for the given path by checking against the render configuration. This houses the central routing
-/// algorithm of Perseus, which is based fully on the fact that we know about every single page except those rendered with ISR, and we
-/// can infer about them based on template root path domains. If that domain system is violated, this routing algorithm will not
-/// behave as expected whatsoever (as far as routing goes, it's undefined behaviour)!
+/// Determines the template to use for the given path by checking against the render configuration., also returning whether we matched
+/// a simple page or an incrementally-generated one (`true` for incrementally generated). Note that simple pages include those on
+/// incrementally-generated templates that we pre-rendered with *build paths* at build-time (and are hence in an immutable store rather
+/// than a mutable store).
+///
+/// This houses the central routing algorithm of Perseus, which is based fully on the fact that we know about every single page except
+/// those rendered with ISR, and we can infer about them based on template root path domains. If that domain system is violated, this
+/// routing algorithm will not behave as expected whatsoever (as far as routing goes, it's undefined behaviour)!
pub fn get_template_for_path<'a, G: GenericNode>(
raw_path: &str,
render_cfg: &HashMap
,
templates: &'a TemplateMap,
-) -> Option<&'a Template> {
+) -> (Option<&'a Template>, bool) {
let mut path = raw_path;
// If the path is empty, we're looking for the special `index` page
if path.is_empty() {
path = "index";
}
+ let mut was_incremental_match = false;
// Match the path to one of the templates
let mut template_name = String::new();
// We'll try a direct match first
@@ -36,6 +41,7 @@ pub fn get_template_for_path<'a, G: GenericNode>(
// If we find something, keep going until we don't (maximise specificity)
if let Some(template_root_path) = render_cfg.get(&path_to_try) {
+ was_incremental_match = true;
template_name = template_root_path.to_string();
} else {
break;
@@ -44,11 +50,11 @@ pub fn get_template_for_path<'a, G: GenericNode>(
}
// If we still have nothing, then the page doesn't exist
if template_name.is_empty() {
- return None;
+ return (None, was_incremental_match);
}
// Get the template to use (the `Option` this returns is perfect) if it exists
- templates.get(&template_name)
+ (templates.get(&template_name), was_incremental_match)
}
/// Matches the given path to a `RouteVerdict`. This takes a `TemplateMap` to match against, the render configuration to index, and it
@@ -72,13 +78,15 @@ pub fn match_route(
// We'll assume this has already been i18ned (if one of your routes has the same name as a supported locale, ffs)
let path_without_locale = path_slice[1..].to_vec().join("/");
// Get the template to use
- let template = get_template_for_path(&path_without_locale, render_cfg, templates);
+ let (template, was_incremental_match) =
+ get_template_for_path(&path_without_locale, render_cfg, templates);
verdict = match template {
Some(template) => RouteVerdict::Found(RouteInfo {
locale: locale.to_string(),
// This will be used in asset fetching from the server
path: path_without_locale,
template: template.clone(),
+ was_incremental_match,
}),
None => RouteVerdict::NotFound,
};
@@ -93,13 +101,15 @@ pub fn match_route(
verdict = RouteVerdict::LocaleDetection(path_joined);
} else {
// Get the template to use
- let template = get_template_for_path(&path_joined, render_cfg, templates);
+ let (template, was_incremental_match) =
+ get_template_for_path(&path_joined, render_cfg, templates);
verdict = match template {
Some(template) => RouteVerdict::Found(RouteInfo {
locale: locales.default.to_string(),
// This will be used in asset fetching from the server
path: path_joined,
template: template.clone(),
+ was_incremental_match,
}),
None => RouteVerdict::NotFound,
};
@@ -115,6 +125,9 @@ pub struct RouteInfo {
pub path: String,
/// The template that will be used. The app shell will derive pros and a translator to pass to the template function.
pub template: Template,
+ /// Whether or not the matched page was incrementally-generated at runtime (if it has been yet). If this is `true`, the server will
+ /// use a mutable store rather than an immutable one. See the book for more details.
+ pub was_incremental_match: bool,
/// The locale for the template to be rendered in.
pub locale: String,
}
diff --git a/packages/perseus/src/serve.rs b/packages/perseus/src/serve.rs
index fb27a8403d..94a8cd1e9f 100644
--- a/packages/perseus/src/serve.rs
+++ b/packages/perseus/src/serve.rs
@@ -1,8 +1,8 @@
// This file contains the universal logic for a serving process, regardless of framework
-use crate::config_manager::ConfigManager;
use crate::decode_time_str::decode_time_str;
use crate::errors::*;
+use crate::stores::{ImmutableStore, MutableStore};
use crate::template::{States, Template, TemplateMap};
use crate::Request;
use crate::TranslationsManager;
@@ -25,11 +25,11 @@ pub struct PageData {
pub head: String,
}
-/// Gets the configuration of how to render each page.
+/// Gets the configuration of how to render each page using an immutable store.
pub async fn get_render_cfg(
- config_manager: &impl ConfigManager,
+ immutable_store: &ImmutableStore,
) -> Result, ServerError> {
- let content = config_manager.read("render_conf.json").await?;
+ let content = immutable_store.read("render_conf.json").await?;
let cfg = serde_json::from_str::>(&content).map_err(|e| {
// We have to convert it into a build error and then into a server error
let build_err: BuildError = e.into();
@@ -39,20 +39,21 @@ pub async fn get_render_cfg(
Ok(cfg)
}
-/// Renders a template that uses state generated at build-time.
+/// 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,
- config_manager: &impl ConfigManager,
+ immutable_store: &ImmutableStore,
) -> Result<(String, String, Option), ServerError> {
// Get the static HTML
- let html = config_manager
+ let html = immutable_store
.read(&format!("static/{}.html", path_encoded))
.await?;
- let head = config_manager
+ let head = immutable_store
.read(&format!("static/{}.head.html", path_encoded))
.await?;
// Get the static JSON
- let state = match config_manager
+ let state = match immutable_store
.read(&format!("static/{}.json", path_encoded))
.await
{
@@ -62,7 +63,32 @@ async fn render_build_state(
Ok((html, head, state))
}
-/// Renders a template that generated its state at request-time. Note that revalidation and ISR have no impact on SSR-rendered pages.
+/// 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), 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,
translator: Rc,
@@ -79,12 +105,14 @@ async fn render_request_state(
Ok((html, head, state))
}
-/// Checks if a template that uses ISR has already been cached.
+/// 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,
- config_manager: &impl ConfigManager,
+ mutable_store: &impl MutableStore,
) -> Option<(String, String)> {
- let html_res = config_manager
+ let html_res = mutable_store
.read(&format!("static/{}.html", path_encoded))
.await;
@@ -92,8 +120,8 @@ async fn get_incremental_cached(
match html_res {
Ok(html) if !cfg!(debug_assertions) => {
// If the HTML exists, the head must as well
- let head = config_manager
- .read(&format!("static/{}.html", path_encoded))
+ let head = mutable_store
+ .read(&format!("static/{}.head.html", path_encoded))
.await
.unwrap();
Some((html, head))
@@ -101,17 +129,19 @@ async fn get_incremental_cached(
Ok(_) | Err(_) => None,
}
}
-/// Checks if a template should revalidate by time.
+/// 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,
path_encoded: &str,
- config_manager: &impl ConfigManager,
+ mutable_store: &impl MutableStore,
) -> Result {
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)
- let datetime_to_revalidate_str = config_manager
+ // 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)
@@ -135,13 +165,14 @@ async fn should_revalidate(
}
Ok(should_revalidate)
}
-/// Revalidates a template
+/// 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,
translator: Rc,
path: &str,
path_encoded: &str,
- config_manager: &impl ConfigManager,
+ mutable_store: &impl MutableStore,
) -> Result<(String, String, Option), ServerError> {
// We need to regenerate and cache this page for future usage (until the next revalidation)
let state = Some(
@@ -159,23 +190,23 @@ async fn revalidate(
// 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())?;
- config_manager
+ mutable_store
.write(
&format!("static/{}.revld.txt", path_encoded),
&datetime_to_revalidate,
)
.await?;
}
- config_manager
+ mutable_store
.write(
&format!("static/{}.json", path_encoded),
&state.clone().unwrap(),
)
.await?;
- config_manager
+ mutable_store
.write(&format!("static/{}.html", path_encoded), &html)
.await?;
- config_manager
+ mutable_store
.write(&format!("static/{}.head.html", path_encoded), &head)
.await?;
@@ -183,15 +214,18 @@ async fn revalidate(
}
/// 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).
+/// 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,
+ // 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,
- config_manager: &impl ConfigManager,
+ (immutable_store, mutable_store): (&ImmutableStore, &impl MutableStore),
translations_manager: &impl TranslationsManager,
) -> Result {
// Get a translator for this locale (for sanity we hope the manager is caching)
@@ -219,20 +253,22 @@ pub async fn get_page_for_template(
// 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() {
+ // TODO separate out build paths pages, which are in the immutable store
+ 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, config_manager).await;
+ 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, config_manager).await? {
+ if should_revalidate(template, &path_encoded, mutable_store).await? {
let (html_val, head_val, state) = revalidate(
template,
Rc::clone(&translator),
path,
&path_encoded,
- config_manager,
+ mutable_store,
)
.await?;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
@@ -248,7 +284,7 @@ pub async fn get_page_for_template(
head = head_val;
}
// Get the static JSON (if it exists, but it should)
- states.build_state = match config_manager
+ states.build_state = match mutable_store
.read(&format!("static/{}.json", path_encoded))
.await
{
@@ -258,6 +294,7 @@ pub async fn get_page_for_template(
}
}
// 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()).await?);
@@ -273,7 +310,7 @@ pub async fn get_page_for_template(
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
- config_manager
+ mutable_store
.write(
&format!("static/{}.revld.txt", path_encoded),
&datetime_to_revalidate,
@@ -281,17 +318,17 @@ pub async fn get_page_for_template(
.await?;
}
// Cache all that
- config_manager
+ mutable_store
.write(
&format!("static/{}.json", path_encoded),
&state.clone().unwrap(),
)
.await?;
// Write that prerendered HTML to a static file
- config_manager
+ mutable_store
.write(&format!("static/{}.html", path_encoded), &html_val)
.await?;
- config_manager
+ mutable_store
.write(&format!("static/{}.head.html", path_encoded), &head_val)
.await?;
@@ -304,14 +341,17 @@ pub async fn get_page_for_template(
}
}
} 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
- if should_revalidate(template, &path_encoded, config_manager).await? {
+ // 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,
Rc::clone(&translator),
path,
&path_encoded,
- config_manager,
+ mutable_store,
)
.await?;
// Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has
@@ -320,9 +360,21 @@ pub async fn get_page_for_template(
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, config_manager).await?;
+ 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;
@@ -366,16 +418,16 @@ pub async fn get_page_for_template(
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.
+/// 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: &str,
+ (template_name, was_incremental_match): (&str, bool),
req: Request,
templates: &TemplateMap,
- config_manager: &impl ConfigManager,
+ (immutable_store, mutable_store): (&ImmutableStore, &impl MutableStore),
translations_manager: &impl TranslationsManager,
) -> Result {
let mut path = raw_path;
@@ -400,8 +452,9 @@ pub async fn get_page(
raw_path,
locale,
template,
+ was_incremental_match,
req,
- config_manager,
+ (immutable_store, mutable_store),
translations_manager,
)
.await?;
diff --git a/packages/perseus/src/shell.rs b/packages/perseus/src/shell.rs
index 898125af61..bc84a10a2f 100644
--- a/packages/perseus/src/shell.rs
+++ b/packages/perseus/src/shell.rs
@@ -214,7 +214,7 @@ pub enum InitialState {
// TODO handle exceptions higher up
pub async fn app_shell(
path: String,
- template: Template,
+ (template, was_incremental_match): (Template, bool),
locale: String,
translations_manager: Rc>,
error_pages: Rc>,
@@ -288,10 +288,11 @@ pub async fn app_shell(
};
// Get the static page data
let asset_url = format!(
- "/.perseus/page/{}/{}.json?template_name={}",
+ "/.perseus/page/{}/{}.json?template_name={}&was_incremental_match={}",
locale,
path.to_string(),
- template.get_path()
+ template.get_path(),
+ was_incremental_match
);
// If this doesn't exist, then it's a 404 (we went here by explicit navigation, but it may be an unservable ISR page or the like)
let page_data_str = fetch(&asset_url).await;
diff --git a/packages/perseus/src/stores/immutable.rs b/packages/perseus/src/stores/immutable.rs
new file mode 100644
index 0000000000..bcfa23ae53
--- /dev/null
+++ b/packages/perseus/src/stores/immutable.rs
@@ -0,0 +1,51 @@
+use crate::errors::*;
+use std::fs;
+
+/// An immutable storage system. This wraps filesystem calls in a sensible asynchronous API, allowing abstraction of the base path
+/// to a distribution directory or the like. Perseus uses this to store assts created at build time that won't change, which is
+/// anything not involved in the *revalidation* or *incremental generation* strategies.
+///
+/// Note: the `.write()` methods on this implementation will create any missing parent directories automatically.
+#[derive(Clone)]
+pub struct ImmutableStore {
+ root_path: String,
+}
+impl ImmutableStore {
+ /// Creates a new immutable store. You should provide a path like `dist/` here.
+ pub fn new(root_path: String) -> Self {
+ Self { root_path }
+ }
+ /// Reads the given asset from the filesystem asynchronously.
+ pub async fn read(&self, name: &str) -> Result {
+ let asset_path = format!("{}/{}", self.root_path, name);
+ match fs::metadata(&asset_path) {
+ Ok(_) => fs::read_to_string(&asset_path).map_err(|err| StoreError::ReadFailed {
+ name: asset_path,
+ source: err.into(),
+ }),
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
+ Err(StoreError::NotFound { name: asset_path })
+ }
+ Err(err) => Err(StoreError::ReadFailed {
+ name: asset_path,
+ source: err.into(),
+ }),
+ }
+ }
+ /// Writes the given asset to the filesystem asynchronously. This must only be used at build-time, and must not be changed
+ /// afterward.
+ pub async fn write(&self, name: &str, content: &str) -> Result<(), StoreError> {
+ let asset_path = format!("{}/{}", self.root_path, name);
+ let mut dir_tree: Vec<&str> = asset_path.split('/').collect();
+ dir_tree.pop();
+
+ fs::create_dir_all(dir_tree.join("/")).map_err(|err| StoreError::WriteFailed {
+ name: asset_path.clone(),
+ source: err.into(),
+ })?;
+ fs::write(&asset_path, content).map_err(|err| StoreError::WriteFailed {
+ name: asset_path,
+ source: err.into(),
+ })
+ }
+}
diff --git a/packages/perseus/src/stores/mod.rs b/packages/perseus/src/stores/mod.rs
new file mode 100644
index 0000000000..168754e2d6
--- /dev/null
+++ b/packages/perseus/src/stores/mod.rs
@@ -0,0 +1,7 @@
+/// Utilities for working with immutable stores.
+pub mod immutable;
+/// Utilities for working with mutable stores.
+pub mod mutable;
+
+pub use immutable::ImmutableStore;
+pub use mutable::{FsMutableStore, MutableStore};
diff --git a/packages/perseus/src/stores/mutable.rs b/packages/perseus/src/stores/mutable.rs
new file mode 100644
index 0000000000..1635097bd8
--- /dev/null
+++ b/packages/perseus/src/stores/mutable.rs
@@ -0,0 +1,65 @@
+use crate::errors::*;
+use std::fs;
+
+/// A trait for mutable stores. This is abstracted away so that users can implement a non-filesystem mutable store, which is useful
+/// for read-only filesystem environments, as on many modern hosting providers. See the book for further details on this subject.
+#[async_trait::async_trait]
+pub trait MutableStore: Clone {
+ /// Reads data from the named asset.
+ async fn read(&self, name: &str) -> Result;
+ /// Writes data to the named asset. This will create a new asset if one doesn't exist already.
+ async fn write(&self, name: &str, content: &str) -> Result<(), StoreError>;
+}
+
+/// The default mutable store, which simply uses the filesystem. This is suitable for development and production environments with
+/// writable filesystems (in which it's advised), but this is of course not usable on production read-only filesystems, and another
+/// implementation of `MutableStore` should be preferred.
+///
+/// Note: the `.write()` methods on this implementation will create any missing parent directories automatically.
+#[derive(Clone)]
+pub struct FsMutableStore {
+ root_path: String,
+}
+impl FsMutableStore {
+ /// Creates a new filesystem configuration manager. You should provide a path like `/dist/mutable` here. Make sure that this is
+ /// not the same path as the immutable store, as this will cause potentially problematic overlap between the two systems.
+ pub fn new(root_path: String) -> Self {
+ Self { root_path }
+ }
+}
+#[async_trait::async_trait]
+impl MutableStore for FsMutableStore {
+ async fn read(&self, name: &str) -> Result {
+ let asset_path = format!("{}/{}", self.root_path, name);
+ match fs::metadata(&asset_path) {
+ Ok(_) => fs::read_to_string(&asset_path).map_err(|err| StoreError::ReadFailed {
+ name: asset_path,
+ source: err.into(),
+ }),
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
+ return Err(StoreError::NotFound { name: asset_path })
+ }
+ Err(err) => {
+ return Err(StoreError::ReadFailed {
+ name: asset_path,
+ source: err.into(),
+ })
+ }
+ }
+ }
+ // This creates a directory structure as necessary
+ async fn write(&self, name: &str, content: &str) -> Result<(), StoreError> {
+ let asset_path = format!("{}/{}", self.root_path, name);
+ let mut dir_tree: Vec<&str> = asset_path.split('/').collect();
+ dir_tree.pop();
+
+ fs::create_dir_all(dir_tree.join("/")).map_err(|err| StoreError::WriteFailed {
+ name: asset_path.clone(),
+ source: err.into(),
+ })?;
+ fs::write(&asset_path, content).map_err(|err| StoreError::WriteFailed {
+ name: asset_path,
+ source: err.into(),
+ })
+ }
+}
diff --git a/packages/perseus/src/template.rs b/packages/perseus/src/template.rs
index 6bee1edb09..d7cd741d13 100644
--- a/packages/perseus/src/template.rs
+++ b/packages/perseus/src/template.rs
@@ -209,7 +209,6 @@ impl Template {
// Unlike `template`, this may not be set at all (especially in very simple apps)
head: Rc::new(|_: Option| sycamore::template! {}),
// We create sensible header defaults here
- // TODO header defaults
set_headers: Rc::new(|_: Option| default_headers()),
get_build_paths: None,
incremental_generation: false,
diff --git a/packages/perseus/src/translator/errors.rs b/packages/perseus/src/translator/errors.rs
index ba38ceaa43..6ad4cafd74 100644
--- a/packages/perseus/src/translator/errors.rs
+++ b/packages/perseus/src/translator/errors.rs
@@ -10,7 +10,6 @@ pub enum TranslatorError {
#[error("translations string for locale '{locale}' couldn't be parsed")]
TranslationsStrSerFailed {
locale: String,
- // TODO
#[source]
source: Box,
},