Switch over to WebAssembly, Rust and Yew #35
@ -25,6 +25,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="head-ssg-before"></script>
|
||||||
|
<script id="head-ssg-after"></script>
|
||||||
|
|
||||||
<link data-trunk rel="rust" data-bin="site" data-wasm-opt="z" />
|
<link data-trunk rel="rust" data-bin="site" data-wasm-opt="z" />
|
||||||
<link data-trunk rel="inline" type="css" href="target/main.css" />
|
<link data-trunk rel="inline" type="css" href="target/main.css" />
|
||||||
<link data-trunk rel="copy-dir" href="public/media" />
|
<link data-trunk rel="copy-dir" href="public/media" />
|
||||||
|
60
src/app.rs
60
src/app.rs
@ -1,12 +1,13 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
use yew::{function_component, html, html::PhantomComponent, ContextProvider, Html, Properties};
|
||||||
|
|
||||||
use yew::{function_component, html, ContextProvider, Html, Properties};
|
|
||||||
use yew_router::{
|
use yew_router::{
|
||||||
history::{AnyHistory, MemoryHistory},
|
history::{AnyHistory, MemoryHistory},
|
||||||
BrowserRouter, Routable, Router, Switch,
|
BrowserRouter, Routable, Router, Switch,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{components::layout::Layout, pages::Route};
|
use crate::{
|
||||||
|
components::{head::HeadContext, layout::Layout},
|
||||||
|
pages::Route,
|
||||||
|
};
|
||||||
|
|
||||||
#[function_component(AppContent)]
|
#[function_component(AppContent)]
|
||||||
fn app_content() -> Html {
|
fn app_content() -> Html {
|
||||||
@ -19,60 +20,21 @@ fn app_content() -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Properties, PartialEq)]
|
|
||||||
pub struct AppProps {}
|
|
||||||
|
|
||||||
#[function_component(App)]
|
#[function_component(App)]
|
||||||
#[allow(unused_variables)]
|
pub fn app() -> Html {
|
||||||
pub fn app(props: &AppProps) -> Html {
|
|
||||||
let head = HeadWriter::default();
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<ContextProvider<HeadWriter> context={head}>
|
<PhantomComponent<ContextProvider<HeadContext>>>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ContextProvider<HeadWriter>>
|
</PhantomComponent<ContextProvider<HeadContext>>>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct HeadWriter {
|
|
||||||
content: Arc<Mutex<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for HeadWriter {
|
|
||||||
fn eq(&self, _: &Self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for HeadWriter {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
content: Arc::clone(&self.content),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HeadWriter {
|
|
||||||
pub fn take(self) -> String {
|
|
||||||
let mut content = self.content.lock().unwrap();
|
|
||||||
let mut taken = String::new();
|
|
||||||
std::mem::swap(&mut taken, &mut *content);
|
|
||||||
taken
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_fmt(&self, args: std::fmt::Arguments<'_>) {
|
|
||||||
let mut content = self.content.lock().unwrap();
|
|
||||||
std::fmt::Write::write_fmt(&mut *content, args).unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct StaticAppProps {
|
pub struct StaticAppProps {
|
||||||
pub route: Route,
|
pub route: Route,
|
||||||
pub head: HeadWriter,
|
pub head: HeadContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticAppProps {
|
impl StaticAppProps {
|
||||||
@ -88,10 +50,10 @@ pub fn static_app(props: &StaticAppProps) -> Html {
|
|||||||
let history = props.create_history();
|
let history = props.create_history();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<ContextProvider<HeadWriter> context={props.head.clone()}>
|
<ContextProvider<HeadContext> context={props.head.clone()}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</Router>
|
</Router>
|
||||||
</ContextProvider<HeadWriter>>
|
</ContextProvider<HeadContext>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,14 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use site::{
|
use site::{
|
||||||
app::{HeadWriter, StaticApp, StaticAppProps},
|
app::{StaticApp, StaticAppProps},
|
||||||
|
components::head::{HeadContext, HeadRender, HeadRenderProps},
|
||||||
pages::Route,
|
pages::Route,
|
||||||
};
|
};
|
||||||
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use time::format_description::well_known::{Rfc2822, Rfc3339};
|
use time::format_description::well_known::{Rfc2822, Rfc3339};
|
||||||
use yew::ServerRenderer;
|
use yew::LocalServerRenderer;
|
||||||
use yew_router::Routable;
|
use yew_router::Routable;
|
||||||
|
|
||||||
struct Template {
|
struct Template {
|
||||||
@ -24,8 +25,8 @@ impl Template {
|
|||||||
println!("Loading template from: {:?}", path.as_ref());
|
println!("Loading template from: {:?}", path.as_ref());
|
||||||
let content = tokio::fs::read_to_string(path).await?;
|
let content = tokio::fs::read_to_string(path).await?;
|
||||||
|
|
||||||
let Some(head_index) = content.find("</head>") else {
|
let Some(head_index) = content.find("<script id=\"head-ssg-after\"") else {
|
||||||
eprintln!("error: Failed to find index of '</head>' close tag in 'dist/index.html'");
|
eprintln!("error: Failed to find index of 'head-ssg-after' tag in 'dist/index.html'");
|
||||||
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Malformed index.html"));
|
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Malformed index.html"));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,17 +82,23 @@ impl Env {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn render_route(&self, route: Route) -> String {
|
async fn render_route(&self, route: Route) -> String {
|
||||||
let head = HeadWriter::default();
|
let head = HeadContext::default();
|
||||||
|
|
||||||
let render = {
|
let render = {
|
||||||
let head = head.clone();
|
let head = head.clone();
|
||||||
ServerRenderer::<StaticApp>::with_props(move || StaticAppProps { route, head })
|
LocalServerRenderer::<StaticApp>::with_props(StaticAppProps { route, head })
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
render.render_to_string(&mut body).await;
|
render.render_to_string(&mut body).await;
|
||||||
|
|
||||||
self.template.render(head.take(), body).await
|
let render =
|
||||||
|
LocalServerRenderer::<HeadRender>::with_props(HeadRenderProps { context: head });
|
||||||
|
|
||||||
|
let mut head = String::new();
|
||||||
|
render.render_to_string(&mut head).await;
|
||||||
|
|
||||||
|
self.template.render(head, body).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn write_str<P: AsRef<Path>>(&self, path: P, s: &str) -> std::io::Result<()> {
|
async fn write_str<P: AsRef<Path>>(&self, path: P, s: &str) -> std::io::Result<()> {
|
||||||
|
@ -1,7 +1,36 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use web_sys::HtmlHeadElement;
|
use web_sys::HtmlHeadElement;
|
||||||
use yew::{create_portal, function_component, html, use_state, Children, Html, Properties};
|
use yew::{
|
||||||
|
create_portal, function_component, html, use_context, use_state, Children, Html, Properties,
|
||||||
|
};
|
||||||
use yew_hooks::use_effect_once;
|
use yew_hooks::use_effect_once;
|
||||||
|
|
||||||
|
// Remove the elements inserted by SSG.
|
||||||
|
fn remove_ssg_elements(head: &HtmlHeadElement) {
|
||||||
|
let mut node = head.first_element_child();
|
||||||
|
let mut removing = false;
|
||||||
|
while let Some(child) = node {
|
||||||
|
let next = child.next_element_sibling();
|
||||||
|
|
||||||
|
let is_script = child.tag_name() == "SCRIPT";
|
||||||
|
|
||||||
|
if is_script && child.id() == "head-ssg-before" {
|
||||||
|
removing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if removing {
|
||||||
|
child.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_script && child.id() == "head-ssg-after" {
|
||||||
|
removing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct HeadProps {
|
pub struct HeadProps {
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
@ -12,11 +41,23 @@ pub struct HeadProps {
|
|||||||
pub fn head(props: &HeadProps) -> Html {
|
pub fn head(props: &HeadProps) -> Html {
|
||||||
let head = use_state(|| None::<HtmlHeadElement>);
|
let head = use_state(|| None::<HtmlHeadElement>);
|
||||||
|
|
||||||
|
if let Some(head_cxt) = use_context::<HeadContext>() {
|
||||||
|
head_cxt.append(html! {
|
||||||
|
<>{props.children.clone()}</>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let head = head.clone();
|
let head = head.clone();
|
||||||
use_effect_once(move || {
|
use_effect_once(move || {
|
||||||
let head_el = gloo::utils::head();
|
let head_el = gloo::utils::head();
|
||||||
|
|
||||||
|
// Remove the elements that were inserted into the <head> by the SSG.
|
||||||
|
remove_ssg_elements(&head_el);
|
||||||
|
|
||||||
|
// Store the <head> tag in the state.
|
||||||
head.set(Some(head_el));
|
head.set(Some(head_el));
|
||||||
|
|
||||||
|| ()
|
|| ()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -27,3 +68,50 @@ pub fn head(props: &HeadProps) -> Html {
|
|||||||
html! {}
|
html! {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct HeadContext {
|
||||||
|
content: Arc<Mutex<Vec<Html>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for HeadContext {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for HeadContext {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
content: Arc::clone(&self.content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeadContext {
|
||||||
|
pub fn take(&self) -> Vec<Html> {
|
||||||
|
let content = self.content.lock().unwrap();
|
||||||
|
content.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&self, html: Html) {
|
||||||
|
let mut content = self.content.lock().unwrap();
|
||||||
|
content.push(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct HeadRenderProps {
|
||||||
|
pub context: HeadContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(HeadRender)]
|
||||||
|
pub fn head_render(props: &HeadRenderProps) -> Html {
|
||||||
|
let content = props.context.take();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
{content}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,16 +11,6 @@ pub struct LdJsonProps {
|
|||||||
|
|
||||||
#[function_component(LdJson)]
|
#[function_component(LdJson)]
|
||||||
pub fn ld_json(props: &LdJsonProps) -> Html {
|
pub fn ld_json(props: &LdJsonProps) -> Html {
|
||||||
#[cfg(feature = "static")]
|
|
||||||
{
|
|
||||||
let head = use_context::<crate::app::HeadWriter>().expect("HeadContext to be provided");
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<script type=\"application/ld+json\">{}</script>",
|
|
||||||
props.json
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Head>
|
<Head>
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
@ -74,26 +64,6 @@ pub fn web_page_seo(props: &WebPageSeoProps) -> Html {
|
|||||||
"noindex,nofollow"
|
"noindex,nofollow"
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "static")]
|
|
||||||
{
|
|
||||||
let head = use_context::<crate::app::HeadWriter>().expect("HeadContext to be provided");
|
|
||||||
write!(head, "<meta name=\"robots\" content=\"{robots}\">");
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"og:title\" content=\"{}\">",
|
|
||||||
props.title
|
|
||||||
);
|
|
||||||
write!(head, "<meta property=\"og:url\" content=\"{url}\">");
|
|
||||||
|
|
||||||
if let Some(excerpt) = &props.excerpt {
|
|
||||||
write!(head, "<meta name=\"description\" content=\"{excerpt}\">");
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"org:description\" content=\"{excerpt}\">"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<LdJson json={(*json).clone()} />
|
<LdJson json={(*json).clone()} />
|
||||||
@ -196,54 +166,6 @@ pub fn blog_post_seo(props: &BlogPostSeoProps) -> Html {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "static")]
|
|
||||||
{
|
|
||||||
let head = use_context::<crate::app::HeadWriter>().expect("HeadWriter to be provided");
|
|
||||||
|
|
||||||
if let Some(excerpt) = &props.excerpt {
|
|
||||||
write!(head, "<meta name=\"description\" content=\"{excerpt}\">");
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"og:description\" content=\"{excerpt}\">"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(head, "<meta property=\"og:type\" content=\"article\">");
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"og:title\" content=\"{}\">",
|
|
||||||
props.title
|
|
||||||
);
|
|
||||||
write!(head, "<meta property=\"og:url\" content=\"{url}\">");
|
|
||||||
|
|
||||||
if let Some(published) = &published {
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"article:published_time\" content=\"{published}\">"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"article:author\" content=\"Blake Rain\">"
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(image) = &image {
|
|
||||||
write!(head, "<meta property=\"og:image\" content=\"{image}\" />");
|
|
||||||
write!(
|
|
||||||
head,
|
|
||||||
"<meta property=\"og:image:alt\" content=\"{}\" />",
|
|
||||||
props.title
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for tag in tags.iter() {
|
|
||||||
write!(head, "<meta property=\"article:tag\" content=\"{tag}\">");
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(head, "<link rel=\"canonical\" href=\"{url}\">");
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<LdJson json={(*json).clone()} />
|
<LdJson json={(*json).clone()} />
|
||||||
|
@ -9,12 +9,6 @@ pub struct TitleProps {
|
|||||||
|
|
||||||
#[function_component(Title)]
|
#[function_component(Title)]
|
||||||
pub fn title(props: &TitleProps) -> Html {
|
pub fn title(props: &TitleProps) -> Html {
|
||||||
#[cfg(feature = "static")]
|
|
||||||
{
|
|
||||||
let head = yew::use_context::<crate::app::HeadWriter>().expect("HeadWriter to be provided");
|
|
||||||
write!(head, "<title>{}</title>", props.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<Head>
|
<Head>
|
||||||
<title>{props.title.clone()}</title>
|
<title>{props.title.clone()}</title>
|
||||||
|
Loading…
Reference in New Issue
Block a user