Switch over to WebAssembly, Rust and Yew #35
@ -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
|
||||
|
@ -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"
|
||||
```
|
||||
|
||||
|
@ -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 |
@ -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!(),
|
||||
|
101
style/main.css
101
style/main.css
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user