Switch over to WebAssembly, Rust and Yew #35
86
src/app.rs
86
src/app.rs
@ -1,15 +1,11 @@
|
|||||||
use yew::{function_component, html, Html, Properties};
|
use std::sync::{Arc, Mutex};
|
||||||
use yew_router::Switch;
|
|
||||||
|
|
||||||
#[cfg(feature = "static")]
|
use yew::{function_component, html, ContextProvider, Html, Properties};
|
||||||
use yew_router::{
|
use yew_router::{
|
||||||
history::{AnyHistory, History, MemoryHistory},
|
history::{AnyHistory, History, MemoryHistory},
|
||||||
Router,
|
BrowserRouter, Router, Switch,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "static"))]
|
|
||||||
use yew_router::BrowserRouter;
|
|
||||||
|
|
||||||
use crate::{components::layout::Layout, pages::Route};
|
use crate::{components::layout::Layout, pages::Route};
|
||||||
|
|
||||||
#[function_component(AppContent)]
|
#[function_component(AppContent)]
|
||||||
@ -23,32 +19,72 @@ fn app_content() -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Default, Properties, PartialEq)]
|
||||||
#[cfg_attr(not(feature = "static"), derive(Default))]
|
pub struct AppProps {}
|
||||||
pub struct AppProps {
|
|
||||||
#[cfg(feature = "static")]
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[function_component(App)]
|
#[function_component(App)]
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub fn app(props: &AppProps) -> Html {
|
pub fn app(props: &AppProps) -> Html {
|
||||||
#[cfg(feature = "static")]
|
let head = HeadWriter::default();
|
||||||
{
|
|
||||||
log::info!("Application is running in static mode");
|
|
||||||
let history = AnyHistory::from(MemoryHistory::default());
|
|
||||||
history.push(&props.url);
|
|
||||||
html! {
|
|
||||||
<Router history={history}>
|
|
||||||
<AppContent />
|
|
||||||
</Router>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "static"))]
|
|
||||||
html! {
|
html! {
|
||||||
|
<ContextProvider<HeadWriter> context={head}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AppContent />
|
<AppContent />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</ContextProvider<HeadWriter>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)]
|
||||||
|
pub struct StaticAppProps {
|
||||||
|
pub url: String,
|
||||||
|
pub head: HeadWriter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(StaticApp)]
|
||||||
|
pub fn static_app(props: &StaticAppProps) -> Html {
|
||||||
|
let history = AnyHistory::from(MemoryHistory::default());
|
||||||
|
history.push(&props.url);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<ContextProvider<HeadWriter> context={props.head.clone()}>
|
||||||
|
<Router history={history}>
|
||||||
|
<AppContent />
|
||||||
|
</Router>
|
||||||
|
</ContextProvider<HeadWriter>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use site::{
|
use site::{
|
||||||
app::{App, AppProps},
|
app::{HeadWriter, StaticApp, StaticAppProps},
|
||||||
pages::Route,
|
pages::Route,
|
||||||
};
|
};
|
||||||
|
|
||||||
use yew::ServerRenderer;
|
use yew::ServerRenderer;
|
||||||
use yew_router::Routable;
|
use yew_router::Routable;
|
||||||
|
|
||||||
struct Template {
|
struct Template {
|
||||||
content: String,
|
content: String,
|
||||||
index: usize,
|
head_index: usize,
|
||||||
|
body_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template {
|
impl Template {
|
||||||
@ -17,19 +19,30 @@ 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(index) = content.find("</body>") else {
|
let Some(head_index) = content.find("</head>") else {
|
||||||
|
eprintln!("error: Failed to find index of '</head>' close tag in 'dist/index.html'");
|
||||||
|
return Err(std::io::Error::new(std::io::ErrorKind::Other, "Malformed index.html"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(body_index) = content.find("</body>") else {
|
||||||
eprintln!("error: Failed to find index of '</body>' close tag in 'dist/index.html'");
|
eprintln!("error: Failed to find index of '</body>' close 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"));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self { content, index })
|
Ok(Self {
|
||||||
|
content,
|
||||||
|
head_index,
|
||||||
|
body_index,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render(&self, app: ServerRenderer<App>) -> String {
|
async fn render(&self, head: String, body: String) -> String {
|
||||||
let mut result = String::with_capacity(self.content.len());
|
let mut result = String::with_capacity(self.content.len());
|
||||||
result.push_str(&self.content[..self.index]);
|
result.push_str(&self.content[..self.head_index]);
|
||||||
app.render_to_string(&mut result).await;
|
result.push_str(&head);
|
||||||
result.push_str(&self.content[self.index..]);
|
result.push_str(&self.content[self.head_index..self.body_index]);
|
||||||
|
result.push_str(&body);
|
||||||
|
result.push_str(&self.content[self.body_index..]);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,8 +76,17 @@ impl Env {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn render_route(&self, url: String) -> String {
|
async fn render_route(&self, url: String) -> String {
|
||||||
let render = ServerRenderer::<App>::with_props(move || AppProps { url });
|
let head = HeadWriter::default();
|
||||||
self.template.render(render).await
|
|
||||||
|
let render = {
|
||||||
|
let head = head.clone();
|
||||||
|
ServerRenderer::<StaticApp>::with_props(move || StaticAppProps { url, head })
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
render.render_to_string(&mut body).await;
|
||||||
|
|
||||||
|
self.template.render(head.take(), 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,21 +1,30 @@
|
|||||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||||
use yew::{function_component, html, use_context, use_memo, Children, Html, Properties};
|
use yew::{function_component, html, use_context, use_memo, Html, Properties};
|
||||||
use yew_router::Routable;
|
use yew_router::Routable;
|
||||||
|
|
||||||
use crate::{components::head::Head, model::TagsContext, pages::Route};
|
use crate::{components::head::Head, model::TagsContext, pages::Route};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct LdJsonProps {
|
pub struct LdJsonProps {
|
||||||
#[prop_or_default]
|
pub json: String,
|
||||||
pub children: Children,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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">
|
||||||
{props.children.clone()}
|
{props.json.clone()}
|
||||||
</script>
|
</script>
|
||||||
</Head>
|
</Head>
|
||||||
}
|
}
|
||||||
@ -65,9 +74,29 @@ 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}</LdJson>
|
<LdJson json={(*json).clone()} />
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="robots" content={robots} />
|
<meta name="robots" content={robots} />
|
||||||
|
|
||||||
@ -167,9 +196,57 @@ 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}</LdJson>
|
<LdJson json={(*json).clone()} />
|
||||||
<Head>
|
<Head>
|
||||||
if let Some(excerpt) = &props.excerpt {
|
if let Some(excerpt) = &props.excerpt {
|
||||||
<>
|
<>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use yew::{function_component, html, use_effect_with_deps, Html, Properties};
|
use yew::{function_component, html, Html, Properties};
|
||||||
|
|
||||||
|
use crate::components::head::Head;
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct TitleProps {
|
pub struct TitleProps {
|
||||||
@ -7,20 +9,15 @@ pub struct TitleProps {
|
|||||||
|
|
||||||
#[function_component(Title)]
|
#[function_component(Title)]
|
||||||
pub fn title(props: &TitleProps) -> Html {
|
pub fn title(props: &TitleProps) -> Html {
|
||||||
let title = props.title.clone();
|
#[cfg(feature = "static")]
|
||||||
|
{
|
||||||
|
let head = yew::use_context::<crate::app::HeadWriter>().expect("HeadWriter to be provided");
|
||||||
|
write!(head, "<title>{}</title>", props.title);
|
||||||
|
}
|
||||||
|
|
||||||
use_effect_with_deps(
|
html! {
|
||||||
move |_| {
|
<Head>
|
||||||
let head_el = gloo::utils::head();
|
<title>{props.title.clone()}</title>
|
||||||
let title_el = head_el
|
</Head>
|
||||||
.query_selector("title")
|
}
|
||||||
.expect("query_selector")
|
|
||||||
.expect("title");
|
|
||||||
|
|
||||||
title_el.set_text_content(Some(&title));
|
|
||||||
},
|
|
||||||
props.title.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
html! {}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
// pub mod source;
|
|
||||||
|
|
||||||
use std::{collections::HashMap, rc::Rc};
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use model::{document::Details, tag::Tag};
|
use model::{document::Details, tag::Tag};
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use yew::{function_component, html, Html};
|
use yew::{function_component, html, Html};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{content::PostContent, title::Title},
|
components::{content::PostContent, seo::WebPageSeo, title::Title},
|
||||||
model::{
|
model::{
|
||||||
pages::{render, DocId},
|
pages::{render, DocId},
|
||||||
ProvideTags,
|
ProvideTags,
|
||||||
},
|
},
|
||||||
|
pages::Route,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
@ -17,6 +18,12 @@ pub fn page() -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<ProvideTags>
|
<ProvideTags>
|
||||||
<Title title={details.summary.title.clone()} />
|
<Title title={details.summary.title.clone()} />
|
||||||
|
<WebPageSeo
|
||||||
|
route={Route::About}
|
||||||
|
title={details.summary.title.clone()}
|
||||||
|
excerpt={details.summary.excerpt.clone()}
|
||||||
|
index={true}
|
||||||
|
follow={true} />
|
||||||
<PostContent<DocId> details={details.clone()} content={content} />
|
<PostContent<DocId> details={details.clone()} content={content} />
|
||||||
</ProvideTags>
|
</ProvideTags>
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use yew::{function_component, html, Html};
|
use yew::{function_component, html, Html};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{content::PostContent, title::Title},
|
components::{content::PostContent, seo::WebPageSeo, title::Title},
|
||||||
model::{
|
model::{
|
||||||
pages::{render, DocId},
|
pages::{render, DocId},
|
||||||
ProvideTags,
|
ProvideTags,
|
||||||
},
|
},
|
||||||
|
pages::Route,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
@ -17,6 +18,12 @@ pub fn page() -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<ProvideTags>
|
<ProvideTags>
|
||||||
<Title title={details.summary.title.clone()} />
|
<Title title={details.summary.title.clone()} />
|
||||||
|
<WebPageSeo
|
||||||
|
route={Route::About}
|
||||||
|
title={details.summary.title.clone()}
|
||||||
|
excerpt={details.summary.excerpt.clone()}
|
||||||
|
index={false}
|
||||||
|
follow={false} />
|
||||||
<PostContent<DocId> details={details} content={content} />
|
<PostContent<DocId> details={details} content={content} />
|
||||||
</ProvideTags>
|
</ProvideTags>
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user