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
6 changed files with 291 additions and 111 deletions
Showing only changes of commit 32c868dcfa - Show all commits

View File

@ -21,14 +21,17 @@ November, Aleksandr wrote about the performance of [x86 vs
ARM](https://filia-aleks.medium.com/aws-lambda-battle-x86-vs-arm-graviton2-perfromance-3581aaef75d9)
on AWS Lambda, which again showed Rust to be quite performant, and even more so on ARM.
<Bookmark
url="https://github.com/awslabs/aws-lambda-rust-runtime"
title="GitHub - awslabs/aws-lambda-rust-runtime: A Rust runtime for AWS Lambda"
description="A Rust runtime for AWS Lambda. Contribute to awslabs/aws-lambda-rust-runtime development by creating an account on GitHub."
author="awslabs"
publisher="GitHub"
thumbnail="https://opengraph.githubassets.com/e6c849253e37fbc1db7ae49d6368cc42988843123134001207eff64f7c470c9f/awslabs/aws-lambda-rust-runtime"
icon="https://github.com/fluidicon.png" />
```bookmark
url: "https://github.com/awslabs/aws-lambda-rust-runtime"
title: "GitHub - awslabs/aws-lambda-rust-runtime: A Rust runtime for AWS Lambda"
description: |
A Rust runtime for AWS Lambda. Contribute to awslabs/aws-lambda-rust-runtime development by
creating an account on GitHub.
author: awslabs
publisher: GitHub
thumbnail: "https://opengraph.githubassets.com/e6c849253e37fbc1db7ae49d6368cc42988843123134001207eff64f7c470c9f/awslabs/aws-lambda-rust-runtime"
icon: "https://github.com/fluidicon.png"
```
## Site Analytics
@ -60,14 +63,18 @@ crate. This seemed to only arise when I compiled in Docker on the M1 when target
support for ARM in Lambda is quite recent, none of the AWS Lambda Rust build images I looked at
currently seemed to support it.
<Bookmark
url="https://github.com/softprops/lambda-rust"
title="GitHub - softprops/lambda-rust: 🐳 🦀 a dockerized lambda build env for rust applications"
description="This docker image extends lambda ci provided.al2 builder docker image, a faithful reproduction of the actual AWS 'provided.al2' Lambda runtime environment, and installs rustup and the stable rust toolchain."
author="awslabs"
publisher="GitHub"
thumbnail="https://opengraph.githubassets.com/31c9066c430630fe306c04d47e6ef314b5395bc6ce40867c9d890c2e5e13e21a/softprops/lambda-rust"
icon="https://github.com/fluidicon.png" />
```bookmark
url: "https://github.com/softprops/lambda-rust"
title: "GitHub - softprops/lambda-rust: 🐳 🦀 a dockerized lambda build env for rust applications"
description: |
This docker image extends lambda ci provided.al2 builder docker image, a faithful reproduction of
the actual AWS 'provided.al2' Lambda runtime environment, and installs rustup and the stable rust
toolchain.
author: awslabs
publisher: GitHub
thumbnail: "https://opengraph.githubassets.com/31c9066c430630fe306c04d47e6ef314b5395bc6ce40867c9d890c2e5e13e21a/softprops/lambda-rust"
icon: "https://github.com/fluidicon.png"
```
I'm sure that at some point they will support ARM, but for the time being it was necessary for me
to create a [Dockerfile] that used the `al2-arm64` image provided by AWS in the [ECR]. This

View File

@ -24,11 +24,11 @@ the time of writing, he has already amassed 51k followers.
```bookmark
url: "https://mastodonapp.uk/@stephenfry"
title: @stephenfry@mastodonapp.uk
title: "@stephenfry@mastodonapp.uk"
description: Stephen Fry on Mastodon
author: Stephen Fry
publisher: Mastodon
thumbnail: /content/moving-to-mastodon/fry-mastodon.jpg
thumbnail: "/content/moving-to-mastodon/fry-mastodon.jpg"
icon: "https://cdn.simpleicons.org/mastodon/6364FF"
```

View File

@ -41,14 +41,17 @@ There was one issue I had that ended up taking some time to remediate: the chang
In order to render the site I decided to use React Static: a static site generator for React. I chose this approach over other [much easier options](https://ghost.org/docs/jamstack/) as I wanted to move away from Ghost themes and I really enjoy using React :)
<Bookmark
url="https://github.com/react-static/react-static"
title="GitHub - react-static/react-static: ⚛️ 🚀 A progressive static site generator for React."
description="⚛️ 🚀 A progressive static site generator for React. - GitHub - react-static/react-static: ⚛️ 🚀 A progressive static site generator for React."
author="react-static"
publisher="GitHub"
thumbnail="https://repository-images.githubusercontent.com/102987907/733d9200-6288-11e9-9f58-538c156753f8"
icon="https://github.com/fluidicon.png" />
```bookmark
url: "https://github.com/react-static/react-static"
title: "GitHub - react-static/react-static: ⚛️ 🚀 A progressive static site generator for React."
description: |
⚛️ 🚀 A progressive static site generator for React. - GitHub - react-static/react-static:
⚛️ 🚀 A progressive static site generator for React.
author: react-static
publisher: GitHub
thumbnail: "https://repository-images.githubusercontent.com/102987907/733d9200-6288-11e9-9f58-538c156753f8"
icon: "https://github.com/fluidicon.png"
```
I used the Ghost [Content API](https://ghost.org/docs/content-api/) to extract the navigation, posts, and pages. I then render them using React. The site is a very simple React application, with only a few components.
@ -73,14 +76,15 @@ In order to achieve this I created a new Docker container on the same machine. T
As a security precaution, GitHub encourage you to not attach a self-hosted runner to a public repository. Therefore it was necessary for me to create a private repository which contains the workflow for building and deploying the site. As the repository is private, I have reproduced the workflow as a Gist:
<Bookmark
url="https://gist.github.com/BlakeRain/cae8edfa273d7603d25e5527c6821984"
title="Workflow to build and deploy the static blakerain.com"
description="Workflow to build and deploy the static blakerain.com - deploy.yml"
author="262588213843476"
publisher="Gist"
thumbnail="https://github.githubassets.com/images/modules/gists/gist-og-image.png"
icon="https://gist.github.com/fluidicon.png" />
```bookmark
url: "https://gist.github.com/BlakeRain/cae8edfa273d7603d25e5527c6821984"
title: "Workflow to build and deploy the static blakerain.com"
description: "Workflow to build and deploy the static blakerain.com - deploy.yml"
author: Blake Rain
publisher: GitHub Gist
thumbnail: "https://github.githubassets.com/images/modules/gists/gist-og-image.png"
icon: "https://gist.github.com/fluidicon.png"
```
The final piece of the puzzle was to connect Ghost to GitHub: when I make a change to the site I wanted the GitHub workflow to execute. As the GitHub API requires authentication, I created a small [lambda function](https://github.com/BlakeRain/blakerain.com/blob/main/lambda/ghost-post-actions/index.js). This function processes the POST request from the Ghost CMS [webhook](https://ghost.org/docs/webhooks/) and in turn makes a call to the GitHub API to trigger a [workflow dispatch event](https://docs.github.com/en/rest/reference/actions#create-a-workflow-dispatch-event).
@ -90,11 +94,14 @@ Now that I have a static version of the site, hosted for free at Netlify, I'm su
As before, all the sources for the site are available on GitHub. This includes the cobbled together bits and pieces for the S3 storage adapter and the GitHub Actions Runner Docker image.
<Bookmark
url="https://github.com/BlakeRain/blakerain.com"
title="GitHub - BlakeRain/blakerain.com: Repository for the static generator for my blog"
description="Repository for the static generator for my blog. Contribute to BlakeRain/blakerain.com development by creating an account on GitHub."
author="BlakeRain"
publisher="GitHub"
thumbnail="https://repository-images.githubusercontent.com/155570276/9e808f00-3b06-11eb-913d-44e30d832a70"
icon="https://github.com/fluidicon.png" />
```bookmark
url: "https://github.com/BlakeRain/blakerain.com"
title: "GitHub - BlakeRain/blakerain.com: Repository for the static generator for my blog"
description: |
Repository for the static generator for my blog. Contribute to BlakeRain/blakerain.com development
by creating an account on GitHub.
author: BlakeRain
publisher: GitHub
thumbnail: "https://repository-images.githubusercontent.com/155570276/9e808f00-3b06-11eb-913d-44e30d832a70"
icon: "https://github.com/fluidicon.png"
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -8,12 +8,13 @@ use yew::{
virtual_dom::{VList, VNode, VTag, VText},
Html,
};
use yew_icons::{Icon, IconId};
#[derive(Deserialize)]
struct BookmarkDecl {
url: String,
title: String,
description: String,
description: Option<String>,
author: String,
publisher: Option<String>,
thumbnail: Option<String>,
@ -23,10 +24,43 @@ struct BookmarkDecl {
impl BookmarkDecl {
fn generate(self) -> VNode {
html! {
<figure>
<a href={self.url}>
<div>
<h1>{self.title}</h1>
<figure class="w-full text-base">
<a href={self.url}
class="plain w-full flex flex-col lg:flex-row rounded-md shadow-md min-h-[148px] border border-neutral-300 dark:border-neutral-700">
if let Some(thumbnail) = self.thumbnail {
<div class="relative lg:order-2 min-w-[33%] min-h-[160px] lg:min-h-fit max-h-[100%]">
<img
class="absolute top-0 left-0 w-full h-full rounded-r-md object-cover"
src={thumbnail}
alt={self.title.clone()}
loading="lazy"
decoding="async" />
</div>
}
<div class="font-sans ld:order-1 grow flex flex-col justify-start align-start p-5">
<div class="font-semibold">{self.title}</div>
if let Some(description) = self.description {
<div class="grow overflow-y-hidden mt-3 max-h-12">{description}</div>
}
<div class="flex flex-row flex-wrap align-center gap-1 mt-3.5">
if let Some(icon) = self.icon {
<img
class="w-[18px] h-[18px] lg:w-[22px] lg:h-[22px] mr-2"
alt={self.publisher.clone()}
src={icon} />
}
if let Some(publisher) = self.publisher {
<span>{publisher}</span>
if !self.author.is_empty() {
<Icon icon_id={IconId::BootstrapDot} />
}
}
if !self.author.is_empty() {
<span>{self.author}</span>
}
</div>
</div>
</a>
</figure>
@ -34,8 +68,39 @@ impl BookmarkDecl {
}
}
#[derive(Deserialize)]
struct QuoteDecl {
quote: String,
author: Option<String>,
url: Option<String>,
}
impl QuoteDecl {
fn generate(self) -> VNode {
let cite = self.author.map(|author| {
self.url
.map(|url| {
html! {
<cite>
<a href={url} target="_blank" rel="noreferrer">{author.clone()}</a>
</cite>
}
})
.unwrap_or_else(|| html! { <cite>{author.clone()}</cite> })
});
html! {
<figure class="quote">
<p>{self.quote}</p>
{cite}
</figure>
}
}
}
enum GeneratorBlock {
Bookmark(BookmarkDecl),
Quote(QuoteDecl),
}
impl GeneratorBlock {
@ -46,9 +111,17 @@ impl GeneratorBlock {
Self::Bookmark(decl)
}
fn new_quote(content: String) -> Self {
let decl = gray_matter::engine::YAML::parse(&content)
.deserialize()
.expect("QuoteDecl");
Self::Quote(decl)
}
fn generate(self) -> VNode {
match self {
Self::Bookmark(decl) => decl.generate(),
Self::Quote(decl) => decl.generate(),
}
}
}
@ -61,6 +134,8 @@ impl FromStr for Generator {
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "bookmark" {
Ok(Generator(Box::new(GeneratorBlock::new_bookmark)))
} else if s == "quote" {
Ok(Generator(Box::new(GeneratorBlock::new_quote)))
} else {
Err(())
}
@ -70,7 +145,7 @@ impl FromStr for Generator {
struct Writer<'a, I> {
tokens: I,
output: Vec<VNode>,
stack: Vec<VTag>,
stack: Vec<VNode>,
footnotes: HashMap<CowStr<'a>, usize>,
}
@ -87,9 +162,17 @@ where
}
}
fn output(&mut self, node: VNode) {
if let Some(VNode::VTag(top)) = self.stack.last_mut() {
top.add_child(node);
} else {
self.output.push(node);
}
}
fn start_tag(&mut self, tag: Tag) {
match tag {
Tag::Paragraph => self.stack.push(VTag::new("p")),
Tag::Paragraph => self.stack.push(VTag::new("p").into()),
Tag::Heading(level, ident, classes) => {
let mut tag = VTag::new(match level {
@ -109,33 +192,41 @@ where
tag.add_attribute("class", classes.join(" "));
}
self.stack.push(tag);
self.stack.push(tag.into());
}
Tag::BlockQuote => self.stack.push(VTag::new("blockquote")),
Tag::BlockQuote => self.stack.push(VTag::new("blockquote").into()),
Tag::CodeBlock(kind) => {
if let CodeBlockKind::Fenced(language) = &kind {
if let Ok(generator) = language.parse::<Generator>() {
if let Ok(content) = self.raw_text() {
let block = (generator.0)(content);
let tag = block.generate();
self.stack.push(tag);
self.output((generator.0)(content).generate());
return;
}
}
}
let mut pre = VTag::new("pre");
let mut code = VTag::new("code");
if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
code.add_attribute("class", format!("lang-{language}"));
}
}
if let Ok(content) = self.raw_text() {
let mut figure = VTag::new("figure");
figure.add_attribute("class", "code");
pre.add_child(code.into());
self.stack.push(pre);
let mut pre = VTag::new("pre");
let mut code = VTag::new("code");
if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
code.add_attribute("class", format!("lang-{language}"));
}
}
code.add_child(VText::new(content).into());
pre.add_child(code.into());
figure.add_child(pre.into());
self.output(figure.into());
} else {
self.stack.push(VTag::new("pre").into());
}
}
Tag::List(ordered) => {
@ -144,24 +235,27 @@ where
tag.add_attribute("start", start.to_string());
}
self.stack.push(tag);
self.stack.push(tag.into());
}
Tag::Item => self.stack.push(VTag::new("li")),
Tag::Item => self.stack.push(VTag::new("li").into()),
Tag::FootnoteDefinition(_) => todo!(),
Tag::Table(_) => todo!(),
Tag::TableHead => todo!(),
Tag::TableRow => todo!(),
Tag::TableCell => todo!(),
Tag::Emphasis => self.stack.push(VTag::new("em")),
Tag::Strong => self.stack.push(VTag::new("strong")),
Tag::Strikethrough => self.stack.push(VTag::new("s")),
Tag::Emphasis => self.stack.push(VTag::new("em").into()),
Tag::Strong => self.stack.push(VTag::new("strong").into()),
Tag::Strikethrough => self.stack.push(VTag::new("s").into()),
Tag::Link(_, href, title) => {
let mut anchor = VTag::new("a");
anchor.add_attribute("href", href.to_string());
anchor.add_attribute("title", title.to_string());
self.stack.push(anchor);
self.stack.push(anchor.into());
}
Tag::Image(_, href, title) => {
@ -184,7 +278,10 @@ where
let top_p = self
.stack
.last()
.map(|top| top.tag() == "p")
.map(|top| match top {
VNode::VTag(tag) => tag.tag() == "p",
_ => false,
})
.unwrap_or_default();
if top_p {
self.stack.pop();
@ -201,25 +298,17 @@ where
// Put the <figure> onto the stack. This will be popped by the `Event::End` for the
// `<p>` tag that Markdown likes to wrap around images.
self.stack.push(figure);
self.stack.push(figure.into());
}
}
}
fn end_tag(&mut self, tag: Tag) {
let element = self
.stack
.pop()
.unwrap_or_else(|| {
panic!("Expected stack to have an element at end of tag: {tag:?}");
})
.into();
let element = self.stack.pop().unwrap_or_else(|| {
panic!("Expected stack to have an element at end of tag: {tag:?}");
});
if let Some(top) = self.stack.last_mut() {
top.add_child(element);
} else {
self.output.push(element);
}
self.output(element);
}
fn raw_text(&mut self) -> Result<String, std::fmt::Error> {
@ -229,8 +318,7 @@ where
for event in self.tokens.by_ref() {
match event {
Event::Start(_) => nest += 1,
Event::End(tag) => {
log::info!("raw_text event.end: {tag:?}");
Event::End(_) => {
if nest == 0 {
break;
}
@ -260,40 +348,25 @@ where
fn run(mut self) -> Html {
while let Some(event) = self.tokens.next() {
log::info!("{event:?}");
match event {
Event::Start(tag) => self.start_tag(tag),
Event::End(tag) => self.end_tag(tag),
Event::Text(text) => {
if let Some(top) = self.stack.last_mut() {
let text = VText::new(text.to_string());
top.add_child(text.into());
}
}
Event::Text(text) => self.output(VText::new(text.to_string()).into()),
Event::Code(text) => {
if let Some(top) = self.stack.last_mut() {
let text = VText::new(text.to_string());
let mut code = VTag::new("code");
code.add_child(text.into());
top.add_child(code.into());
}
let text = VText::new(text.to_string());
let mut code = VTag::new("code");
code.add_child(text.into());
self.output(code.into());
}
Event::Html(_) => {
log::info!("Ignoring html: {event:?}")
}
Event::FootnoteReference(_) => todo!(),
Event::SoftBreak => {
if let Some(top) = self.stack.last_mut() {
let text = VText::new("\n");
top.add_child(text.into());
}
}
Event::SoftBreak => self.output(VText::new("\n").into()),
Event::HardBreak => {}
Event::Rule => {}
Event::TaskListMarker(_) => todo!(),

View File

@ -7,20 +7,113 @@
}
.markdown {
@apply flex flex-col;
@apply font-text text-xl;
@apply text-neutral-800 dark:text-neutral-300;
p {
@apply mb-6 leading-relaxed;
h1,
h2,
h3,
h4,
h5,
h6 {
@apply w-full font-sans;
}
a {
h1,
h2,
h3,
h4 {
+ figure {
@apply mt-3;
}
}
h1 {
@apply text-5xl font-bold mt-8 mb-4;
}
h2 {
@apply text-4xl font-bold mt-8 mb-4;
}
h3 {
@apply text-3xl font-bold mt-6 mb-3;
}
h4 {
@apply text-2xl font-semibold mt-6 mb-2;
}
h6 {
@apply text-xl font-semibold mt-4 mb-2;
}
p {
@apply w-full mb-8 leading-relaxed;
}
a:not(.plain) {
@apply underline text-blue-500 dark:text-blue-200;
@apply hover:text-blue-600 dark:hover:text-blue-300;
}
dl,
ul,
ol {
@apply w-full self-start mb-6;
li {
@apply break-words mt-2.5 pl-1.5;
&:first-child {
@apply mt-0;
}
}
}
ol {
@apply list-decimal pl-10;
}
ul {
@apply list-disc;
}
figure {
@apply mb-6 flex flex-col items-center;
@apply w-full mb-6 flex flex-col items-center;
pre {
@apply w-full;
}
&.quote {
@apply relative items-start;
&:before {
position: absolute;
top: 24px;
left: -44px;
content: "“";
font-size: 6rem;
@apply text-neutral-400 dark:text-neutral-700;
@apply hidden md:block;
}
p {
@apply italic mb-0;
@apply text-neutral-600 dark:text-neutral-500;
}
cite {
&:before {
content: "— ";
}
@apply font-sans ml-4;
}
}
}
figcaption {