# Rasmus P - Fullstack developer with a love for UX
> Full-stack developer from Denmark with a passion for building seamless and engaging web experiences. I enjoy navigating the space between elegant frontends and solid backend architecture—bringing ideas to life with both creativity and precision.
## Pages
- [About](https://rasmusp.com/)
- [TILs](https://rasmusp.com/tils)
- [Uses](https://rasmusp.com/uses)
## Articles
- [Generate Open Graph Images in Astro](https://rasmusp.com/tils/astro-og-image-generation)
- [Better Error Pages in Nuxt — One for Users, One for Developers](https://rasmusp.com/tils/better-error-pages-in-nuxt-one-for-users-one-for-developers)
- [Release-it — Automation for versioning and changelog](https://rasmusp.com/tils/release-it-automation-for-versioning-and-changelog)
- [Tailwind CSS utility feature](https://rasmusp.com/tils/tailwind-css-utility-feature)
## Socials
- [GitHub](https://github.com/djrasmusp)
- [LinkedIn](https://linkedin.com/in/djrasmusp)
- [Bluesky](https://bsky.app/profile/rasmusp.com)
- [X](https://x.com/djrasmusp)
---
## Rasmus P | Fullstack developer
# Hi there! I'm Rasmus
I'm a full-stack developer from Denmark🇩🇰 with a passion for building seamless and engaging web experiences. I enjoy navigating the space between elegant frontends and solid backend architecture—bringing ideas to life with both creativity and precision.
Driven by curiosity and a love for lifelong learning, I'm always exploring new technologies and sharing insights with others.
I use HTML, CSS, TypeScript and JavaScript to craft high-quality, performant websites and applications. I continuously evolve with the landscape, currently working deeply with React.js, Vue.js, Node.js, while also exploring serverless architectures and agentic engineering.
When I'm not immersed in code, you'll likely find me diving into the latest tech trends, experimenting in the kitchen, DJing a rooftop party, getting lost in a good movie, or simply enjoying the little moments that makes life great.
### Experiences
- Senior Frontend Developer at Knowit Experience, Aarhus, DK (Nov 2023–present)
- On a daily basis I work in Team Gaia, where we both develop brand-new web solutions but also maintain and update existing clients solutions.
Leading frontend development across multiple projects, collaborating closely with designers, system architects, backend developers, and stakeholders to deliver high-quality, user-focused web solutions.
Technologies: React, Vue, Nuxt, Vitest, Git, Jira, Azure DevOps, Payment Gateway API, Ecommerce, Digital self-service, Umbraco, Tailwind CSS, TypeScript, JavaScript, Nitro, Node.js, Agentic Engineering
- Senior Developer at Novicell, Aarhus, DK (Dec 2021–Oct 2023)
- Developing and maintaining WordPress & WooCommerce sites as a key member of the WordPress team, delivering custom, scalable solutions for a diverse range of clients.
- Leading frontend development across multiple projects, collaborating closely with designers and stakeholders to deliver high-quality and user-focused web solutions.
- Built a custom learning portal for a public sector project, providing a user-friendly and scalable platform to support educational initiatives.
Technologies: WordPress, WooCommerce, PHP, Alpine.js, Tailwind CSS, Ecommerce, Customer Portals, TypeScript, JavaScript, Jira, Trello, Git, CMS, MySQL
- Developer at Aveo, Aarhus, DK (Oct 2020–Nov 2021)
- Developing and maintaining WordPress & WooCommerce sites as part of the technical team, delivering custom, scalable solutions for a diverse range of clients.
Specializing in custom plugin development, theme extensions, and bespoke coding to meet client requirements and enhance website functionality, performance, and user experience.
Technologies: WordPress, WooCommerce, MySQL, Ecommerce, CSS, JavaScript, PHP
- Self-employed Developer (Jan 2008–Dec 2020)
- Built several custom Facebook Page gateway apps for artists, nightclubs, and brands, driving higher audience engagement and streamlining page management through automation.
Built a viral Christmas calendar campaign deployed on 20+ Facebook Pages, attracting 12,000+ participants and driving high user engagement throughout December
Built a mobile web application for Secret Customer reporting, streamlining feedback collection and analytics.
Built a Guestlist SMS platform that let nightclub guests sign up via shortcode messages — providing managers with a streamlined overview and eliminating the chaos of managing hundreds of individual SMS submissions weekly.
Delivered modern websites for small and medium-sized businesses, focusing on performance and user experience.
Technologies: Facebook Web Apps, PHP, WordPress, Tailwind CSS, Vue, JavaScript, Astro, SMS Gateway API, Ecommerce, REST API, Integrations, Nuxt, Coolify, Cloudflare
### Skills
Programming Languages: TypeScript, JavaScript, CSS3, HTML5, PHP, SQL
Libraries & Frameworks: Vue.js, Nuxt, React, Node.js, Alpine.js, Laravel, WordPress, WooCommerce, MySQL, PostgreSQL, Umbraco, Tailwind CSS, Vite, Vitest, Storybook, Astro, Oxlint / Oxfmt
Tools & Platforms: Git, GitHub, Vercel, Coolify, Figma, WebStorm, Bruno, Azure DevOps, Jira, Cloudflare, Postman, OpenAI, Cursor, NPM, PNPM, GitHub Actions, Claude Code
---
## Today I learned
# Today I learned
A growing collection of insights, learnings and ideas from my daily adventures.
---
## Uses
# Uses
Here’s what’s in my digital toolbox — the gear I use to write code, craft interfaces, and stay productive.
## Editor + Terminal
- My main battlestation is **WebStorm**, unmatched for TypeScript, Vue, and React projects. For backend work, I use **Rider** for C#/.NET and **PHPStorm** for PHP-based projects.
- I'm currently experimenting with **Cursor**. AI-assisted coding feels like the future, and I’m curious to see how it fits into my daily workflow.
- **Ghostty** is my terminal of choice: sleek, minimal, and lightning fast. I still keep **iTerm2** around for nostalgia.
- I mostly stick with **Chrome**, not perfect but reliable and fast enough to stay out of my way.
## Desktop Apps & Tools
- **Raycast** is my command bar sidekick. My dock is basically retired.
- **Homebrew** automates my setup, from CLI essentials to niche dev tools.
- Once a heavy **Adobe** user (Photoshop, Illustrator, InDesign, After Effects), I now prefer **Figma** for its clean, collaborative interface.
- **TablePlus** handles all my SQL database work with a clean and intuitive UI.
- **1Password** keeps my credentials safe and synced across devices.
- **MiniSim** & **Xcode** serve as my mobile testing grounds, where I push layouts to their limits and debug scroll views.
- **Raindrop.io** is my digital library for articles, tools, inspiration, and occasionally a meme disguised as research.
- **BetterTouchTool** is my Swiss Army knife for window management. I even mapped F13–F15 for maximum obscure key-flexing.
## Desk Setup
- My daily driver is a **13" Apple MacBook M1 MAX** with 32GB RAM — small form, big power.
- Connected to a **27" Apple Studio Display**, a stunning screen that helps me maintain focus.
- I sit like royalty in a **Herman Miller Aeron**, one of my smartest purchases, upgraded with **Rollerblade wheels** to glide into meetings effortlessly. Shoutout to [**Wes Bos**](https://wesbos.com/uses) for the inspiration.
- The desk is a custom-cut countertop mounted on an **IKEA standing gaming desk** frame, minimal yet rock solid.
- Keyboard: **Apple Magic Keyboard with numpad**. Mouse: **Magic Mouse 2** — love it or hate it.
- Audio setup: **Pioneer DJ studio monitors**, perfect for deep focus or casual beat-making.
## Backup Strategy
- My system is continuously backed up using **Backblaze**, ensuring nothing important is ever lost.
- **Time Machine** runs local backups for an extra layer of safety.
- All code is version-controlled and backed up on **GitHub**, with private repositories for personal projects and organization repos for team work. I also leverage **GitHub Actions** for automated testing and deployment workflows.
---
## Generate Open Graph Images in Astro
If you’ve ever shared a link on Slack, LinkedIn, or Twitter and thought “hmm… that preview could look better”.
Open Graph images are used by social platforms to display visual previews when a page is shared. By automatically generating these images in Astro, you can ensure consistent, dynamic previews without manually designing an image for each page.
In this guide, I’ll walk you through a practical and flexible way to generate Open Graph images automatically in Astro, using Satori, Sharp, and React. The goal is simple: no manual image design, no forgotten previews — just consistent, on‑brand images for every page.
## Setup
### Prerequisites
We assume the following basic Astro project structure with dynamic routes:
``` txt
/src/
/pages/
[...slug].astro
```
### Installation
To generate OG images, install the following packages:
* **`Satori`** - Converts a React/JSX template into an SVG. It's a lightweight library that renders JSX to SVG, making it perfect for server-side image generation.
* **`Sharp`** - A high-performance image processing library that converts the SVG into PNG, WebP, JPG or other formats. Sharp is much faster than alternatives like `jimp` or native Node.js image processing.
* **`React`** - Used to build the template in JSX/TSX. Even though you're not building a React app, React is required as a peer dependency for Satori to parse JSX syntax.
```bash
npm i react satori sharp
```
## Generating Open Graph images
### Basic Satori setup
Satori is the core library that converts JSX/React components into SVG. It's designed specifically for server-side rendering and supports a subset of CSS properties and HTML elements.
A minimal Satori example:
```typescript
await satori(
'
Hey world
',
{
height: 630,
width: 1200,
}
)
```
`satori()` takes two arguments:
* **element** — HTML string or JSX/TSX component. You can pass a string directly, but using JSX components gives you more flexibility and type safety.
* **options** — Configuration object with width, height, fonts, styles, and more. The most important options are:
- `width` and `height` - Dimensions in pixels. In this guide we set it to the recommend size 1200x630. This size works well across all major social platforms including Twitter, Facebook, LinkedIn, and Discord.
- `fonts` - Array of font definitions for custom typography
This guide focuses on width, height and fonts. [Full documentation can be found in the Satori README.](https://github.com/vercel/satori#readme)
**Performance note:** Satori is fast, but generating images on every request can impact performance. Consider caching generated images or generating them at build time for static sites.
### Implementing generateOgImage()
The `generateOgImage()` function is the core of our OG image generation. It takes page data (like title, description, etc.) and returns a binary image buffer that can be served as an HTTP response.
**Basic function**
```typescript title="generateOgImages.ts"
export default async function generateOgImage({ title} : { title: string }) {
const svg = await satori('{ title }
', {
height: 630,
width: 1200,
})
return new Uint8Array(svg)
}
```
**Why Uint8Array?** Satori returns an SVG string, which we convert to a `Uint8Array` for efficient binary handling. This format works well with Astro's `Response` API when serving images.
**Error handling:** Consider adding try-catch blocks in production to handle potential errors gracefully, especially when dealing with font loading or template rendering.
#### Adding Fonts
To use custom fonts such as Inter, load the font file and include it in Satori's font configuration. Using custom fonts is crucial for maintaining brand consistency across your OG images.
**Font format considerations:**
- Satori works with `.woff`, `.ttf`, and `.otf` fonts. `.ttf` and `.otf` are faster to load and parse, but they are typically larger in size. `.woff` is a good balance of size and parsing speed.
- You can load multiple font weights (regular, bold, etc.) by adding multiple font objects
- Font files should be placed in your `public` directory or imported as static assets
```diff lang="ts" title="generateOgImages.ts"
+import path from 'node:path'
+import { readFile } from 'node:fs/promises'
+const InterRegularFontFile = await readFile(path.resolve('public/fonts/InterRegular.woff'))
export default async function generateOgImage({ title} : { title: string }) {
- const svg = await satori('{ title }
', {
+ const svg = await satori('{ title }
', {
height: 630,
width: 1200,
+ fonts: [{
+ name: "Inter",
+ weight: 400 as const,
+ style: "normal" as const,
+ data: InterRegularFontFile
+ }]
})
return new Uint8Array(svg)
}
```
**Notes:**
* The font object must include name, weight, style, and data. The `as const` assertions ensure TypeScript recognizes the literal types.
* The loaded font has to be an ArrayBuffer (web) or Buffer (Node.js). `readFile` returns a Buffer in Node.js environments.
* **Multiple weights:** To use bold or italic variants, add additional font objects to the array with different weights/styles.
* **Font loading:** Fonts are loaded at module initialization. For better performance, consider lazy loading fonts only when needed.
### Building a Template with JSX/TSX
Using JSX allows you to create flexible, reusable templates. This approach separates your design logic from the image generation logic, making it easier to maintain and update your OG images.
```tsx
//template.tsx
// @ts-nocheck
function template(title: string) {
return (
{ title }
)
}
export default template
```
**The `@ts-nocheck` comment:** Satori's JSX support is a subset of React's JSX, so TypeScript may complain about certain patterns. The `@ts-nocheck` comment disables type checking for this file. Alternatively, you can configure your `tsconfig.json` to be more lenient with JSX.
**Styling in Satori:** Satori supports a limited subset of CSS properties. Common supported properties include:
- Layout: `display`, `flexDirection`, `justifyContent`, `alignItems`
- Spacing: `padding`, `margin`, `gap`
- Typography: `fontSize`, `fontWeight`, `lineHeight`, `color`
- Background: `backgroundColor`, `backgroundImage`
- Borders: `border`, `borderRadius`
#### Using Tailwind CSS with Satori
Satori supports Tailwind through the `tw` attribute, which is a convenient way to style components without writing inline styles. This is especially useful if you're already using Tailwind in your project.
**Important limitations:**
- Not all Tailwind classes are supported
- Complex utilities like `backdrop-blur` may not work
- Always test your design to ensure it renders correctly
```diff lang="tsx"
//template.tsx
// @ts-nocheck
function template(title: string) {
return (
-
+
{ title }
)
}
export default template
```
**Mixing styles and Tailwind:** You can combine inline `style` props with `tw` attributes. Inline styles take precedence, which is useful for dynamic values or properties not supported by Tailwind.
And this is how we will use it:
```diff lang="ts" title="generateOgImages.ts"
+import template from 'template.tsx'
const InterRegularFontFile = await readFile(path.resolve('public/fonts/InterRegular.woff'))
export default async function generateOgImage({ title} : { title: string }) {
- const svg = await satori('
{ title }
', {
+ const svg = await satori(
+ template( title ),
{
height: 630,
width: 1200,
fonts: [{
name: "Inter",
weight: 400 as const,
style: "normal" as const,
data: InterRegularFontFile
}]
})
return new Uint8Array(svg)
}
```
### Converting SVG to PNG / WebP
Satori outputs SVG, but social platforms typically prefer raster formats like PNG or WebP. SVG files can be larger and may not render consistently across all platforms.
Use Sharp to convert:
```diff lang="ts" title="generateOgImages.ts"
+import sharp from 'sharp'
const InterRegularFontFile = await readFile(path.resolve('public/fonts/InterRegular.woff'))
export default async function generateOgImage({ title} : { title: string }) {
const svg = await satori(
template( title ),
{
height: 630,
width: 1200,
fonts: [{
name: "Inter",
weight: 400 as const,
style: "normal" as const,
data: InterRegularFontFile
}]
})
+const webp = await sharp(Buffer.from(svg)).webp().toBuffer()
-return new Uint8Array(svg)
+return new Uint8Array(webp)
}
```
**Sharp configuration:** You can customize the output quality:
```typescript
const webp = await sharp(Buffer.from(svg))
.webp({ quality: 90 }) // Adjust quality (1-100)
.toBuffer()
```
**Performance tip:** Sharp operations are asynchronous and CPU-intensive. For high-traffic sites, consider caching generated images or using a CDN.
### Astro GET Integration
To serve OG images in Astro, we create a dynamic API route that generates images on-demand. This route will be called whenever a social media platform requests an OG image.
**File structure:**
Our new route structure:
``` txt
/src/
/pages/
/...slug/
og.webp.ts
[...slug].astro
```
The `[...slug]` pattern matches any path, and the `og.webp.ts` file will handle requests to `/{slug}/og.webp`.
**Implementation:**
Here is the code for the GET route for the image:
``` typescript
export async function getStaticPaths() {
const pages = await getCollection('pages')
return pages.map((page) => ({
params: { slug: page.id },
props: { page },
}))
}
export const GET: APIRoute = async function get({ props }) {
const image = await generateOgImage({
title: props.page.data.title,
})
return new Response(image, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=31536000, immutable' // Cache for 1 year
},
})
}
```
**Key points:**
- `getStaticPaths()` pre-generates all routes at build time for static sites
- The `GET` function receives the page data via `props`
- We return a `Response` with the image buffer and appropriate headers
- **Caching headers:** The `Cache-Control` header tells browsers and CDNs to cache the image, reducing server load
**Error handling:** Consider adding error handling for missing pages or generation failures:
```typescript
export const GET: APIRoute = async function get({ props }) {
try {
const image = await generateOgImage({
title: props.page.data.title,
})
return new Response(image, {
headers: {
'Content-Type': 'image/webp',
'Cache-Control': 'public, max-age=31536000, immutable'
},
})
} catch (error) {
return new Response('Failed to generate image', { status: 500 })
}
}
```
**Dynamic routes:** If you're using server-side rendering (SSR), you can use `getStaticPaths()` with `fallback: 'blocking'` or handle dynamic routes differently.
## Performance considerations
A few practical notes before you ship this to production:
- On static sites, images are generated at build time, so there’s no runtime cost
- On SSR sites, consider caching or serving images through a CDN
- Keep an eye on generation time if your templates become more complex
In most cases, the performance impact is minimal — especially compared to the consistency you gain.
## Conclusion
By combining **Satori**, **Sharp**, and **React**, you've created a powerful system for generating Open Graph images in Astro.
This workflow ensures that every page in your Astro site gets a beautiful, consistent OG image — automatically. Your social media shares will now stand out with professional, on-brand preview images that accurately represent your content.
---
## Better Error Pages in Nuxt — One for Users, One for Developers
You've just defined a beautiful custom error page design in the error.vue file in Nuxt. Everything looks beautiful for the end-user, but from a developer's perspective, all the useful error information is now gone. So how can we fix that?
In my work, I've come across projects where I, as a developer, face restrictions in my workflow — for example, when a client, due to policy restrictions, can't provide access to their API for local development. In such cases, the only way to test is by deploying the site and hoping for the best.
For a long time, I searched for a way to use a custom or the default error page depending on the environment — and to show the beautiful custom error page only in production. I reached out to [Daniel Roe](https://roe.dev/) and [Alexander Lichter](https://www.lichter.io/) from the Nuxt core team, and they pointed me in the right direction.
## Setup
Start by creating a `.env` file, or add `NUXT_SITE_ENV` to your existing one. This variable defines the current environment, e.g. **development**, **staging**, or **production**.
```dotenv
// .env
NUXT_SITE_ENV=development
```
Then, add the following to your Nuxt config file:
```ts showLineNumbers
// nuxt.config.ts
export default defineNuxtConfig(){
...
{
'app:resolve': (app) => {
if(env.process.NUXT_SITE_ENV && env.process.NUXT_SITE_ENV !== 'production' ) {
app.errorComponent = './node_modules/nuxt/dist/app/components/nuxt-error-page.vue'
}
}
}
...
}
```
Here’s what’s happening in the example above:
- On lines 3–4, we hook into `app:resolve`. Since this hook only runs at build time, we use environment variables to define which environment we're in.
- On line 5, we check if `NUXT_SITE_ENV` exists and that its value is not **production**.
- On line 6, we override the default error component path to point to Nuxt’s built-in error page located in `node_modules`.
With this setup, you can define a different error page per environment, or however you'd like.
## Conclusion
This setup might not suit everyone, but I find it a quick and effective way to provide a polished error page for end-users in production, while keeping a developer-friendly error page for development and staging environments.
---
## Release-it — Automation for versioning and changelog
You've just merged the last feature of the sprint, eager to ship it to your app. Everything looks great, and you are ready to deploy, but now you're stuck manually bumping the version, tagging the commit, pushing to GitHub, and crafting a tidy `CHANGELOG.md`. It's boring, repetitive, and prone to mistakes. How can we fix that?
Enter **Release-it** — a CLI that helps automate version bumping, changelog generation, Git tagging, GitHub releases, and more. With a few lines of configuration you can turn those 15-minute "release chores" into a single command (or a CI step) you never think about again.
## Setup
### Prerequisites
Before starting there some minor prerequisites:
| Requirement | Why it matters |
| ----------------------------------------------------------------------------- | ------------------------------------------ |
| **Git** repository | Release-it tags and pushes via Git |
| Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) | Enables automatic changelog categorisation |
| Node ≥ 16 (LTS recommended) | Required runtime for the CLI |
### Installation
Starting a fresh project? First, create a `package.json`:
```bash
npm init # creates package.json
```
Then, whether it’s a new or existing project, install the required dev dependencies:
```bash
npm install -D release-it @release-it/conventional-changelog
```
For convenience, add a script to `package.json`:
```json
// package.json
{
"scripts": {
"release": "release-it"
}
}
```
Now you can run the command `npm run release` to start the release process.
On the first run, Release-it will be prompted to choose between embedding the configuration in `package.json` or a `.release-it.json` file.
This guide assumes the latter for clarity.
## Configuration
Release-it's configuration is powerful but flexible. Let’s walk through the basics and some advanced options.
### Basic Configuration
Here's a minimal configuration to get started:
```json
// .release-it.json
{
"$schema": "https://unpkg.com/release-it@19/schema/release-it.json",
"git": {
"commitMessage": "chore: release v${version}"
},
"github": {
"release": false // toggle true to create GitHub releases
},
"npm": {
"publish": false // set true for publishing libraries
}
}
```
### Advanced Configuration Options
#### Git Configuration
```json
{
"git": {
"commitMessage": "chore: release v${version}",
"tagName": "v${version}",
"tagAnnotation": "Release v${version}",
"push": true,
"requireUpstream": true,
"requireCleanWorkingDir": true,
"requireBranch": "main"
}
}
```
- **commitMessage**: Customize the commit message format
- **tagName**: Define the Git tag format
- **tagAnnotation**: Add detailed tag annotations
- **push**: Control whether to push to remote
- **requireUpstream**: Ensure branch is tracking remote
- **requireCleanWorkingDir**: Prevent releases with uncommitted changes
- **requireBranch**: Restrict releases to specific branches
#### GitHub Configuration
```json
{
"github": {
"release": true,
"releaseName": "Release v${version}",
"releaseNotes": null,
"draft": false,
"prerelease": false,
"tokenRef": "GITHUB_TOKEN"
}
}
```
- **release**: Enable/disable GitHub releases
- **releaseName**: Customize release title
- **releaseNotes**: Override default release notes
- **draft**: Create releases as drafts
- **prerelease**: Mark releases as pre-releases
- **tokenRef**: Specify GitHub token reference
#### NPM Configuration
```json
{
"npm": {
"publish": true,
"publishPath": ".",
"access": "public",
"otp": null
}
}
```
- **publish**: Enable/disable npm publishing
- **publishPath**: Specify package location
- **access**: Control package access level
- **otp**: Two-factor authentication code
### Conventional Changelog Configuration
The `@release-it/conventional-changelog` plugin helps you maintain a standardized changelog based on your commit messages. Here's how to configure it:
```json [.release-it.json]
{
"plugins": {
"@release-it/conventional-changelog": {
"infile": "CHANGELOG.md",
"preset": {
"name": "conventionalcommits",
"types": [
{ "type": "feat", "section": "Features" },
{ "type": "fix", "section": "Bug Fixes" },
{ "type": "docs", "section": "Documentation" },
{ "type": "style", "section": "Styles" },
{ "type": "refactor", "section": "Code Refactoring" },
{ "type": "perf", "section": "Performance Improvements" },
{ "type": "test", "section": "Tests" },
{ "type": "build", "section": "Builds" },
{ "type": "ci", "section": "Continuous Integration" },
{ "type": "chore", "section": "Chores" },
{ "type": "revert", "section": "Reverts" }
]
}
}
}
}
```
#### Configuration Options
- **infile**: Path to your changelog file (default: "CHANGELOG.md")
- **preset**: Configuration for commit types and their sections
- **name**: The preset to use (conventionalcommits, angular, etc.)
- **types**: Array of commit types and their corresponding changelog sections
#### Commit Types and Sections
The plugin supports all standard Conventional Commits types:
- **feat**: New features
- **fix**: Bug fixes
- **docs**: Documentation changes
- **style**: Code style changes (formatting, etc.)
- **refactor**: Code refactoring
- **perf**: Performance improvements
- **test**: Adding or modifying tests
- **build**: Build system or external dependency changes
- **ci**: CI configuration changes
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
#### Example Changelog Output
```markdown
# Changelog
## [1.1.0] - 2024-05-05
### Features
- Add new authentication system
- Implement dark mode
### Bug Fixes
- Fix login form validation
- Resolve navigation issues
### Documentation
- Update API documentation
- Add setup instructions
```
### Hooks: Automate Your Workflow
Hooks are powerful tools that let you run custom scripts at specific points in the release process. They follow the format **[prefix]:[plugin]:[hook]**.
#### Hook Types
- **before**: Run before a specific step
- **after**: Run after a specific step
- **plugin**: Target specific plugin (git, npm, github)
- **hook**: The specific lifecycle event
#### Common Hook Examples
```json
{
"hooks": {
// Pre-release checks
"before:init": [
"npm run lint",
"npm test",
"npm run build"
],
// Version bump notifications
"after:bump": "echo 'Version bumped to ${version}'",
// Post-release tasks
"after:release": [
"echo 'Release ${version} completed'",
"npm run deploy"
],
// Git-specific hooks
"after:git:release": "echo 'Git tag v${version} created'",
// GitHub-specific hooks
"after:github:release": "echo 'GitHub release created'"
}
}
```
#### Available Variables in Hooks
You can use these variables in your hook commands:
- `${version}`: The new version
- `${latestVersion}`: The previous version
- `${changelog}`: The generated changelog
- `${name}`: Package name
- `${repo.repository}`: Repository name
- `${branchName}`: Current branch
- `${releaseUrl}`: URL to the release
#### Real-world Hook Examples
```json
{
"hooks": {
// Run tests and build before release
"before:init": [
"npm run test:ci",
"npm run build"
],
// Update documentation after version bump
"after:bump": "npm run docs:update",
// Deploy to staging after release
"after:release": "npm run deploy:staging",
// Notify team on Slack
"after:github:release": "curl -X POST -H 'Content-type: application/json' --data '{\"text\":\"New release ${version} is out!\"}' $SLACK_WEBHOOK"
}
}
```
### Dry run
You can preview the release process without modifying Git:
```bash
npx release-it --dry-run
```
#### Useful Links
- [Release-it — GitHub Repository](https://github.com/release-it/release-it)
- [Release-it — Documentation](https://github.com/release-it/release-it/blob/master/docs/configuration/README.md)
- [Release-it — Node.js Version Support](https://github.com/release-it/release-it?tab=readme-ov-file#nodejs-version-support)
- [@release-it/conventional-changelog Plugin](https://github.com/release-it/conventional-changelog)
## GitHub Actions
You can automate releases using GitHub Actions, e.g. on merge into `main`.
```yaml [.github/workflows/release-it.yml]
name: Release-it
on:
pull_request:
types: [closed]
branches: [main]
permissions:
contents: write
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set git identity
run:
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- run: npm install
- run: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
## Conclusion
Manual releases steal time and invite errors. With Release-it you:
- Write clear Conventional Commits.
- Run one command (or let CI run it for you).
- Ship a perfectly tagged, documented release in seconds.
So the next time you finish that sprint, forget the copy-paste routine and let Release-it do the boring stuff—while you start the next great feature.
---
## Tailwind CSS utility feature
We have all been there: the CSS Working Group releases a shiny new CSS feature, and we immediately want to use it — but Tailwind CSS does not yet provide first-class utilities for it.
In this article, we will look at how Tailwind CSS 4 makes it easier to adopt experimental or cutting-edge CSS features by adding custom utilities, using the new `text-box` property as a concrete example.
## The Problem: adopting new CSS features early
`Text-box` is a good example of a modern CSS feature that solves a real design problem: removing excess vertical whitespace caused by font metrics.
You can read more about it on MDN:
[MDN: Text-box](https://developer.mozilla.org/en-US/docs/Web/CSS/text-box)
However, because the property is still experimental and browser support is limited, Tailwind understandably does not ship built-in utilities for it (yet).
That does not mean we cannot use it.
## Adding custom utilities in Tailwind CSS 4
Tailwind CSS 4 provides a clean, official way to add custom utilities via the [@utility API](https://tailwindcss.com/docs/adding-custom-styles#adding-custom-utilities).
This allows us to define utilities that behave exactly like native Tailwind classes — without hacks or large plugin abstractions.
## Example: Text-box utilities
Let’s create a small set of utilities for text-box-trim and its companion property text-box-edge.
```css
/*
utility classes for:
Text-box: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/text-box
*/
@utility text-box-* {
--text-box-trim: --value("none", "trim-start", "trim-end", "trim-both");
}
@utility text-box-edge-start-* {
--text-box-edge-start: --value("text", "cap", "ex");
}
@utility text-box-edge-end-* {
--text-box-edge-end: --value("alphabetic", "text");
}
@utility text-box {
text-box: var(--text-box-trim, none) var(--text-box-edge-start, text) var(--text-box-edge-end, text);
}
```
This approach mirrors Tailwind’s compositional philosophy:
* One base utility (text-box)
* Small, focused modifier utilities
* No duplicated declarations
### usage
```html
Tailwind CSS rocks!
```
This will generate the following CSS
```css
text-box: trim-both cap alphabetic;
```
## Why this approach works well
This pattern might look slightly verbose at first glance, but it has some very real advantages.
* ***Readable*** — class names describe intent clearly
* ***Composable*** — utilities can be mixed and matched
* ***Future-proof*** — easy to remove once Tailwind supports the feature natively
* ***Progressive enhancement*** — unsupported browsers simply ignore the property
Most importantly, this keeps your codebase honest.
You are not hiding experimental CSS behind magic abstractions. You are making a conscious, documented choice to use a modern feature where it provides real value.
This same technique works extremely well for other emerging CSS features, for example:
* [anchor-position](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position-anchor)
* [view-transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/View_transitions)
If the browser can parse it, Tailwind CSS 4 can host it.
Happy Hacking