Compare commits

..

22 Commits

Author SHA1 Message Date
b40659723f
use only obsidian to render blog posts
Some checks failed
Deploy Hugo site to Pages / build (push) Has been cancelled
Deploy Hugo site to Pages / deploy (push) Has been cancelled
2025-05-29 16:53:14 +03:00
cc6c1f46f9
add obsidian vault as source for blogging
Some checks failed
Deploy Hugo site to Pages / build (push) Has been cancelled
Deploy Hugo site to Pages / deploy (push) Has been cancelled
2025-05-29 16:29:37 +03:00
a6ffa5eab9
remove twitter link 2025-01-12 11:57:45 +02:00
6fdaffc7df
Put social links on two lines 2024-12-26 12:27:44 +02:00
7e765dbb95
Change boxicons -> font awesome
boxicons didn't have a Bluesky logo
2024-12-26 12:25:30 +02:00
16cdb23fd3
Remove code duplication for landing page 2024-12-26 12:18:01 +02:00
0878052375
clean up hugo.yaml 2024-12-26 12:10:29 +02:00
e8873e7b8c
AI-assisted refactor 2024-12-26 11:44:57 +02:00
b4948d99a8
First draft of servo contribution post 2024-12-26 11:37:30 +02:00
04971f4785
Be more explicit about my location 2024-12-26 11:37:14 +02:00
5918bc1c6f
call myself fullstack rather than freelance 2024-11-28 10:50:02 +02:00
3fd7059f6b
tweak CV organization, as per Anders' suggestions 2024-11-18 12:24:56 +02:00
265e454f4a
fix links, change syntax highlighting theme 2024-10-23 15:48:52 +03:00
83fe8acf7e
publish blog 2024-10-23 15:33:19 +03:00
cad3df3824
add more skills to CV 2024-10-22 14:56:18 +03:00
a094570ce0
add blog post, blog UI still hidden 2024-10-22 14:54:54 +03:00
9664764db5
obfuscate email a bit 2024-10-04 10:38:00 +03:00
fb0073d34d
tweak CV looks 2024-10-04 09:04:20 +03:00
18986d9b8f
hide blog link 2024-10-03 18:05:23 +03:00
108abb8aba
fix twitter card image AGAIN 2024-10-03 18:01:30 +03:00
883ac0a154
fix twitter card image 2024-10-03 17:58:55 +03:00
5bf7dfbd7f
Merge pull request #1 from vlindhol/simple-hugo
From-scratch hugo setup
2024-10-03 17:54:46 +03:00
22 changed files with 341 additions and 174 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
content/blog/*.md
# Created by https://www.toptal.com/developers/gitignore/api/hugo,macos,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=hugo,macos,visualstudiocode

3
.gitmodules vendored
View File

@ -0,0 +1,3 @@
[submodule "content-sources/obsidian-default"]
path = content-sources/obsidian-default
url = git@git.lindholm.dev:vlindhol/obsidian-default.git

View File

@ -1,3 +1,5 @@
# Development
You need Hugo to build this site:
```
@ -5,3 +7,8 @@ $ brew install hugo
```
then use `hugo server` to run the dev server.
## Update blog posts from Obsidian
1. `git submodule update --remote`
2. `./scripts/generate-blog-posts.sh`

@ -0,0 +1 @@
Subproject commit ef860ef26b98a15e5152c2a76da122d2dcef8c2e

View File

@ -1,9 +1,24 @@
Hi! My name is **Ville Lindholm** and I'm a freelance software engineer.
---
title: "Home"
---
I've spent a lot of my professional life building web apps, mostly using full-stack **TypeScript** with **React** as the front-end library. However, I'm a passionate programming-language.-and-paradigm junkie, and will jump at the chance to work with (for example):
{{< rawhtml >}}
* Rust
* Elixir
* Any Lisp
Get in touch via e-mail, the address is `<my first name>@<my last name>.dev`.
<div style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; height: 50vh;">
<img src="/images/portrait.jpeg" alt="Picture of Ville Lindholm" style="border-radius: 50%; width: 150px; height: 150px;" />
<hgroup>
<h1>Ville Lindholm</h1>
<p>Full-stack Software engineer</p>
<p>Helsinki, Finland</p>
</hgroup>
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: center;">
<div style="display: flex; gap: 0.25rem;">
<a href="https://github.com/vlindhol"><i class="fa-brands fa-github"></i></a>
<a href="https://www.linkedin.com/in/villelindholm/"><i class="fa-brands fa-linkedin"></i></a>
<a href="https://qoto.org/@ville" rel="me"><i class="fa-brands fa-mastodon"></i></a>
<a href="https://bsky.app/profile/vlindholm.bsky.social"><i class="fa-brands fa-bluesky"></i></a>
</div>
</div>
<div>Hello</div>
</div>
{{< /rawhtml >}}

1
content/blog/.gitkeep Normal file
View File

@ -0,0 +1 @@
We will generate blog posts here using `scripts/generate-blog-posts.sh`.

View File

@ -0,0 +1,103 @@
---
title: "My experience contributing to Servo"
date: 2024-12-09T12:14:57+03:00
tags: \["programming", "rust"\]
draft: true
---
I've been a long-time Rust enthusiast, but haven't worked a lot with Rust in a real way. Thus, as I found myself with some spare time during my sabbatical, I decided to find an open source Rust project to contribute to, and Servo fit that bill perfectly.
Why Servo? It's a browser engine written entirely in Rust, started at Mozilla, where Rust was invented. The stated (and unstated) goals tick a lot of boxes for me:
* The browser landscape is practically a Chrome monoculture, I want to help counteract that.
* The future of UI for most apps will either be a more polished PWA experience (which depends on OS maker support) or less memory and processor intensive desktop web apps á la Electron (which doesn't). Servo is designed to be embeddable, i.e. "used as a library", so it fits nicely into either future.
* Compared to incumbents, Servo is designed for GPUs and parallel processing from the ground up. It's state of the art in many ways.
* I like building web apps, so this gives me a deeper appreciation of how the DOM and rendering really works.
## Orienting myself in the project
As a start, I headed straight to the [Servo Github repo](https://github.com/servo/servo/). I quickly found myself reading [the Servo Book](https://book.servo.org/), which is meant to serve as the one-stop shop for documentation on using/hacking Servo.
I'll be quick to point out that while there is some useful info in the book, it definitely doesn't feel complete nor polished. Information is also found in the [wiki at the Github repo](https://github.com/servo/servo/wiki), with many pages currently only containing "this content has moved" links to the book, while other pages like the [2025 roadmap](https://github.com/servo/servo/wiki/Roadmap) fits in there naturally.
Last not but least I encourage would-be contributors to join the [Servo Zulip chat](https://servo.zulipchat.com/), which is a great place to ask for guidance and direction.
## My contribution: an XPath parser and evaluator
My strategy for finding something to work on wasn't very sophisticated. I spent a few days looking at newly-opened Issues in Github, fixing some truly miniscule bugs and linter warnings. After a while I happened upon an issue regarding [htmx not working](https://github.com/servo/servo/issues/34060), due to XPath queries not being implemented at all.
This seemed perfect: a tightly defined, reasonably substantial piece of work that nobody else would probably deem sexy enough to want to pick up any time soon, leaving me plenty of time to experiment and learn. Besides, I have a soft spot for `htmx`, a library I've reached for a few times when wanting to keep a website light on the JS. I want Servo to support it!
### The story of an XPath implementation
This project was as much about doing Rust "properly" as it was implementing the feature. I started out looking at crates that would do most of the work for me, but turns out that XML and XPath isn't super hot in 2024 🙈. There were some options available for parsing and evaluating XPath, but the implementations all wanted to parse the XML themselves and traverse the node tree using their own datastructures. Servo obviously has its own DOM Node implementation, and I want to use that since it's already in memory.
Last but not least, the XPath 1.0 standard (there's also 2.0 and 3.0 available) is for pure XML, whereas "XPath for DOM" lives in a slightly murky grey area that is about 95% XPath 1.0 and 5% "how browsers actually do it". Turns out that there's a need for someone to write down the actual spec!
At this point, however, I was more interested in getting some results, so I soon decided to write my own parser and evaluator, right in the Servo repo.
Step one was the parser, which could almost be a separate crate in and of itself, since it's all about converting a string into an AST tree. Maybe one day it'll be a crate, but for now it's just a single `parser.rs` file. I've used [nom](https://github.com/rust-bakery/nom) to great effect before, so that's what I ended up using here as well. In the long term it would be great to make a hand-written parser with really good error messages, but this was a good enough start. Luckily the XPath 1.0 grammar is pretty clearly defined, and is exactly the same in the DOM, so this part was a bit bothersome but not too challenging.
Step two was the evaluator, which takes the AST produced by the parser in step one, and a "starting node" (called the *context node*) from the DOM tree, and produces a valid `XPathResult`, which is either a primitive value or (more commonly) a set of matching DOM nodes.
At this point I really needed to get my hands dirty with Servo.
### Adding DOM features in Servo: the moving parts
Servo is a Cargo workspace, with the gargantuan `script` module being where all my work needed to take place. This is where everything DOM and JavaScript-related happens. The main things I ended up needing to learn was:
1. How memory management works (spoiler: the JS runtime does garbage collection)
1. WebIDL
1. Running WPT tests effectively
**Memory management**, which can get tricky in any Rust project, turned out to be quite painless (from my point of view). There's a [detailed writeup](XXX) about it, but the gist of it is:
1. All structs that correspond to classes in the DOM spec (e.g. `Element`) are marked with `#[dom_struct]`, which among other things is an alias for `#[derive(JSTraceable)]`.
1. Once the JS runtime (called SpiderMonkey) is aware of the existence of such a struct, its garbage collector looks at every field, looking for other `JSTraceable` contents. This type of GC is literally called [tracing garbage collection](XXX).
1. Once SpiderMonkey can't trace a `JSTraceable` object, the memory can safely get freed by the garbage collector *i.e. not by Rust logic*.
1. Not every `JSTraceable` struct lives inside another `JSTraceable`, so you can create a new GC "root" by saying `DomRoot::new(&ref_to_a_dom_struct)`. A root is simply one of many "starting points" for the GC's tracing.
From a Rustacean's perspective this means that for complex tree structures like the DOM, one doesn't need to think quite so much about lifetimes, since one rarely stores plain references to anything, put rather an opaque "this value is part of the DOM and thus gets GC'd" type. Like so:
````rust
// "traditional" Rust
struct Node {
parent_node: Option<&Node>,
}
// GC'd by SpiderMonkey
#[dom_struct]
struct Node {
parent_node: Dom<Option<Node>>
}
````
`Dom<T>` implement `Deref`, so you can still get ahold of a `&Node` by saying `&some_node.parent_node` etc. And you can at any time safely convert a `&Node` back to a traced `Dom<Node>` with `DomRoot::from_ref(some_node_ref)` (`DomRoot<T>` is a `Root<Dom<T>>`, the `Root` signifies that a new root has been stored for GC).
Are there any drawbacks? The main thing I've noted so far is that there are a lot of roots created "just in case", since it can be quite difficult to determine whether a root should be created or not. Picture a simple document with no JS scripting: optimally there would be only ONE root, which would be the containing `Document`, and all DOM objects can be found through that. But as soon as any type of JS gets involved, it's possible that the same object is reachable from many roots, which makes the garbage collector "double count" the same object. This is harmless, because all we care about is that the count is >0, but it can get wasteful.
As a fairly new Rustacean, I was thankful for the option of being able to ignore lifetimes, since an XPath evaluator traverses the DOM tree quite intensely. At first I tried using only pure `&Node` references, but it quickly got messy. Someone more seasoned could probably optimize the code, but now I ended up creating a lot of extraneous roots for simplicity of the implementation.

View File

@ -1,42 +0,0 @@
---
title: "If by Rudyard Kipling"
date: 2024-10-02T12:14:57+03:00
tags: ["poetry"]
draft: false
---
If you can keep your head when all about you
Are losing theirs and blaming it on you,
If you can trust yourself when all men doubt you,
But make allowance for their doubting too;
If you can wait and not be tired by waiting,
Or being lied about, don't deal in lies,
Or being hated, don't give way to hating,
And yet don't look too good, nor talk too wise:
If you can dream—and not make dreams your master;
If you can think—and not make thoughts your aim;
If you can meet with Triumph and Disaster
And treat those two impostors just the same;
If you can bear to hear the truth you've spoken
Twisted by knaves to make a trap for fools,
Or watch the things you gave your life to, broken,
And stoop and build 'em up with worn-out tools:
If you can make one heap of all your winnings
And risk it on one turn of pitch-and-toss,
And lose, and start again at your beginnings
And never breathe a word about your loss;
If you can force your heart and nerve and sinew
To serve your turn long after they are gone,
And so hold on when there is nothing in you
Except the Will which says to them: Hold on!'
If you can talk with crowds and keep your virtue,
Or walk with Kings—nor lose the common touch,
If neither foes nor loving friends can hurt you,
If all men count with you, but none too much;
If you can fill the unforgiving minute
With sixty seconds' worth of distance run,
Yours is the Earth and everything that's in it,
And—which is more—you'll be a Man, my son!

View File

@ -6,26 +6,29 @@
data-placement="left"><i class='bx bx-printer'></i> PDF version</button>
</div>
<div class="onlyPrint">
<div class="grid">
<div><i class='bx bx-envelope'></i> &langle;first_name&rangle;@&langle;last_name&rangle;.dev</a></div>
<div><i class='bx bx-globe'></i> <a href="https://lindholm.dev">https://lindholm.dev</a></div>
<div><i class='bx bxl-github'></i> <a href="https://github.com/vlindhol">@vlindhol</a></div>
<div><i class='bx bxl-linkedin-square'></i> <a
href="https://www.linkedin.com/in/villelindholm/">villelindholm</a></div>
</div>
<hr />
<div style="display: flex; flex-wrap: wrap; gap: 1rem;">
<div><i class="fa-regular fa-envelope"></i> <span id="lindholm-email" /></div>
<div><i class="fa-brands fa-github"></i> <a href="https://github.com/vlindhol">@vlindhol</a></div>
<div><i class="fa-brands fa-linkedin"></i> <a href="https://www.linkedin.com/in/villelindholm/">villelindholm</a></div>
<div class="onlyPrint"><i class="fa-solid fa-globe"></i> <a href="https://lindholm.dev">https://lindholm.dev</a></div>
<div><i class="fa-solid fa-location-dot"></i> Helsinki, Finland</div>
</div>
<hr />
<div id="description">
<p>
I am an experienced full stack software engineer currently based in Helsinki, Finland. I am interested in
I am an experienced <b>full stack software engineer</b> currently based in <b>Helsinki, Finland</b>. I am
interested in
writing code that is readable, (appropriately) scalable and
well-architected. I am always curious about new paradigms, patterns and tools.
</p>
<p>I tend to <b>gravitate towards the backend</b>, since that's usually where the growing pains are for services
that are scaling up. In the frontend I am <b>strong with React</b>, but am increasingly interested in
vanilla JS and other natively supported features that browsers have nowadays.</p>
<p>
In my spare time I write poetry and fiction, read a lot of books and dabble in improvisational theatre
and windsurfing. I also contribute to open source projects when I have the time!
I've been programming since childhood, first by building Web 1.0-style web pages, then by coding games. At
university,
I used my programming talents to simulate plasma at a <b>fusion energy lab</b> for my <b>physics degree</b>.
</p>
</div>
<dl>
@ -34,20 +37,28 @@
</dt>
<dd>
<div class="note-box">
<strong>Years of experience: ~12 years</strong>. This is a rough estimate: I started learning
programming as a teen in 1999 and my first programming job was in 2002,
constructing web shops in Perl for my local ISP. I then worked on and off with coding until I graduated
university, so it's hard to put a number on my "years of experience".
<strong>Years of software engineering experience: ~12 years</strong>. This is a rough estimate; I've
been writing code most
of my life and I believe that deserves to be included, so a tacked on a few extra years to represent
that!
</div>
</dd>
<dd>
<div class="no-page-break">
<h4><a href="https://memfault.com/">Memfault</a> <span>2021 &mdash; 2024</span></h4>
<h4><a href="https://memfault.com/">Memfault (YC19)</a> <span>2021 &mdash; 2024</span></h4>
<p>
<b>Full-stack developer</b> for a web app that ingests data from millions
of IoT devices. Migrated the service from Heroku to AWS. Developed
a more advanced permission system (backend + UI). Developed a "batch operations"
API for common tasks in the UI (think e-mail batch operations).
of IoT devices.
<ul>
<li>Migrated the service from Heroku to AWS, reducing costs ~50%.</li>
<li>Developed a more advanced permission system (backend + UI), unlocking a new customer size
segment.</li>
<li>Developed "batch operations" for common tasks in the UI (think e-mail batch operations),
preventing churn of large customers.
</li>
<li>PostgreSQL fine-tuning and optimization, enabling ingestion of 100% more devices.</li>
</ul>
</p>
<p>
<i>
@ -61,11 +72,15 @@
2021</span></h4>
<p>
<b>Senior Software Developer</b>. Backend and admin/content creation UI of
an educational mobile app for children (built with Unity). Implemented a
GraphQL API for the admin web app, and substantial UI/UX improvements. Added a
robust DevOps pipeline instead of manual deployments, bringing deployment time
down from hours/days to minutes (on both AWS and AWS China). Also worked on the
product itself, a Unity-based mobile app, mainly on asset organization improvements.
an educational mobile app for children (built with Unity).
<ul>
<li>Converted a REST API to GraphQL for the admin web app, plugging dozens of security holes.</li>
<li>Added a robust DevOps pipeline instead of manual deployments, bringing deployment time down from
hours/days to minutes.</li>
<li>Asset organization improvements in the Unity-based mobile app, reducing runtime bugs by 25%.
</li>
</ul>
</p>
<p>
<i>
@ -78,23 +93,25 @@
&mdash;
2019</span></h4>
<p>
<b>Lead developer</b> for <a href="https://www.ebmeds.org/en/">EBMEDS</a>, a CE class IIa
medical
device (it's purely software
though). Did a full rewrite of an extremely legacy codebase into modern TypeScript, as well as
moving the service into AWS.
</p>
<p>
Also worked on the early version of the
<a href="https://www.hl7.org/fhir/overview.html">HL7 FHIR</a> standard, both implementing
parts of it for the Finnish <a href="https://www.omaolo.fi?lang=en">OmaOlo</a> national
self-assessment platform (available to all
Finnish citizens to get automated access to appropriate public healthcare), as well as taking
part in working group meetings and conferences related to the standard itself.
<b>Lead developer</b> for <a href="https://www.ebmeds.org/en/">EBMEDS</a>, a CE-marked, MDR class
IIa
medical device (it's purely software though).
<ul>
<li>Full conversion of an extremely legacy JScript(!) codebase into modern TypeScript.</li>
<li>Implemented CI/CD, deploying with horizontal scaling on AWS ECS.</li>
<li>Made the contents of the service searchable, with ElasticSearch.</li>
<li>Integrated the service for use in the Finnish <a
href="https://www.omaolo.fi?lang=en">OmaOlo</a>, a national health service usable by 5.5
million users a.k.a. citizens.</li>
<li>Early contributions to the global <a href="https://www.hl7.org/fhir/overview.html">HL7 FHIR</a>
standard, mainly by taking
part in working group meetings and conferences.</li>
</ul>
</p>
<p>
<i>
Stack: AWS, TypeScript.
Stack: Express (TypeScript), Docker, Docker Swarm, AWS, AWS ECS, ElasticSearch
</i>
</p>
</div>
@ -119,19 +136,20 @@
</div>
</dd>
</dl>
<dl>
<dl class="no-page-break">
<dt>
<h3>Skills</h3>
</dt>
<dd>
<div class="no-page-break">
<div>
<h4>Proficient</h4>
<p>
<strong>Languages/Frameworks</strong>: TypeScript, Python, React, HTML and CSS.
<strong>Languages/Frameworks</strong>: TypeScript, Node.js, Express.js, Python, Flask, React, HTML
and CSS.
</p>
<p>
<strong>Databases/Infra</strong>: REST and GraphQL APIs, AWS, Terraform, Ansible, PostgreSQL,
ClickHouse, Redis, RabbitMQ, shell scripting.
ClickHouse, Redis, RabbitMQ, shell scripting. Also, data modeling and DB schema design/optimization.
</p>
<p>
<strong>Other</strong>: Scrum, Canban and Shape Up work processes
@ -147,6 +165,17 @@
</div>
</dd>
</dl>
<dl>
<dt>
<h3>Other hobbies/skills</h3>
</dt>
<dd>
<div class="no-page-break">
In my spare time I write poetry and fiction, read a lot of books and dabble in improvisational theatre
and windsurfing. I also contribute to open source projects when I have the time!
</div>
</dd>
</dl>
<dl>
<dt>
<h3>References</h3>

View File

@ -4,23 +4,6 @@ title: Lindholm Software
copyright: © Ville Lindholm
disableHugoGeneratorInject: true
params:
profileMode:
enabled: true
title: Ville Lindholm
subtitle: Freelance developer
imageUrl: "/images/portrait.jpeg"
imageTitle: "Portrait of Ville"
# imageWidth: 120 # custom size
# imageHeight: 120 # custom size
buttons:
- name: CV
url: "/cv"
socialIcons:
- name: Github
url: https://github.com/vlindhol/
- name: X
url: https://x.com/lindholm_dev
- name: Linkedin
url: https://www.linkedin.com/in/villelindholm/
markup:
highlight:
style: solarized-dark

View File

@ -1,51 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Open Graph metadata -->
<meta property="og:title" content="Lindholm Software">
<meta property="og:description" content="Ville Lindholm is a freelance software developer.">
<meta property="og:image" content="https://lindholm.dev/portrait.jpg">
<meta property="og:url" content="https://lindholm.dev">
<meta property="og:type" content="website">
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caprasimo&display=swap" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/junicode-2" rel="stylesheet">
<!-- icons -->
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- pico helps with some basic CSS styling -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.css">
<!-- the rest is ours -->
<link rel="stylesheet" href="/style.css">
<title>{{ .Site.Title }}</title>
</head>
<body>
<header class="container noPrint">
<nav>
<ul>
<li>
<h4><a href="/" class="contrast">{{ .Site.Title }}</a></h4>
</li>
</ul>
<ul>
<li><a href="/cv" class="contrast">CV</a></li>
<li><a href="/blog" class="contrast">Blog</a></li>
</ul>
</nav>
</header>
<main class="container">
{{ block "main" . }}{{ end }}
</main>
</body>
{{ partial "head.html" . }}
<body>
{{ partial "header.html" . }}
<main class="container">
{{ block "main" . }}{{ end }}
</main>
</body>
</html>

View File

@ -0,0 +1,10 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ range .Pages.ByPublishDate.Reverse }}
<p>
<a href="{{ .RelPermalink }}">{{ .Title }} {{ .Date | time.Format "January 2, 2006" }}</a>
</p>
{{ end }}
{{ end }}

View File

@ -1,3 +1,4 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ end }}

View File

@ -1,16 +1,3 @@
{{ define "main" }}
<div
style="text-align: center; display: flex; justify-content: center; flex-direction: column; align-items: center; height: 50vh;">
<img src="/images/portrait.jpeg" alt="Picture of Ville Lindholm"
style="border-radius: 50%; width: 150px; height: 150px;" />
<hgroup>
<h1>Ville Lindholm</h1>
<p>Freelance Software engineer</p>
</hgroup>
<div style="display: flex;">
<a href="https://github.com/vlindhol"><i class='bx bxl-github'></i></a>
<a href="https://x.com/lindholm_dev"><i class='bx bxl-twitter'></i></a>
<a href="https://www.linkedin.com/in/villelindholm/"><i class='bx bxl-linkedin-square'></i></a>
</div>
</div>
{{ .Content }}
{{ end }}

View File

@ -0,0 +1,17 @@
<script>
window.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('span#lindholm-email').forEach(function (span) {
span.innerHTML = "Rendering email...";
span.setAttribute('aria-busy', 'true');
});
setTimeout(function () {
let first = 'ville';
let at = '@';
let last = 'lindholm';
document.querySelectorAll('span#lindholm-email').forEach(function (span) {
span.innerHTML = `${first}${at}${last}.dev`;
span.removeAttribute('aria-busy');
});
}, 2000);
});
</script>

View File

@ -0,0 +1,29 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Open Graph metadata -->
<meta property="og:title" content="{{ .Site.Title }}">
<meta property="og:description" content="Ville Lindholm is a freelance software developer.">
<meta property="og:image" content="https://lindholm.dev/images/portrait.jpeg">
<meta property="og:url" content="https://lindholm.dev">
<meta property="og:type" content="website">
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caprasimo&display=swap" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/junicode-2" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<!-- pico helps with some basic CSS styling -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.css">
<!-- the rest is ours -->
<link rel="stylesheet" href="/style.css">
<title>{{ .Site.Title }}</title>
{{ partial "email-obfuscator.html" . }}
</head>

View File

@ -0,0 +1,11 @@
<header class="container noPrint">
<nav>
<ul>
<li><h4><a href="/" class="contrast">{{ .Site.Title }}</a></h4></li>
</ul>
<ul>
<li><a href="/cv"><h5>CV</h5></a></li>
<li><a href="/blog"><h5>Blog</h5></a></li>
</ul>
</nav>
</header>

View File

@ -0,0 +1 @@
{{ .Inner | safeHTML }}

Binary file not shown.

Binary file not shown.

31
scripts/generate-blog-posts.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Detect the platform
PLATFORM="$(uname -s)"
ARCH="$(uname -m)"
# Determine which binary to use
if [ "$PLATFORM" = "Darwin" ]; then
if [ "$ARCH" = "arm64" ]; then
BINARY="obsidian-export-aarch64-apple-darwin"
else
echo "Unsupported architecture on macOS: $ARCH"
exit 1
fi
elif [ "$PLATFORM" = "Linux" ]; then
if [ "$ARCH" = "x86_64" ]; then
BINARY="obsidian-export-x86_64_unknown-linux-gnu"
else
echo "Unsupported architecture on Linux: $ARCH"
exit 1
fi
else
echo "Unsupported platform: $PLATFORM"
exit 1
fi
# Make sure the binary is executable
chmod +x "$BINARY"
# Run the appropriate binary
./"$BINARY" --start-at content-sources/obsidian-default/Blog content-sources/obsidian-default/ content/blog/

View File

@ -25,6 +25,25 @@ h5 {
}
}
/* avoid page breaks for this element when printing */
@media print {
.no-page-break {
break-inside: avoid;
}
}
/* smaller font when printing, Pico is pretty big */
@media print {
html {
font-size: 16px;
}
.container {
max-width: 100%;
}
}
/* for displaying notes */
.note-box {
border: 1px solid #cce5ff;