Switch over to WebAssembly, Rust and Yew #35
@ -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
29
src/components/head.rs
Normal 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
205
src/components/seo.rs
Normal 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>
|
||||
</>
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user