Merge pull request #4 from JasterV/about-interfaces

[Post] Decoupling Elixir GenServers with Phoenix PubSub
This commit is contained in:
Víctor Martínez 2025-06-05 22:12:16 +02:00 committed by GitHub
commit 45af154c14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 597 additions and 48 deletions

View file

@ -13,6 +13,11 @@ jobs:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v4
# https://d2lang.com/tour/install/
- name: Install D2
run: |
curl -fsSL https://d2lang.com/install.sh | sh -s --
- name: Install bun - name: Install bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:

View file

@ -21,6 +21,11 @@ jobs:
- name: 🚚 Get latest code - name: 🚚 Get latest code
uses: actions/checkout@v4 uses: actions/checkout@v4
# https://d2lang.com/tour/install/
- name: Install D2
run: |
curl -fsSL https://d2lang.com/install.sh | sh -s --
- name: Install bun - name: Install bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:

3
.gitignore vendored
View file

@ -19,3 +19,6 @@ pnpm-debug.log*
.env .env
.env.production .env.production
# Ignore automatic generated diagrams from D2
public/assets/d2/

View file

@ -1,19 +1,38 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import icon from "astro-icon"; import icon from "astro-icon";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import d2 from "astro-d2";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://jaster.xyz", site: "https://jaster.xyz",
markdown: {
shikiConfig: {
theme: "dracula",
wrap: false,
},
},
integrations: [ integrations: [
icon({ icon({
include: { include: {
// Include only three `mdi` icons in the bundle // Include only three `mdi` icons in the bundle
// Otherwise, Astro Icons could include every single icon in the mdi package and result in a huge bundle size // Otherwise, Astro Icons could include every single icon in the mdi package and result in a huge bundle size
mdi: ["github", "linkedin"], mdi: ["github", "linkedin", "alternate-email"],
}, },
}), }),
sitemap(), sitemap(),
// Refer to: https://astro-d2.vercel.app/configuration/
d2({
// Outputs to `public/assets/d2`
output: "assets/d2",
sketch: true,
pad: 50,
layout: "dagre",
// https://d2lang.com/tour/themes/
theme: {
default: "3",
dark: "200",
},
}),
], ],
}); });

BIN
bun.lockb

Binary file not shown.

6
codebook.toml Normal file
View file

@ -0,0 +1,6 @@
words = [
"astro",
"craftzdog",
"martínez",
"neovim",
]

View file

@ -15,6 +15,7 @@
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
"@astrojs/sitemap": "^3.4.0", "@astrojs/sitemap": "^3.4.0",
"astro": "^5.8.1", "astro": "^5.8.1",
"astro-d2": "^0.8.0",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },

View file

@ -17,6 +17,9 @@ import profileImage from "@assets/profile.jpeg";
<a href="https://linkedin.com/in/jaster-victor/"> <a href="https://linkedin.com/in/jaster-victor/">
<Icon size="1.5rem" name="mdi:linkedin" /> <Icon size="1.5rem" name="mdi:linkedin" />
</a> </a>
<a href="mailto:jaster.victor@gmail.com">
<Icon size="1.5rem" name="mdi:alternate-email" />
</a>
</nav> </nav>
<div class="profile"> <div class="profile">
@ -70,7 +73,7 @@ import profileImage from "@assets/profile.jpeg";
} }
.profile-image-wrapper:hover { .profile-image-wrapper:hover {
border-color: var(--violet300); border-color: var(--link-color);
cursor: pointer; cursor: pointer;
} }

View file

