Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion lib/jola_dev/blog/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ defmodule JolaDev.Blog.Post do
Struct representing a blog post with metadata and content.
"""
@enforce_keys [:id, :author, :title, :body, :description, :tags, :date]
defstruct [:id, :author, :title, :body, :description, :tags, :date, :last_modified]
defstruct [
:id,
:author,
:title,
:body,
:description,
:tags,
:date,
:last_modified,
:canonical_url
]

def build(filename, attrs, body) do
[year, month_day_id] =
Expand Down
6 changes: 3 additions & 3 deletions lib/jola_dev_web/controllers/page_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule JolaDevWeb.PageController do
recent_posts = Enum.take(JolaDev.Blog.all_posts(), 3)

render(conn, :home,
page_title: "Johanna LarssonSoftware Engineer & Speaker",
page_title: "Johanna Larsson, Software Engineer and Speaker",
meta_description:
"Johanna Larsson is a software engineer, engineering leader, writer, and speaker with many years of experience building products and leading teams.",
recent_posts: recent_posts
Expand All @@ -14,9 +14,9 @@ defmodule JolaDevWeb.PageController do

def about(conn, _params) do
render(conn, :about,
page_title: "About | jola.dev",
page_title: "About Johanna Larsson",
meta_description:
"About Johanna Larsson software engineer, engineering leader, writer, and speaker with many years of experience.",
"About Johanna Larsson, software engineer, engineering leader, writer, and speaker with many years of experience.",
page_type: :about
)
end
Expand Down
2 changes: 2 additions & 0 deletions lib/jola_dev_web/helpers/canonical_url.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule JolaDevWeb.Helpers.CanonicalUrl do
@doc """
Generates a canonical URL for the current request.
"""
def canonical_url(%{assigns: %{post: %{canonical_url: url}}}) when is_binary(url), do: url

def canonical_url(conn) do
"https://jola.dev#{conn.request_path}"
end
Expand Down
57 changes: 44 additions & 13 deletions lib/jola_dev_web/helpers/seo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ defmodule JolaDevWeb.Helpers.SEO do
@host "https://jola.dev"

def json_ld(conn) do
[website_schema()] ++ page_schemas(conn) ++ breadcrumb_schemas(conn)
[website_schema(), person_schema()] ++ page_schemas(conn) ++ breadcrumb_schemas(conn)
end

defp website_schema do
%{
"@context" => "https://schema.org",
"@type" => "WebSite",
"name" => "jola.dev",
"url" => @host,
"author" => person_ref()
"url" => @host
}
end

defp person_schema do
Map.put(person(), "@context", "https://schema.org")
end

defp page_schemas(%{assigns: %{post: post}}) do
url = "#{@host}#{~p"/posts/#{post.id}"}"

Expand Down Expand Up @@ -48,28 +51,56 @@ defmodule JolaDevWeb.Helpers.SEO do
%{
"@context" => "https://schema.org",
"@type" => "ProfilePage",
"mainEntity" =>
Map.merge(person(), %{
"sameAs" => [
"https://github.com/joladev",
"https://linkedin.com/in/joladev",
"https://bsky.app/profile/jola.dev",
"https://twitter.com/joladev"
]
})
"mainEntity" => person_ref()
}
]
end

defp page_schemas(%{assigns: %{posts: _, tag: _}}), do: []

defp page_schemas(%{assigns: %{posts: posts}}) do
[
%{
"@context" => "https://schema.org",
"@type" => "Blog",
"@id" => "#{@host}/posts#blog",
"url" => "#{@host}/posts",
"name" => "jola.dev blog",
"author" => person_ref(),
"blogPost" => Enum.map(posts, &blog_post_summary/1)
}
]
end

defp page_schemas(_conn), do: []

defp blog_post_summary(post) do
url = "#{@host}#{~p"/posts/#{post.id}"}"

%{
"@type" => "BlogPosting",
"headline" => post.title,
"url" => url,
"datePublished" => Date.to_iso8601(post.date),
"dateModified" => Date.to_iso8601(post.last_modified),
"image" => "#{@host}#{JolaDev.OGImage.path_for("posts/#{post.id}")}",
"author" => person_ref()
}
end

defp person do
%{
"@type" => "Person",
"@id" => "#{@host}/#person",
"name" => "Johanna Larsson",
"url" => "#{@host}#{~p"/about"}",
"jobTitle" => "Software Engineer & Engineering Leader"
"jobTitle" => "Software Engineer & Engineering Leader",
"sameAs" => [
"https://github.com/joladev",
"https://linkedin.com/in/joladev",
"https://bsky.app/profile/jola.dev",
"https://twitter.com/joladev"
]
}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Fortunately, Elixir and Erlang have a solution for this: ETS. In the words of th

> This module is an interface to the Erlang built-in term storage BIFs. These provide the ability to store very large quantities of data in an Erlang runtime system and to have constant access time to the data.

So here we have something that acts a little bit more like a mutable hash map. There are lots of other reasons why ETS is much cooler than a plan hash map, but I’ll leave that to a future article. For now, let’s just pretend that it’s a hash map.
So here we have something that acts a little bit more like a mutable hash map. There are lots of other reasons why ETS is much cooler than a plain hash map, but I [wrote about that separately](/posts/patterns-for-managing-ets-tables). For now, let’s just pretend that it’s a hash map.

It’s started with `:ets.new(:table_name, options)`. You can insert data with `:ets.insert` and read with `:ets.lookup`, but the two operations we’re interested in are `:ets.update_counter` and `:ets.match_object`. Here’s what it looks like if we replace the map with an ETS table.

Expand Down
2 changes: 1 addition & 1 deletion priv/posts/2019/05-13-registry-select-2-and-match-specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ After bringing this up in the Slack I was encouraged to suggest it on the Elixir

## Registry.select/2

So, the final version that got merged ended up being based on match specs, giving a close to direct access to the underlying ETS table of the Registry. I’ll grab some examples from the documentation to give an idea of what this means.
So, the final version that got merged ended up being based on match specs, giving a close to direct access to the [underlying ETS table](/posts/patterns-for-managing-ets-tables) of the Registry. I’ll grab some examples from the documentation to give an idea of what this means.

Get all registered processes, keys and values.

Expand Down
2 changes: 1 addition & 1 deletion priv/posts/2019/06-05-patterns-for-managing-ets-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This article attempts to show some basic patterns for how to use ETS in Elixir o

ETS is a really cool tool in the Erlang runtime system, but to understand it you may need a little context. I won’t go too deep into how it works, but even a very short explanation of it starts with a description of the memory management of the BEAM. In short, every single line of Elixir or Erlang runs in a process and each process has its own heap. This provides all kinds of cool properties for your code: including sidestepping problems of shared memory access and making garbage collection cheap by limiting it per process (since the heap is automatically freed by processes that end, sometimes you don’t even necessarily get any overhead from garbage collection). But if every process owns its own heap and is unable to access any other’s, how can they share data?

One way is sending messages, which means copying it from one heap to another. This is what you do if you communicate across processes. Let’s take [Agents](https://elixir-lang.org/getting-started/mix-otp/agent.html) as an example. Each agent stores its own state and accepts requests to get or update that data. Any data you get out of an Agent is copied to the requesting process. There are downsides to storing data in processes though, you are not able to access it concurrently. The process holding the data acts as a bottle-neck. This in and of It also generally keeps its state in a standard immutable Elixir data structure. Immutable data structures are really cool, but as I discussed in [Elixir String Processing Optimization](https://blog.jola.dev/elixir-string-processing-optimization), their performance characteristics don’t make them the appropriate choice for all use cases.
One way is sending messages, which means copying it from one heap to another. This is what you do if you communicate across processes. Let’s take [Agents](https://elixir-lang.org/getting-started/mix-otp/agent.html) as an example. Each agent stores its own state and accepts requests to get or update that data. Any data you get out of an Agent is copied to the requesting process. There are downsides to storing data in processes though, you are not able to access it concurrently. The process holding the data acts as a bottle-neck. This in and of It also generally keeps its state in a standard immutable Elixir data structure. Immutable data structures are really cool, but as I discussed in [Elixir String Processing Optimization](/posts/elixir-string-processing-optimization), their performance characteristics don’t make them the appropriate choice for all use cases.

The Erlang runtime system offers ETS precisely to solve those problems (though technically that may not be why they were originally created). It offers extremely performant mutable updates and concurrent reads. Just to give you a ballpark notion, a microbenchmark I made showed ETS as about 25 times faster than [Redis/Redix](https://github.com/whatyouhide/redix), with both reads and writes coming in at a median of 2µs. But don’t trust me, if speed matters to you make sure to measure it yourself!

Expand Down
2 changes: 1 addition & 1 deletion priv/posts/2019/10-18-erlang-queue-module-elixir.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Erlang queues are similar to lists but double-ended, meaning you can efficiently

> Queues are double-ended. The mental picture of a queue is a line of people (items) waiting for their turn. The queue front is the end with the item that has waited the longest. The queue rear is the end an item enters when it starts to wait. If instead using the mental picture of a list, the front is called head and the rear is called tail.

A common use case is where you want to temporarily store items and eventually take them back out in the order that you put them in.
A common use case is where you want to temporarily store items and eventually take them back out in the order that you put them in. I use this pattern as the buffer in [Push-based GenStage](/posts/push-based-genstage).

Using them can feel a bit awkward, partly because Erlang orders arguments differently than Elixir, and partly because the internal representation of queues is exposed. Note that the docs clearly state that you should treat it as an opaque type.

Expand Down
2 changes: 1 addition & 1 deletion priv/posts/2019/10-19-push-based-genstage.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This blog post is mostly educational, but this pattern can be applied effectivel

Let’s take a look at a stripped-down example. We’ll set up a producer that accepts messages from any other process, like a regular GenServer, and distributes them to the consumers. We’ll avoid worrying about `min_demand` and `max_demand` and limit consumers to ask for a single event at a time. This is perfectly reasonable where the work is slow and processing time irregular, eg if they involve making requests to network resources. `GenStage` comes with a built-in buffer, if a producer produces more messages than the consumers can handle, they’re automatically buffered with a maximum of `10_000` items by default. We’re not going to rely on it though, we’ll be buffering ourselves in the producer. This has some interesting benefits in improvements we can build to the producer, including backpressure. That’s a future post though.

First out, we define a module using `GenStage` with `start_link` and `init`, with the latter returning a tuple of a queue and an initial demand of 0. If you havent seen `:queue` before, take a look at the [Erlang documentation](http://erlang.org/doc/man/queue.html). In short, it allows us to create an efficient buffer for situations where the producer gets pushed more events than the consumers can handle.
First out, we define a module using `GenStage` with `start_link` and `init`, with the latter returning a tuple of a queue and an initial demand of 0. If you haven't seen :queue before, take a look at [my walkthrough](/posts/erlang-queue-module-elixir) or the [Erlang documentation](http://erlang.org/doc/man/queue.html). In short, it allows us to create an efficient buffer for situations where the producer gets pushed more events than the consumers can handle.

```elixir
defmodule Producer do
Expand Down
2 changes: 1 addition & 1 deletion priv/posts/2020/01-21-announcing-hex-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ When it’s easy to work in a secure way, people are more likely to do it. This

## What’s next?

The project is open-source, licensed under Apache 2.0, like Elixir itself. You’ll find it under the `hexpm` organization [on Github](https://github.com/hexpm/diff). Please don’t hesitate to share your ideas for improvements or additions!
The project is open-source, licensed under Apache 2.0, like Elixir itself. You’ll find it under the `hexpm` organization [on Github](https://github.com/hexpm/diff). Please don’t hesitate to share your ideas for improvements or additions! If you're curious about how we built it, I wrote a separate post on [Building Hex Diff](/posts/building-hex-diff).
5 changes: 3 additions & 2 deletions priv/posts/2020/01-21-building-hex-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
description: "I wanted to give some insight into the Hex Diff project, how it works, and some issues we ran into on the way."
}
---
I wanted to give some insight into the Hex Diff project, how it works, and some issues we ran into on the way. The project is [open-source](https://github.com/hexpm/diff), so feel free to look behind the curtain to see what makes it tick. Some parts of the project originate from my toy version (no longer live), but others were written specifically for this project. We also ended up finding and fixing a bunch of bugs in other projects!

I wanted to give some insight into the Hex Diff project, how it works, and some issues we ran into on the way. The project is [open-source](https://github.com/hexpm/diff), so feel free to look behind the curtain to see what makes it tick. Some parts of the project originate from my toy version (no longer live), but others were written specifically for this project. We also ended up finding and fixing a bunch of bugs in other projects! This is a companion to [the announcement post](/posts/announcing-hex-diff).

## The Search View

Expand All @@ -19,7 +20,7 @@ Hex has an API with loads of useful endpoints, including search, but we ended up

Really, the only things we need are a list of package names and a list of versions for each of the packages, and we’ll be able to implement our own search. Luckily, `hex_core` also lets you query the CDN fronted registry itself for simple stuff, like getting all package names and versions! This means we can do super cheap requests for this data.

The last thing to consider is how LiveView works. While typing into the input field every single key input turns into an event on the backend. Although you could build throttling into this, the slower you are to react to the events, the less snappy the UI feels. Instead of loading all the repo data from Hex on `mount` into each LV process, or throttling or debouncing events, I went with a different approach. I set up an ETS table to own all the data, and then a process that periodically refreshes it with the latest results from Hex. Now we can query the package name and version data directly from ETS, and not worry about LiveView overloading anything while making the UI super fast.
The last thing to consider is how LiveView works. While typing into the input field every single key input turns into an event on the backend. Although you could build throttling into this, the slower you are to react to the events, the less snappy the UI feels. Instead of loading all the repo data from Hex on `mount` into each LV process, or throttling or debouncing events, I went with a different approach. I set up [an ETS table](/posts/patterns-for-managing-ets-tables) to own all the data, and then a process that periodically refreshes it with the latest results from Hex. Now we can query the package name and version data directly from ETS, and not worry about LiveView overloading anything while making the UI super fast.

## Generating and Rendering Diffs

Expand Down
4 changes: 3 additions & 1 deletion priv/posts/2025/02-04-distributed-ratelimiter.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
title: "Building a Distributed Rate Limiter in Elixir with HashRing",
author: "Johanna Larsson",
tags: ~w(disterl),
description: "Using distributed Elixir to upgrade a node local rate limiter"
description: "Using distributed Elixir to upgrade a node local rate limiter",
canonical_url:
"https://blog.appsignal.com/2025/02/04/building-a-distributed-rate-limiter-in-elixir-with-hashring.html"
}
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,4 @@ This app is intentionally kept simple but powerful. Everything is set up the way

And while you’re at it, why not host it on Hetzner! If you use the [referral link to sign up you get €20 and I get €10](https://hetzner.cloud/?ref=SjrsM8GhyYOl). If you prefer not to use the referral link, here’s a plain link: https://www.hetzner.com/cloud/. Also consider joining me in [sponsoring Dokploy](https://github.com/sponsors/Dokploy).

Source code is available at: https://github.com/joladev/jola.dev. Next up I’ll talk about setting up [bunny.net](https://bunny.net?ref=f0l8865b7g) and a separate post on Dokploy on Hetzner.
Source code is available at: https://github.com/joladev/jola.dev. [Next up](/posts/dropping-cloudflare) I’ll talk about setting up [bunny.net](https://bunny.net?ref=f0l8865b7g) and a separate post on Dokploy on Hetzner.
Loading
Loading