Switch over to WebAssembly, Rust and Yew #35

Merged
BlakeRain merged 87 commits from yew-static into main 2023-08-30 18:01:40 +00:00
4 changed files with 245 additions and 1 deletions
Showing only changes of commit eb345e3208 - Show all commits

View File

@ -1,5 +1,7 @@
pub mod blog;
pub mod content;
pub mod head;
pub mod layout;
pub mod render;
pub mod seo;
pub mod title;

29
src/components/head.rs Normal file
View File

@ -0,0 +1,29 @@
use web_sys::HtmlHeadElement;
use yew::{create_portal, function_component, html, use_state, Children, Html, Properties};
use yew_hooks::use_effect_once;
#[derive(Properties, PartialEq)]
pub struct HeadProps {
#[prop_or_default]
pub children: Children,
}
#[function_component(Head)]
pub fn head(props: &HeadProps) -> Html {
let head = use_state(|| None::<HtmlHeadElement>);
{
let head = head.clone();
use_effect_once(move || {
let head_el = gloo::utils::head();
head.set(Some(head_el));
|| ()
})
}
if let Some(head) = &*head {
create_portal(html! { <>{props.children.clone()}</> }, head.clone().into())
} else {
html! {}
}
}

205
src/components/seo.rs Normal file
View File

@ -0,0 +1,205 @@
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use yew::{function_component, html, use_context, use_memo, Children, Html, Properties};
use yew_router::Routable;
use crate::{components::head::Head, model::TagsContext, pages::Route};
#[derive(Properties, PartialEq)]
pub struct LdJsonProps {
#[prop_or_default]
pub children: Children,
}
#[function_component(LdJson)]
pub fn ld_json(props: &LdJsonProps) -> Html {
html! {
<Head>
<script type="application/ld+json">
{props.children.clone()}
</script>
</Head>
}
}
#[derive(Properties, PartialEq)]
pub struct WebPageSeoProps {
pub route: Route,
pub title: String,
pub excerpt: Option<String>,
#[prop_or_default]
pub index: bool,
#[prop_or_default]
pub follow: bool,
}
#[function_component(WebPageSeo)]
pub fn web_page_seo(props: &WebPageSeoProps) -> Html {
let json = use_memo(
|(title, excerpt)| {
serde_json::json!({
"@context": "https://schema.org",
"@type": "WebPage",
"name": title,
"description": excerpt,
"publisher": {
"@type": "ProfilePage",
"name": "Blake Rain's Website"
}
})
.to_string()
},
(props.title.clone(), props.excerpt.clone()),
);
let url = format!("https://blakerain.com{}", props.route.to_path());
let robots = if props.index {
if props.follow {
"index,follow"
} else {
"index,nofollow"
}
} else if props.follow {
"noindex,follow"
} else {
"noindex,nofollow"
};
html! {
<>
<LdJson>{json}</LdJson>
<Head>
<meta name="robots" content={robots} />
if let Some(excerpt) = &props.excerpt {
<>
<meta name="description" content={excerpt.clone()} />
<meta property="og:description" content={excerpt.clone()} />
</>
}
<meta property="og:title" content={props.title.clone()} />
<meta property="og:url" content={url} />
</Head>
</>
}
}
#[derive(Properties, PartialEq)]
pub struct BlogPostSeoProps {
pub route: Route,
pub image: Option<String>,
pub title: String,
pub excerpt: Option<String>,
pub published: Option<OffsetDateTime>,
pub tags: Vec<String>,
}
#[function_component(BlogPostSeo)]
pub fn blog_post_seo(props: &BlogPostSeoProps) -> Html {
let tags = use_context::<TagsContext>().expect("TagsCOntext to be provided");
let tags = props
.tags
.iter()
.filter_map(|tag| tags.get(tag).map(|tag| tag.name.clone()))
.collect::<Vec<_>>();
let url = format!("https://blakerain.com{}", props.route.to_path());
let image = props
.image
.clone()
.map(|image| format!("https://blakerain.com{image}"));
let published = props
.published
.map(|time| time.format(&Rfc3339).expect("time format"));
let json = use_memo(
|(title, excerpt, image, url, published, tags)| {
serde_json::json!({
"@context": "https://schema.org",
"@type": "BlogPosting",
"image": image,
"url": url,
"headline": title,
"alternativeHeadline": excerpt,
"dateCreated": published,
"datePublished": published,
"dateModified": published,
"inLanguage": "en-GB",
"isFamilyFriendly": "true",
"keywords": tags,
"accountablePerson": {
"@type": "Person",
"name": "Blake Rain",
"url": "https://blakerain.com"
},
"author": {
"@type": "Person",
"name": "Blake Rain",
"url": "https://blakerain.com"
},
"creator": {
"@type": "Person",
"name": "Blake Rain",
"url": "https://blakerain.com"
},
"publisher": {
"@type": "Organisation",
"name": "Blake Rain",
"url": "https://blakerain.com",
"logo": {
"@type": "ImageObject",
"url": "https://blakerain.com/media/logo-text.png",
"width": "300",
"height": "56",
}
},
})
.to_string()
},
(
props.title.clone(),
props.excerpt.clone(),
image.clone(),
url.clone(),
published.clone(),
tags.clone(),
),
);
html! {
<>
<LdJson>{json}</LdJson>
<Head>
if let Some(excerpt) = &props.excerpt {
<>
<meta name="description" content={excerpt.clone()} />
<meta property="og:description" content={excerpt.clone()} />
</>
}
<meta property="og:type" content="article" />
<meta property="og:title" content={props.title.clone()} />
<meta property="og:url" content={url.clone()} />
if let Some(image) = &image {
<>
<meta property="og:image" content={image.clone()} />
<meta property="og:image:alt" content={props.title.clone()} />
</>
}
<meta property="article:published_time" content={published} />
<meta property="article:author" content="Blake Rain" />
{
for tags.iter().map(|tag| html! {
<meta property="article:tag" content={tag.clone()} />
})
}
<link rel="canonical" href={url} />
</Head>
</>
}
}

View File

@ -2,11 +2,12 @@
use yew::{function_component, html, Html, Properties};
use crate::{
components::{content::PostContent, layout::goto_top::GotoTop, title::Title},
components::{content::PostContent, layout::goto_top::GotoTop, seo::BlogPostSeo, title::Title},
model::{
blog::{render, DocId},
ProvideTags,
},
pages::Route,
};
#[derive(Properties, PartialEq)]
@ -30,6 +31,13 @@ pub fn page(props: &PageProps) -> Html {
html! {
<ProvideTags>
<Title title={details.summary.title.clone()} />
<BlogPostSeo
route={Route::BlogPost { doc_id: props.doc_id }}
image={details.cover_image.clone()}
title={details.summary.title.clone()}
excerpt={details.summary.excerpt.clone()}
published={details.summary.published}
tags={details.tags.clone()} />
<PostContent<DocId> details={details} content={content} />
<GotoTop />
</ProvideTags>