@ -7,6 +7,7 @@ const blog = defineCollection({
schema: ({ image }) => schema: ({ image }) =>
z.object({ z.object({
title: z.string(), title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(), pubDate: z.coerce.date(),
image: image(), image: image(),
}), }),

View file

@ -0,0 +1,466 @@
---
title: "Decoupling Elixir GenServers with Phoenix PubSub"
description: "A way to make our GenServers more easy to test and maintain by using PubSub"
pubDate: 2025-06-05
image: "./assets/elixir.svg"
---
In this post I'd like to talk about an interesting way to decouple GenServers from the rest of the system by using a PubSub library.
We will do it by going through actual code. We will take a look at a regular Elixir application and we will discuss possible problems in it.
We will then use PubSub to refactor the application and we'll discuss the final result, its benefits but also possible downsides.
## Introduction
The project we will look at is very simple.
It consists of an IoT application that automates locking and unlocking a door.
We can imagine a user interacting with our application using some sort of remote control.
Then, when the door gets locked, we want a light to become red and a notification to be sent to us.
Finally, when the door gets unlocked, we want a light to become green and get notified too.
### Architecture
The application consists of:
- A GenServer that manages the state of the Door and is responsible for locking or unlocking it.
- A module that manages the state of the lights and is responsible for changing colors.
- A module that manages notifications.
Let's look at a diagram of the whole system first:
<div class="diagram" align="center">
```d2 width="500" theme=303 title="Lock door flow"
direction: down
user {
shape: c4-person
}
firmware {
door_server: DoorServer
lights: Lights
notifications: Notifications
label.near: bottom-left
}
third_party_notifications_service: 3rd party notifications service {
shape: cloud
}
hardware: {
shape: rectangle
}
user -> firmware.door_server: lock door {style.animated: true}
firmware.door_server -> firmware.lights: set_lights_red {style.animated: true}
firmware.door_server -> firmware.notifications: send_notification {style.animated: true}
firmware.door_server -> hardware: lock {style.animated: true}
firmware.lights -> hardware: set_lights_red {style.animated: true}
firmware.notifications -> third_party_notifications_service: send_notification {style.animated: true}
```
</div>
In this post we will focus on the firmware layer, more specifically in the Door server, its implementation and tests.
## Let's get into code
Let's go directly to the point, this is how we could implement a basic version of a DoorServer
```elixir
defmodule DoorAutomation.DoorServer do
use GenServer
@locked_state :locked
@unlocked_state :unlocked
alias DoorAutomation.Lights
alias DoorAutomation.Notifications
# Client API
def start_link(initial_state \\ :locked) do
GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)
end
def lock do
GenServer.call(__MODULE__, :lock)
end
def unlock do
GenServer.call(__MODULE__, :unlock)
end
def get_state do
GenServer.call(__MODULE__, :get_state)
end
# Server API
@impl true
def init(initial_state), do: {:ok, initial_state}
@impl true
def handle_call(:get_state, _from, current_state) do
{:reply, current_state, current_state}
end
@impl true
def handle_call(:lock, _from, _current_state) do
# Call the underlying hardware
# Hardware.unlock_door()
Lights.set_red()
Notifications.send_notification("Door has been Locked.")
{:reply, :locked, @locked_state}
end
@impl true
def handle_call(:unlock, _from, _current_state) do
# Call the underlying hardware
# Hardware.unlock_door()
Lights.set_green()
Notifications.send_notification("Door has been Unlocked.")
{:reply, :unlocked, @unlocked_state}
end
end
```
For the purpose of this article, the underlying calls that the Door Server would need to make to the Hardware are left out of the code.
### Let's write some tests
Let's now see a basic test suite for the DoorServer:
```elixir
defmodule DoorAutomation.DoorServerTest do
use ExUnit.Case
alias DoorAutomation.DoorServer
alias DoorAutomation.Lights
alias DoorAutomation.Notifications
setup do
# Here we'd add whatever setup is needed
# for the lights and notifications module
{:ok, _} = start_supervised({DoorServer, :unlocked})
:ok
end
test "locking the door changes state, light, and sends notification" do
assert DoorServer.get_state() == :unlocked
assert :ok = DoorServer.lock()
assert DoorServer.get_state() == :locked
assert Lights.get_color() == "red"
assert Notifications.get_notifications() == ["Door has been Locked."]
end
test "unlocking the door changes state, light, and sends notification" do
assert :ok = DoorServer.lock()
assert DoorServer.get_state() == :locked
Notifications.reset_notifications()
assert :ok = DoorServer.unlock()
assert DoorServer.get_state() == :unlocked
assert Lights.get_color() == "green"
assert Notifications.get_notifications() == ["Door has been Unlocked."]
end
end
```
## What is wrong here?
Well, first of all I'd like to make it clear that there is actually nothing "wrong". The code we see above is completely valid.
It compiles and it could be enough for someone's needs.
With that said, let's talk about some possible issues we could run into.
### Coupling!
Following the previous design, we are coupling the DoorServer implementation with the lights and notifications modules.
The lights module might rely hardware which could fail at any time (perhaps the circuits break) and the notifications module might rely on a network to send messages.
If the lights are faulty or the network goes down, the door could potentially stop working and the user wouldn't be able to lock or unlock it anymore.
In this case, if a user locks the door but the lights don't work or the notifications service is off, we want the door to lock anyway.
Ideally, any issues that might occur related to the lights or notifications system should be handled in their own modules.
Our door server should simply worry about locking or unlocking a door.
Coupling these modules spreads complexity that should be isolated.
### Coupling in tests
Coupling doesn't stop in the DoorServer module, it gets propagated into our tests.
Because our `lock` and `unlock` functions cause a series of side effects, we find ourselves having to add extra setup to test them.
Now, what was supposed to be a unit test suite for our DoorServer ends up looking more like an integration test suite.
Each time we add a new side effect that doesn't necessarily have to do with locking or unlocking a door, these tests have to be updated.
## So, what can we do?
We want to find a way to decouple our door server from the rest of the system and protect it from unrelated issues.
I'm sure there are many ways you could do this, but in this article I want to talk about a specific one I recently deployed to production with success.
Let's make our Elixir application more event-driven!
We can make the DoorServer publish an event whenever the doors are successfully locked or unlocked.
Imagine if we had some sort of dedicated mailbox for our door events that any processes can subscribe to.
Then, if a module that is interested on those events can simply subscribe to them and implement their own handlers.
This automatically makes our application more flexible, testable and maintainable.
## Introducing Phoenix PubSub
[Phoenix PubSub](https://github.com/phoenixframework/phoenix_pubsub) is a known Elixir library that comes with the Phoenix framework.
It is a generic PubSub library which enables us to implement a publish-subscribe pattern in our Elixir applications.
It supports distribution by default (publishing and subscribing between erlang nodes) and multiple backends including Redis.
Even-though it explicitly says it is made for the Phoenix framework, it doesn't have any phoenix-related dependencies and can be used on its own.
Also we know Phoenix has been battle tested, so we can be sure Phoenix PubSub too!
## Rewriting the DoorServer
As mentioned, we'll rewrite the DoorServer by making it publish events and remove any coupling with the other modules:
```elixir
defmodule DoorAutomation.DoorServer do
use GenServer
alias Phoenix.PubSub
@locked_state :locked
@unlocked_state :unlocked
@door_topic "door_events"
# Client API
def start_link(initial_state \\ :locked) do
GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)
end
def lock do
GenServer.call(__MODULE__, :lock)
end
def unlock do
GenServer.call(__MODULE__, :unlock)
end
def get_state do
GenServer.call(__MODULE__, :get_state)
end
def events_topic(), do: @door_topic
# Server API
@impl true
def init(initial_state) do
{:ok, initial_state}
end
@impl true
def handle_call(:get_state, _from, current_state) do
{:reply, current_state, current_state}
end
@impl true
def handle_call(:lock, _from, _current_state) do
# Hardware.lock_door()
Phoenix.PubSub.broadcast(DoorAutomation.PubSub, @door_topic, :door_locked)
{:reply, :locked, @locked_state}
end
@impl true
def handle_call(:unlock, _from, _current_state) do
# Hardware.unlock_door()
Phoenix.PubSub.broadcast(DoorAutomation.PubSub, @door_topic, :door_unlocked)
{:reply, :unlocked, @unlocked_state}
end
end
```
I'd like to also show how to write a subscriber for the lights module to react to pub sub events.
Also, to be consistent with our new design, we'll make it publish events when the lights are updated.
This way testing that the lights were updated after firing a door event becomes simpler as we'll see later.
```elixir
defmodule DoorAutomation.LightsSubscriber do
use GenServer
alias Phoenix.PubSub
@lights_topic "lights_events"
# Client API
def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def events_topic(), do: @lights_topic
# Server API
@impl true
def init(_args) do
# Subscribe to the topic when the GenServer starts
:ok = Phoenix.PubSub.subscribe(DoorAutomation.PubSub, DoorAutomation.DoorServer.events_topic())
{:ok, %{}}
end
@impl true
def handle_info(:door_locked, state) do
:ok = DoorAutomation.Lights.set_red()
Phoenix.PubSub.broadcast(DoorAutomation.PubSub, @lights_topic, :lights_set_red)
{:noreply, state}
end
@impl true
def handle_info(:door_unlocked, state) do
:ok = DoorAutomation.Lights.set_green()
Phoenix.PubSub.broadcast(DoorAutomation.PubSub, @lights_topic, :lights_set_green)
{:noreply, state}
end
end
```
As a note, some people might prefer publishing the light events from inside the Lights module,
I don't have a strong opinion here so for simplicity I'll leave that logic in the LightsSubscriber.
## Rewriting our tests
Now we can rewrite our tests in a more event-driven way:
```elixir
defmodule DoorAutomation.DoorServerTest do
use ExUnit.Case
alias DoorAutomation.DoorServer
alias Phoenix.PubSub
setup do
# Subscribe the current test process to the door events topic
# This makes events published by DoorServer arrive in the test process's mailbox.
:ok = PubSub.subscribe(DoorAutomation.PubSub, DoorAutomation.DoorServer.events_topic())
{:ok, _} = start_supervised({DoorServer, :unlocked})
:ok
end
test "locking the door changes state and publishes :door_locked event" do
assert :ok = DoorServer.lock()
assert DoorServer.get_state() == :locked
assert_receive :door_locked
refute_receive :door_unlocked
end
test "unlocking the door changes state and publishes :door_unlocked event" do
assert :ok = DoorServer.unlock()
assert DoorServer.get_state() == :unlocked
assert_receive :door_unlocked
refute_receive :door_locked
end
end
```
Now to write tests for the subscriber we can do as follows:
```elixir
defmodule DoorAutomation.LightsSubscriberTest do
use ExUnit.Case
alias DoorAutomation.LightsSubscriber
alias DoorAutomation.DoorServer
alias Phoenix.PubSub
# Setup block that runs before each test
setup do
:ok = PubSub.subscribe(DoorAutomation.PubSub, LightsSubscriber.events_topic())
{:ok, _subscriber_pid} = start_supervised(LightsSubscriber)
:ok
end
test "LightsSubscriber subscribes sets lights red on :door_locked" do
PubSub.broadcast(DoorAutomation.PubSub, DoorServer.events_topic(), :door_locked)
assert_receive :lights_set_red
refute_receive :lights_set_green
end
test "LightsSubscriber sets lights green on :door_unlocked" do
PubSub.broadcast(DoorAutomation.PubSub, DoorServer.events_topic(), :door_unlocked)
assert_receive :lights_set_green
refute_receive :lights_set_red
end
end
```
## What has improved?
By refactoring our DoorServer this way we only have to worry about maintaining and testing logic that concerns exclusively the door and the events it is meant to publish.
On the other hand, other modules can be tested very easily if we also refactor them to work in a more event-driven way.
For instance, as we just saw, to test if the lights change when a door change occurs we can use entirely our PubSub library to simulate door events happening and to assert that new events are published.
## Conclusions
Decoupling your elixir application components by making them more event-driven can make them more maintainable,
allowing them to grow independently of the rest of the system being sure you won't be breaking other modules as long as you don't break your events contract.
We've also seen how tests become simpler due to the fact that each test suite becomes more narrowed in scope.
There are some drawbacks though:
- Debugging becomes harder.
- You are introducing an extra of asynchronous broadcast messaging and therefore your elixir processes become harder to debug.
- Event delivery is not ensured.
- If a subscriber goes down, Phoenix PubSub won't try to re-deliver lost events once the process restarts.
- Depending on your situation you might want to implement a more complex pub-sub system that does its best to ensure event delivery.
- You loose orchestration
- You can't enforce the order each subscriber will consume the same event.
If your case requires one side effect to happen before another
(For example, the lights have to turn red before sending a notification) you will need to setup a single subscriber that executes them in order.
So as with every software pattern, depending on the requirements of your system this might or might not work for you.
In my case, my application didn't require side effects to be performed in order nor it was a problem if an event was lost because a process was down.
This is the first ever post that I publish online, I hope someone finds it useful.
If you have any doubts or feedback you can contact me via email, you can find it at the top of the page :)
Thank you for your time!

View file

@ -4,10 +4,8 @@ pubDate: 2024-01-20
image: "./assets/me.png" image: "./assets/me.png"
--- ---
So this is my first post in here, I'm quite excited :D
This is just an introductory post where I will explain a little bit more This is just an introductory post where I will explain a little bit more
what is this and the purpose of it. what is this place and the purpose of it.
## What is this about? ## What is this about?
@ -19,28 +17,19 @@ It will be mainly focused in programming and software development,
but I don't want to constrain myself to just that but I don't want to constrain myself to just that
so let's see where all of this goes. so let's see where all of this goes.
Also you can notice how my goal is not to make this a space to sell myself as a developer. My main purpose is to enjoy the process, have fun while
writing posts and most important to feel comfortable with what I build.
My main purpose is to enjoy the process, to have fun while
building this website and writing posts and most important to feel
comfortable and represented by what I build.
## A few more notes ## A few more notes
The fact that the website is pixel art themed and contains purple-ish colors
is merely because I love pixel art and because that's my favourite color :)
The color palette comes from the same theme I use in my Neovim setup. The color palette comes from the same theme I use in my Neovim setup.
It was developed by [craftzdog](https://www.craftz.dog/), It was developed by [craftzdog](https://www.craftz.dog/),
I am a big fan of his work. Go check out his [Youtube channel](https://www.youtube.com/devaslife)! I am a big fan of his work. Go check out his [Youtube channel](https://www.youtube.com/devaslife)!
The whole website was built using [Astro](https://astro.build/). The whole website was built using [Astro](https://astro.build/).
I've had a blast of a time with this framework, I've had great time with this framework,
it is definitely the right tool if you are looking to build a blog! it is definitely the right tool if you are looking to build a blog!
And that's it for today folks, I hope to be here soon with new content! And that's it for today, I hope to be here soon with new content!
One thing I can already tell is that there will
be a lot of content related to Rust programming 🦀.
**Stay tuned!** **Stay tuned!**

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2122 2122" style="enable-background:new 0 0 2122 2122;" xml:space="preserve">
<g>
<path style="fill:#D1E1EB;" d="M1276.382,844.885V584.528h25.309c43.313,0,78.75-35.438,78.75-78.751
c0-43.313-35.437-78.751-78.75-78.751H819.63c-43.313,0-78.751,35.438-78.751,78.751c0,43.313,35.439,78.751,78.751,78.751h25.309
v260.357c-224.536,86.624-383.844,304.465-383.844,559.545c0,331.13,268.434,599.564,599.564,599.564
s599.564-268.434,599.564-599.564C1660.224,1149.35,1500.917,931.509,1276.382,844.885z"/>
<path style="fill:#9ECDE6;" d="M971.72,521.138h-25.311c-43.313,0-78.75-35.438-78.75-78.751c0-5.255,0.542-10.387,1.537-15.361
H819.63c-43.313,0-78.751,35.438-78.751,78.751c0,43.313,35.439,78.751,78.751,78.751h25.309v260.357
c-224.536,86.624-383.844,304.465-383.844,559.545c0,331.13,268.434,599.564,599.564,599.564
c63.676,0,125.017-9.962,182.593-28.352c-32.428,3.225-64.961,5.283-97.547,5.865c-86.115,1.539-173.238-7.17-255.647-33.108
c-81.667-25.705-157.182-68.89-218.663-128.716c-70.345-68.449-119.891-154.769-146.345-249.128
c-47.507-169.461-7.832-363.453,93.331-505.861c0,0,110.151-162.614,351.532-183.751c0.3-0.026,2.935-253.568,2.935-253.568
l161.067-42.847h142.466h25.309c38.058,0,70.015-27.367,77.213-63.39H971.72z"/>
<path style="fill:#FFFFFF;" d="M1319.442,485.334h-257.555c-9.684,0-17.608-7.924-17.608-17.608l0,0
c0-9.685,7.924-17.608,17.608-17.608h257.555c9.684,0,17.608,7.924,17.608,17.608l0,0
C1337.05,477.411,1329.126,485.334,1319.442,485.334z"/>
<path style="fill:#A75A97;" d="M1550.618,1278.821c-10.375,30.318-226.09,54.53-490.727,54.53
c-258.464,0-470.295-23.094-489.703-52.417c-9.925,39.532-15.256,80.886-15.256,123.495c0,279.306,226.423,505.728,505.728,505.728
c279.305,0,505.729-226.422,505.729-505.728C1566.389,1361.063,1560.885,1318.989,1550.618,1278.821z"/>
<path style="fill:#65488F;" d="M1062.017,1786.685c-15.778-25.921-31.536-51.848-47.568-77.609
c-13.992-22.482-27.311-46.133-34.015-71.944c-6.415-24.704-5.727-51.056,3.993-74.834c7.885-19.29,17.571-37.801,28.077-55.782
c24.306-41.601,52.85-80.709,82.751-118.432c14.948-18.858,30.363-37.36,46.173-55.515c-26.522,0.513-53.754,0.783-81.536,0.783
c-258.464,0-470.295-23.094-489.703-52.417c-9.925,39.532-15.256,80.886-15.256,123.495c0,279.306,226.423,505.728,505.728,505.728
c24.893,0,49.354-1.831,73.285-5.305C1109.969,1865.463,1085.993,1826.074,1062.017,1786.685z"/>
<path style="fill:#CC7AA8;" d="M1059.892,1333.351c264.637,0,480.351-24.212,490.727-54.53c0.258-0.758,0.429-1.52,0.429-2.286
c0-31.378-219.898-56.816-491.155-56.816c-271.258,0-491.155,25.438-491.155,56.816c0,1.48,0.492,2.947,1.452,4.398
C589.596,1310.257,801.428,1333.351,1059.892,1333.351z"/>
<ellipse transform="matrix(0.5 -0.866 0.866 0.5 -274.6768 1609.1263)" style="fill:#FFFFFF;" cx="1256.206" cy="1042.44" rx="105.729" ry="210.461"/>
<path style="fill:#FFFFFF;" d="M1526.553,1228.9c-13.704,15.096-41.065,12.58-61.112-5.619
c-20.047-18.199-25.188-45.19-11.484-60.286c13.705-15.096,41.066-12.58,61.112,5.619
C1535.116,1186.813,1540.258,1213.804,1526.553,1228.9z"/>
<path style="fill:#F39452;" d="M1065.225,174.248c-111.51,13.509-203.883,15.245-212.964,4.374l10.552,248.403h390.743
l19.214-298.554l-0.184,0.021C1262.894,141.262,1174.265,161.038,1065.225,174.248z"/>
<path style="fill:#EF7747;" d="M1164.241,384.442c-32.827-4.501-67.072-11.337-96.265-27.733
c-11.758-6.602-22.574-15.075-30.575-26.016c-10.341-14.138-13.722-32.001-15.545-49.074c-3.667-34.332,0.149-68.572,2.427-102.845
c-92.436,9.219-164.08,9.355-172.021-0.152l10.552,248.403h390.743l2.296-35.681
C1225.225,390.47,1194.604,388.605,1164.241,384.442z"/>
<path style="fill:#F8B88A;" d="M1065.225,174.248c109.04-13.21,197.668-32.986,207.362-45.755c0.665-0.877,0.972-1.721,0.875-2.527
c-1.52-12.548-97.216-11.277-213.742,2.84c-116.526,14.117-209.756,35.733-208.236,48.282c0.066,0.54,0.368,1.045,0.777,1.535
C861.343,189.493,953.715,187.757,1065.225,174.248z"/>
<path style="fill:#FFFFFF;" d="M1213.605,343.089L1213.605,343.089c-15.049-1.088-26.472-14.291-25.384-29.341l7.406-102.438
c1.088-15.049,14.291-26.472,29.34-25.384l0,0c15.049,1.088,26.472,14.291,25.384,29.34l-7.406,102.438
C1241.857,332.754,1228.654,344.177,1213.605,343.089z"/>
<ellipse transform="matrix(0.3838 -0.9234 0.9234 0.3838 -503.6756 2242.0808)" style="fill:#CC6098;" cx="1428.062" cy="1498.424" rx="133.823" ry="74.835"/>
<path style="fill:#CC6098;" d="M1361.324,1698.587c-11.376,22.513-34.099,33.941-50.753,25.525
c-16.654-8.416-20.933-33.488-9.557-56.001c11.376-22.513,34.099-33.941,50.753-25.525
C1368.422,1651.001,1372.7,1676.074,1361.324,1698.587z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -22,7 +22,7 @@ const backgroundImageUrl = `url('${optimizedBgImage.src}')`;
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta <meta
name="description" name="description"
content="My personal blog where I write about my learnings on software development & coding. content="My personal blog where I write about my learning on software development & coding.
Learn with my about best coding practices and new tools to add to your developer toolbox." Learn with my about best coding practices and new tools to add to your developer toolbox."
/> />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
@ -65,7 +65,7 @@ const backgroundImageUrl = `url('${optimizedBgImage.src}')`;
.wrapper { .wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 80ch; width: 90ch;
min-height: calc(100vh - 1rem); min-height: calc(100vh - 1rem);
border: 2px solid white; border: 2px solid white;
background: rgba(29, 31, 33, 0.95); background: rgba(29, 31, 33, 0.95);

View file

@ -46,24 +46,12 @@ const { title, pubDate, image } = Astro.props;
color: var(--text-color-light); color: var(--text-color-light);
} }
/* Revert the changes made by the global styles */
/* So markdown can be spaced as the markdown processor says */
.post-content { .post-content {
h1, * {
h2, margin: revert;
h3, padding: revert;
h4 {
margin-top: 2rem;
}
p {
margin-top: 0.5rem;
}
li {
margin-top: 0.5rem;
}
li > p {
display: contents;
} }
} }
</style> </style>

View file

@ -26,6 +26,7 @@ const sortedPosts = posts.sort(
{post.data.title} {post.data.title}
</a> </a>
</h2> </h2>
{post.data.description && <p>{post.data.description}</p>}
</article> </article>
)) ))
} }

View file

@ -139,11 +139,10 @@
} }
:root { :root {
--base-color: #1d1f21;
--text-color: white; --text-color: white;
--text-color-light: #657b83; --text-color-light: #657b83;
--violet300: #9ca0ed; --link-color: #9ca0ed;
--violet100: #cccfff; --link-hover-color: #cccfff;
--font-size-m: 18px; --font-size-m: 18px;
} }
@ -165,19 +164,28 @@ a {
} }
ul { ul {
list-style: square inside none; list-style: square;
}
ol {
list-style-position: inside;
} }
a { a {
text-decoration: none; text-decoration: none;
color: var(--violet300); color: var(--link-color);
font-weight: bold; font-weight: bold;
} }
a:hover { a:hover {
color: var(--violet100); color: var(--link-hover-color);
}
/* Fix responsiveness issues with code blocks */
.astro-code code {
display: block;
width: 0;
font-size: 16px;
}
/* Fix responsiveness issues with generated diagrams */
.diagram img {
max-width: 100% !important;
height: auto !important;
} }