diff --git a/README.md b/README.md index cf48ed3d82..9b91c7e86d 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,14 @@ These tasks still need to be done before Perseus can be pushed to v1.0.0. - [x] Create a custom CLI as a harness for apps without ridiculous amounts of configuration needed -* [ ] Support custom template hierarchies * [x] Support i18n out of the box -* [x] (Maybe) Implement custom router -* [ ] Pre-built integrations for Actix Web (done) and AWS Lambda (todo) +* [x] Implement custom router +* [x] Allow direct modification of the document head +* [ ] Improve SEO and initial load performance +* [ ] Support custom template hierarchies +* [ ] Pre-built integrations + - [x] Actix Web + - [ ] AWS Lambda ### Beyond diff --git a/examples/basic/index.html b/examples/basic/index.html index d9b3c52d9d..aadba0f1d1 100644 --- a/examples/basic/index.html +++ b/examples/basic/index.html @@ -4,7 +4,6 @@ - Perseus Starter App diff --git a/examples/cli/index.html b/examples/cli/index.html index d9b3c52d9d..aadba0f1d1 100644 --- a/examples/cli/index.html +++ b/examples/cli/index.html @@ -4,7 +4,6 @@ - Perseus Starter App diff --git a/examples/cli/src/pages/about.rs b/examples/cli/src/pages/about.rs index 8c88a6f470..db91bda52d 100644 --- a/examples/cli/src/pages/about.rs +++ b/examples/cli/src/pages/about.rs @@ -10,7 +10,9 @@ pub fn about_page() -> SycamoreTemplate { } pub fn get_page() -> Template { - Template::new("about").template(template_fn()) + Template::new("about") + .template(template_fn()) + .head(head_fn()) } pub fn template_fn() -> perseus::template::TemplateFn { @@ -20,3 +22,11 @@ pub fn template_fn() -> perseus::template::TemplateFn { } }) } + +pub fn head_fn() -> perseus::template::HeadFn { + Rc::new(|_| { + template! { + title { "About Page | Perseus Example – Basic" } + } + }) +} diff --git a/examples/cli/src/pages/index.rs b/examples/cli/src/pages/index.rs index 30a0423953..8c63f16c97 100644 --- a/examples/cli/src/pages/index.rs +++ b/examples/cli/src/pages/index.rs @@ -20,6 +20,7 @@ pub fn get_page() -> Template { Template::new("index") .build_state_fn(Rc::new(get_static_props)) .template(template_fn()) + .head(head_fn()) } pub async fn get_static_props(_path: String) -> StringResultWithCause { @@ -38,3 +39,11 @@ pub fn template_fn() -> perseus::template::TemplateFn { } }) } + +pub fn head_fn() -> perseus::template::HeadFn { + Rc::new(|_| { + template! { + title { "Index Page | Perseus Example – Basic" } + } + }) +} diff --git a/examples/i18n/index.html b/examples/i18n/index.html index d9b3c52d9d..08b218389d 100644 --- a/examples/i18n/index.html +++ b/examples/i18n/index.html @@ -4,7 +4,7 @@ - Perseus Starter App + Perseus Example – i18n diff --git a/examples/showcase/index.html b/examples/showcase/index.html index d7afe211a6..75f8106348 100644 --- a/examples/showcase/index.html +++ b/examples/showcase/index.html @@ -4,7 +4,7 @@ - Perseus Showcase App + Perseus Example – Showcase diff --git a/packages/perseus-actix-web/src/configurer.rs b/packages/perseus-actix-web/src/configurer.rs index 8e68d23a6c..c4a61e0b04 100644 --- a/packages/perseus-actix-web/src/configurer.rs +++ b/packages/perseus-actix-web/src/configurer.rs @@ -56,12 +56,13 @@ pub async fn configurer", // It's safe to assume that something we just deserialized will serialize again in this case &format!( - "\n", + "\n\n", serde_json::to_string(&render_cfg).unwrap() ), ); diff --git a/packages/perseus/src/shell.rs b/packages/perseus/src/shell.rs index 9b1496dcee..53a3d1c044 100644 --- a/packages/perseus/src/shell.rs +++ b/packages/perseus/src/shell.rs @@ -186,6 +186,23 @@ pub fn app_shell( } }; + // Render the document head + let head_str = template.render_head_str(page_data.state.clone(), Rc::clone(&translator)); + // Get the current head + let head_elem = web_sys::window() + .unwrap() + .document() + .unwrap() + .query_selector("head") + .unwrap() + .unwrap(); + let head_html = head_elem.inner_html(); + // We'll assume that there's already previously interpolated head in addition to the hardcoded stuff, but it will be separated by the server-injected delimiter comment + // Thus, we replace the stuff after that delimiter comment with the new head + let head_parts: Vec<&str> = head_html.split("").collect(); + let new_head = format!("{}\n{}", head_parts[0], head_str); + head_elem.set_inner_html(&new_head); + // Hydrate that static code using the acquired state // BUG (Sycamore): this will double-render if the component is just text (no nodes) sycamore::hydrate_to( diff --git a/packages/perseus/src/template.rs b/packages/perseus/src/template.rs index 858fe64e19..74667eef5c 100644 --- a/packages/perseus/src/template.rs +++ b/packages/perseus/src/template.rs @@ -2,6 +2,7 @@ use crate::errors::*; use crate::Request; +use crate::SsrNode; use crate::Translator; use futures::Future; use std::collections::HashMap; @@ -113,6 +114,10 @@ make_async_trait!(ShouldRevalidateFnType, StringResultWithCause); /// The type of functions that are given a state and render a page. If you've defined state for your page, it's safe to `.unwrap()` the /// given `Option`. If you're using i18n, an `Rc` will also be made available through Sycamore's [context system](https://sycamore-rs.netlify.app/docs/advanced/advanced_reactivity). pub type TemplateFn = Rc) -> SycamoreTemplate>; +/// A type alias for the function that modifies the document head. This is just a template function that will always be server-side +/// rendered in function (it may be rendered on the client, but it will always be used to create an HTML string, rather than a reactive +/// template). +pub type HeadFn = TemplateFn; /// The type of functions that get build paths. pub type GetBuildPathsFn = Rc; /// The type of functions that get build state. @@ -137,6 +142,10 @@ pub struct Template { /// This will be executed inside `sycamore::render_to_string`, and should return a `Template`. This takes an `Option` /// because otherwise efficient typing is almost impossible for templates without any properties (solutions welcome in PRs!). template: TemplateFn, + /// A function that will be used to populate the document's `` with metadata such as the title. This will be passed state in + /// the same way as `template`, but will always be rendered to a string, whcih will then be interpolated directly into the ``, + /// so reactivity here will not work! + head: TemplateFn, /// A function that gets the paths to render for at built-time. This is equivalent to `get_static_paths` in NextJS. If /// `incremental_path_rendering` is `true`, more paths can be rendered at request time on top of these. get_build_paths: Option, @@ -171,6 +180,8 @@ impl Template { Self { path: path.to_string(), template: Rc::new(|_: Option| sycamore::template! {}), + // Unlike `template`, this may not be set at all (especially in very simple apps) + head: Rc::new(|_: Option| sycamore::template! {}), get_build_paths: None, incremental_path_rendering: false, get_build_state: None, @@ -183,7 +194,6 @@ impl Template { // Render executors /// Executes the user-given function that renders the template on the server-side (build or request time). - // TODO possibly duplicate routes context here to avoid disappearance issues? pub fn render_for_template( &self, props: Option, @@ -197,6 +207,19 @@ impl Template { }) } } + /// Executes the user-given function that renders the document ``, returning a string to be interpolated manually. Reactivity + /// in this function will not take effect due to this string rendering. Note that this function will provide a translator context. + pub fn render_head_str(&self, props: Option, translator: Rc) -> String { + sycamore::render_to_string(|| { + template! { + // We provide the translator through context, which avoids having to define a separate variable for every translation due to Sycamore's `template!` macro taking ownership with `move` closures + ContextProvider(ContextProviderProps { + value: Rc::clone(&translator), + children: || (self.head)(props) + }) + } + }) + } /// Gets the list of templates that should be prerendered for at build-time. pub async fn get_build_paths(&self) -> Result> { if let Some(get_build_paths) = &self.get_build_paths { @@ -363,6 +386,11 @@ impl Template { self.template = val; self } + /// Sets the document head rendering function to use. + pub fn head(mut self, val: TemplateFn) -> Template { + self.head = val; + self + } /// Enables the *build paths* strategy with the given function. pub fn build_paths_fn(mut self, val: GetBuildPathsFn) -> Template { self.get_build_paths = Some(val);