Skip to content

Commit

Permalink
feat: ✨ added metadata modification systems
Browse files Browse the repository at this point in the history
Sensible modification/setting of the `<head>` is now possible. Still glitchy until #2 is sorted though.

Closes #13.
  • Loading branch information
arctic-hen7 committed Sep 17, 2021
1 parent c3ad018 commit bb847aa
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 10 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion examples/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Starter App</title>
<!-- Importing this runs Perseus -->
<script type="module" src="/.perseus/main.js" defer></script>
</head>
Expand Down
1 change: 0 additions & 1 deletion examples/cli/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Starter App</title>
<!-- Importing this runs Perseus -->
<script type="module" src="/.perseus/main.js" defer></script>
</head>
Expand Down
12 changes: 11 additions & 1 deletion examples/cli/src/pages/about.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ pub fn about_page() -> SycamoreTemplate<G> {
}

pub fn get_page<G: GenericNode>() -> Template<G> {
Template::new("about").template(template_fn())
Template::new("about")
.template(template_fn())
.head(head_fn())
}

pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
Expand All @@ -20,3 +22,11 @@ pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
}
})
}

pub fn head_fn() -> perseus::template::HeadFn {
Rc::new(|_| {
template! {
title { "About Page | Perseus Example – Basic" }
}
})
}
9 changes: 9 additions & 0 deletions examples/cli/src/pages/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub fn get_page<G: GenericNode>() -> Template<G> {
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<String> {
Expand All @@ -38,3 +39,11 @@ pub fn template_fn<G: GenericNode>() -> perseus::template::TemplateFn<G> {
}
})
}

pub fn head_fn() -> perseus::template::HeadFn {
Rc::new(|_| {
template! {
title { "Index Page | Perseus Example – Basic" }
}
})
}
2 changes: 1 addition & 1 deletion examples/i18n/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Starter App</title>
<title>Perseus Example – i18n</title>
<!-- Importing this runs Perseus -->
<script type="module" src="/.perseus/main.js" defer></script>
</head>
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Perseus Showcase App</title>
<title>Perseus Example – Showcase</title>
<!-- Importing this runs Perseus -->
<script type="module" src="/.perseus/main.js" defer></script>
</head>
Expand Down
3 changes: 2 additions & 1 deletion packages/perseus-actix-web/src/configurer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ pub async fn configurer<C: ConfigManager + 'static, T: TranslationsManager + 'st
.expect("Couldn't get render configuration!");
// Get the index file and inject the render configuration into ahead of time
// We do this by injecting a script that defines the render config as a global variable, which we put just before the close of the head
// We also inject a delimiter comment that will be used to wall off the constant document head from the interpolated document head
let index_file = fs::read_to_string(&opts.index).expect("Couldn't get HTML index file!");
let index_with_render_cfg = index_file.replace(
"</head>",
// It's safe to assume that something we just deserialized will serialize again in this case
&format!(
"<script>window.__PERSEUS_RENDER_CFG = '{}';</script>\n</head>",
"<script>window.__PERSEUS_RENDER_CFG = '{}';</script>\n<!--PERSEUS_INTERPOLATED_HEAD_BEGINS-->\n</head>",
serde_json::to_string(&render_cfg).unwrap()
),
);
Expand Down
17 changes: 17 additions & 0 deletions packages/perseus/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<!--PERSEUS_INTERPOLATED_HEAD_BEGINS-->").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(
Expand Down
30 changes: 29 additions & 1 deletion packages/perseus/src/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::errors::*;
use crate::Request;
use crate::SsrNode;
use crate::Translator;
use futures::Future;
use std::collections::HashMap;
Expand Down Expand Up @@ -113,6 +114,10 @@ make_async_trait!(ShouldRevalidateFnType, StringResultWithCause<bool>);
/// 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<Translator>` will also be made available through Sycamore's [context system](https://sycamore-rs.netlify.app/docs/advanced/advanced_reactivity).
pub type TemplateFn<G> = Rc<dyn Fn(Option<String>) -> SycamoreTemplate<G>>;
/// 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<SsrNode>;
/// The type of functions that get build paths.
pub type GetBuildPathsFn = Rc<dyn GetBuildPathsFnType>;
/// The type of functions that get build state.
Expand All @@ -137,6 +142,10 @@ pub struct Template<G: GenericNode> {
/// This will be executed inside `sycamore::render_to_string`, and should return a `Template<SsrNode>`. This takes an `Option<Props>`
/// because otherwise efficient typing is almost impossible for templates without any properties (solutions welcome in PRs!).
template: TemplateFn<G>,
/// A function that will be used to populate the document's `<head>` 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 `<head>`,
/// so reactivity here will not work!
head: TemplateFn<SsrNode>,
/// 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<GetBuildPathsFn>,
Expand Down Expand Up @@ -171,6 +180,8 @@ impl<G: GenericNode> Template<G> {
Self {
path: path.to_string(),
template: Rc::new(|_: Option<String>| sycamore::template! {}),
// Unlike `template`, this may not be set at all (especially in very simple apps)
head: Rc::new(|_: Option<String>| sycamore::template! {}),
get_build_paths: None,
incremental_path_rendering: false,
get_build_state: None,
Expand All @@ -183,7 +194,6 @@ impl<G: GenericNode> Template<G> {

// 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<String>,
Expand All @@ -197,6 +207,19 @@ impl<G: GenericNode> Template<G> {
})
}
}
/// Executes the user-given function that renders the document `<head>`, 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<String>, translator: Rc<Translator>) -> 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<Vec<String>> {
if let Some(get_build_paths) = &self.get_build_paths {
Expand Down Expand Up @@ -363,6 +386,11 @@ impl<G: GenericNode> Template<G> {
self.template = val;
self
}
/// Sets the document head rendering function to use.
pub fn head(mut self, val: TemplateFn<SsrNode>) -> Template<G> {
self.head = val;
self
}
/// Enables the *build paths* strategy with the given function.
pub fn build_paths_fn(mut self, val: GetBuildPathsFn) -> Template<G> {
self.get_build_paths = Some(val);
Expand Down

0 comments on commit bb847aa

Please sign in to comment.