+
+
+ >
)
}
diff --git a/src/app/blog/posts/animated-number-component.mdx b/src/app/blog/posts/animated-number-component.mdx
deleted file mode 100644
index 88e00d7e..00000000
--- a/src/app/blog/posts/animated-number-component.mdx
+++ /dev/null
@@ -1,84 +0,0 @@
----
-title: 'Building an Animated Number Component'
-publishedAt: '2024-10-04'
-summary: 'A reusable React component for smooth number animations with accessibility and performance in mind.'
-categories: [React, Animation, A11y]
----
-
-Animating numbers is tricky. You want it to feel mechanical, like a slot machine or an old odometer, but also digital and fluid. In this post, we'll break down the `AnimatedNumber` component used on this very site.
-
-## The Challenge
-
-Creating number animations that feel natural requires careful consideration of:
-
-- **Easing functions** - Linear animations feel robotic
-- **Duration** - Too fast feels jarring, too slow feels sluggish
-- **Accessibility** - Motion should respect user preferences
-- **Performance** - Smooth 60fps animations
-
-## The Solution
-
-Here's a simplified version of the component:
-
-```jsx
-import { useState, useEffect, useRef } from 'react';
-import { motion } from 'framer-motion';
-
-const AnimatedNumber = ({ value, duration = 1000 }) => {
- const [displayValue, setDisplayValue] = useState(0);
- const startTime = useRef(null);
-
- useEffect(() => {
- const animate = (currentTime) => {
- if (!startTime.current) startTime.current = currentTime;
-
- const progress = Math.min((currentTime - startTime.current) / duration, 1);
-
- // Easing function (ease-out-cubic)
- const eased = 1 - Math.pow(1 - progress, 3);
-
- setDisplayValue(Math.floor(eased * value));
-
- if (progress < 1) {
- requestAnimationFrame(animate);
- }
- };
-
- requestAnimationFrame(animate);
- }, [value, duration]);
-
- return {displayValue.toLocaleString()};
-};
-```
-
-## Key Features
-
-### 1. Custom Easing
-The ease-out-cubic function creates a natural feel that starts fast and slows down as it approaches the target value.
-
-### 2. Accessibility
-Always respect `prefers-reduced-motion`:
-
-```jsx
-const shouldReduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
-
-if (shouldReduceMotion) {
- return {value.toLocaleString()};
-}
-```
-
-### 3. Performance Tips
-- Use `requestAnimationFrame` for smooth 60fps animations
-- Avoid layout thrashing by batching DOM reads/writes
-- Consider using `transform` instead of changing layout properties
-
-## Advanced Usage
-
-For more complex scenarios, consider:
-
-- **Decimal support** for financial applications
-- **Spring animations** for more organic feel
-- **Staggered animations** for lists of numbers
-- **GSAP integration** for timeline-based animations
-
-The key is finding the right balance between visual appeal and performance. Sometimes, a simple CSS transition is all you need!
\ No newline at end of file
diff --git a/src/app/blog/posts/developer-experience/00-intro.mdx b/src/app/blog/posts/developer-experience/00-intro.mdx
new file mode 100644
index 00000000..3c914437
--- /dev/null
+++ b/src/app/blog/posts/developer-experience/00-intro.mdx
@@ -0,0 +1,81 @@
+---
+title: 'Developer Experience Series: Stop Hating Your Development Setup'
+publishedAt: '2025-12-09'
+summary: 'A series about transforming the daily grind of development into something you actually enjoy. We'll explore tools, patterns, and techniques that make coding less painful and more delightful.'
+categories: [Developer Experience, Engineering]
+series: 'Developer Experience'
+seriesIndex: 0
+---
+
+I spent the last two years debugging environment variables.
+
+Not constantly, of course. That would be insane. But ever since switching to Linux (Ubuntu, obviously - with Hyprland because I hate myself), it's been a special kind of pain. One day it's the database URL. The next day it's the API key. Before you know it, you're questioning your decision to leave the comfortable embrace of macOS.
+
+The breaking point? When I deployed to production and realized `.env.example` had `DATABASE_URL="localhost"` hardcoded in it. My users were not impressed with connecting to my tiling window manager.
+
+## The Problem with "Just Works" Development
+
+We love to say "it just works" until it doesn't. And then we spend the next three hours staring at terminal output, muttering about how "it worked on my machine."
+
+```bash
+$ bun run dev
+✅ Database connected!
+✅ Redis connected!
+✅ Auth service ready!
+🚝 [ERROR] SECRET_KEY is missing
+🚝 [ERROR] DATABASE_URL is invalid
+🚝 [ERROR] NODE_ENV should be 'production'
+```
+
+Sound familiar? Of course it does. We've all been there. We build amazing applications with beautiful UIs and sophisticated architecture, but when it comes to developer experience, we're still living in the stone age.
+
+## What This Series Covers
+
+I'm tired of hating my development setup. And you should be too.
+
+Over the next few posts, we're going to fix that. We'll explore:
+
+- **Environment validation that doesn't suck** (coming next)
+- **Hot reloading that actually works reliably**
+- **Tooling that prevents mistakes before they happen**
+- **Debugging setups that make you feel like a detective, not a confused child**
+- **Automation that saves you time instead of creating more work**
+
+The goal isn't just to add more tools to your already overflowing toolkit. It's about building development experiences that make you want to code, not dread it.
+
+## Why This Matters
+
+Look, I get it. You're busy. You have features to ship, bugs to fix, stakeholders to manage. "Developer experience" sounds like one of those buzzwords consultants use to charge more money.
+
+But here's the thing: every minute you spend wrestling with your setup is a minute you're not solving actual problems. Every confusing error message is cognitive load you could be spending on something meaningful. Every time your tools fail you, they're breaking your flow and killing your productivity.
+
+Good developer experience isn't a luxury. It's a competitive advantage. Just like how [over-engineering my site](/blog/posts/over-engineering-my-site) taught me that sometimes the complex path leads to better understanding, improving our development setup is an investment in our future productivity.
+
+## The Philosophy
+
+Before we dive in, let's establish some ground rules:
+
+1. **Fail fast, fail clearly** - Don't make me hunt for errors
+2. **Assume I'm smart but busy** - Give me helpful defaults, let me override when needed
+3. **One source of truth** - Don't make me maintain configuration in five different places
+4. **Local-first, production-ready** - If it works locally, it should work in production
+5. **Self-documenting** - Good tools don't need extensive documentation
+
+## What's Next
+
+In the next post, we'll tackle environment validation. We'll build a system that:
+
+- Catches missing or invalid environment variables at build time
+- Provides helpful error messages with actual context
+- Shows you exactly what's wrong and how to fix it
+- Integrates beautifully with your existing workflow
+
+No more "SECRET_KEY is missing" and wondering what format it should be. No more discovering missing variables in production. Just clear, actionable feedback when something's wrong.
+
+## Join the Conversation
+
+What's your biggest developer experience pain point? Drop me a message or comment below. I'd love to hear what drives you crazy about your current setup – maybe we'll cover it in this series.
+
+---
+
+*P.S. If you're thinking "this is just common sense," you're right. The sad part is how uncommon sense can be in our tooling.*
\ No newline at end of file
diff --git a/src/app/blog/posts/developer-experience/02-new-site-over-engineering.mdx b/src/app/blog/posts/developer-experience/02-new-site-over-engineering.mdx
new file mode 100644
index 00000000..57e8875a
--- /dev/null
+++ b/src/app/blog/posts/developer-experience/02-new-site-over-engineering.mdx
@@ -0,0 +1,232 @@
+---
+title: 'New Site: The Over-Engineering Chronicles Continue'
+publishedAt: '2025-12-09'
+summary: 'Building a new site that somehow ended up more over-engineered than the last one. Custom build tools, monorepo structure, environment validation, and of course, a developer widget that runs cleanup jobs.'
+categories: [Developer Experience, Engineering, Over-engineering]
+series: 'Developer Experience'
+seriesIndex: 2
+tags: [Next.js, TypeScript, Monorepo, Vercel, Environment Variables, Cron Jobs]
+---
+
+*This is part 2 of the Developer Experience series. After building our [environment validator](/blog/posts/developer-experience/01-environment-validator), we're applying those principles to a real project. Spoiler: it got complicated.*
+
+## It Started Simple
+
+The original goal was refreshingly simple: build a site that doesn't make me want to throw my laptop out the window every time I need to update it. My old site, while entertainingly [over-engineered](/blog/posts/over-engineering-my-site), had become a maintenance nightmare.
+
+Three weeks later, I have:
+
+- A monorepo with 4 packages
+- Custom environment validation with UI error states
+- A developer widget with cron job management
+- Anonymous user cleanup with batch processing
+- More TypeScript types than actual content
+
+Some people never learn.
+
+## The Monorepo Rabbit Hole
+
+It began innocently. "I'll just separate the DB logic," I said. "Maybe some shared types," I thought. Before I knew it:
+
+```
+skriuwde/
+├── apps/
+│ └── web/ # Next.js app
+├── packages/
+│ ├── core-logic/ # Shared utilities
+│ ├── db/ # Database schema & migrations
+│ └── crud/ # API wrappers
+└── tools/
+ └── check-db.ts # Database health checker
+```
+
+Why? Because I wanted to reuse the database logic across multiple apps that I will probably never build. The siren call of "future-proofing" strikes again.
+
+## Environment Validation: Episode II
+
+Remember that environment validator we built? Let's just say I implemented it with the enthusiasm of someone who's been burned too many times.
+
+```typescript
+// .env.example with actual helpful comments
+# =============================================================================
+# DATABASE (Required)
+# =============================================================================
+# PostgreSQL connection string
+# Neon: postgresql://user:password@host.neon.tech/database?sslmode=require
+# Local: postgresql://user:password@localhost:5432/database
+DATABASE_URL=postgresql://user:password@localhost:5432/skriuw
+
+# =============================================================================
+# DEVELOPMENT: Quick Reference
+# =============================================================================
+# Bun commands: bun run dev | bun run build | bun run lint
+# Database: drizzle-kit studio (UI) | drizzle-kit generate (migrations)
+# Install: bun install (most projects) or pnpm install (snippets)
+```
+
+No more guessing what format the database URL should be. No more discovering missing variables in production. Just clear, helpful configuration with zero ambiguity.
+
+## The Developer Widget: Because Why Not?
+
+Every good over-engineered project needs a developer widget. Mine has:
+
+- **Database stats** with real-time connection monitoring
+- **User management** with anonymous user tracking
+- **Manual cleanup controls** with dry run mode
+- **Cron job status** with run history
+- **System health monitoring**
+- **Cookie management** (because sometimes you need to hide the hero badge)
+
+```typescript
+// The widget that does way too much
+export function DevWidget() {
+ const [stats, setStats] = useState(null)
+ const [users, setUsers] = useState([])
+ const [cronStatus, setCronStatus] = useState(...))
+ const [activeTab, setActiveTab] = useState('database')
+
+ // Five tabs of pure developer productivity
+ return
+}
+```
+
+## Anonymous User Cleanup: A Feature I Didn't Need
+
+The site supports anonymous sign-ins. This naturally led to needing cleanup for old anonymous users. Which naturally led to:
+
+1. **Cron job configuration** in Vercel
+2. **Batch deletion** (100 users at a time)
+3. **Dry run mode** for testing
+4. **Run history tracking** with success/failure states
+5. **Beautiful UI** for managing it all
+
+```typescript
+// The cleanup route that does too much
+export async function cleanupProcess(dryRun: boolean) {
+ const usersToDelete = await db.query.user.findMany({
+ where: and(
+ eq(schema.user.isAnonymous, true),
+ lt(schema.user.createdAt, twentyFourHoursAgo)
+ )
+ })
+
+ if (!dryRun) {
+ // Delete in batches to be a good citizen
+ for (let i = 0; i < ids.length; i += batchSize) {
+ await db.delete(schema.user).where(inArray(schema.user.id, batch))
+ }
+ }
+}
+```
+
+All this to delete anonymous users older than 24 hours. The complexity is impressive; the necessity is questionable.
+
+## The Stack: Because More Tools = Better Developer Experience
+
+- **Next.js 16** with Turbopack (because waiting is for peasants)
+- **TypeScript** with strict mode (for the type safety gods)
+- **Drizzle ORM** (TypeScript for your database)
+- **Bun** (Fast package manager with built-in everything)
+- **Tailwind CSS v4** (The new hotness)
+- **Zustand** (State management that doesn't make you cry)
+- **Better Auth** (Authentication that doesn't hate you)
+
+Each tool was carefully chosen for maximum developer productivity. The irony that I spent more time configuring tools than building features is not lost on me.
+
+## Environment Variables: The Professional Approach
+
+Remember the pain of environment variables from my old site? Fixed it:
+
+```typescript
+// Clean, type-safe environment access
+export const env = createEnv({
+ client: {
+ NEXT_PUBLIC_APP_URL: z.string().url(),
+ },
+ server: {
+ DATABASE_URL: z.string().url(),
+ SECRET_KEY: z.string().min(32),
+ CRON_SECRET: z.string().min(16),
+ // OAuth providers with conditional requirements
+ GITHUB_CLIENT_ID: z.string().optional(),
+ GITHUB_CLIENT_SECRET: z.string().optional(),
+ },
+ validation: {
+ github: {
+ validate: ({ GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET }) => {
+ if (GITHUB_CLIENT_ID && !GITHUB_CLIENT_SECRET) {
+ throw new Error('GITHUB_CLIENT_SECRET required with GITHUB_CLIENT_ID')
+ }
+ }
+ }
+ }
+})
+```
+
+If something's wrong? Beautiful error UI tells you exactly what to fix. No more cryptic runtime errors or midnight debugging sessions.
+
+## The Build Process: Optimized Into Oblivion
+
+Of course, I couldn't leave well enough alone. The build process includes:
+
+```json
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "buildCommand": "turbo build --filter=web",
+ "installCommand": "bun install",
+ "framework": "nextjs",
+ "outputDirectory": "apps/web/.next",
+ "crons": [
+ {
+ "path": "/api/cron/cleanup",
+ "schedule": "0 2 * * *"
+ }
+ ],
+ "build": {
+ "env": {
+ "NEXT_TELEMETRY_DISABLED": "1"
+ }
+ }
+}
+```
+
+- **Turbo** for monorepo builds
+- **Bun** for faster installs
+- **Optimized Vercel configuration**
+- **Disabled Next.js telemetry** (because privacy)
+- **Automated cron jobs** for user cleanup
+
+The build is fast, the deployment is smooth, and the maintenance overhead is... significant.
+
+## Was It Worth It?
+
+Let's be honest: probably not.
+
+I could have built a simple site in a weekend that does 90% of what this does. Instead, I spent weeks building a developer experience that will save me... maybe a few hours per year?
+
+But here's the thing: I learned a ton about:
+
+- Monorepo management with Turborepo
+- Advanced TypeScript patterns
+- Environment validation best practices
+- Vercel cron jobs and edge functions
+- React state management patterns
+- Database optimization with Drizzle
+
+And most importantly? I built a development setup that doesn't make me hate coding. The environment validation catches issues before they ship. The developer widget makes debugging a joy. The monorepo structure makes adding features painless.
+
+Sometimes the over-engineering journey is the destination. Even if the destination is just a blog with three posts and a developer widget that has five tabs.
+
+## The Real Takeaway
+
+Good developer experience isn't about having the most tools or the fanciest setup. It's about building something that makes you want to keep coding.
+
+My old site was a maintenance nightmare. This new one? It's a joy to work with. The environment validation prevents the stupid mistakes. The developer widget makes debugging actually pleasant. The monorepo structure makes adding features feel easy.
+
+Is it over-engineered? Absolutely. But it's over-engineered in service of making my daily development life better. And sometimes, that's exactly what you need.
+
+---
+
+*P.S. If you're building a new site and want to avoid my mistakes... just use Next.js with the defaults. Maybe add environment validation. Everything else is probably overkill.*
+
+*P.P.S. The developer widget is actually pretty cool though. You should check it out.*
\ No newline at end of file
diff --git a/src/app/blog/posts/engineering/draft-system-implementation.md b/src/app/blog/posts/engineering/draft-system-implementation.md
new file mode 100644
index 00000000..e6c56d79
--- /dev/null
+++ b/src/app/blog/posts/engineering/draft-system-implementation.md
@@ -0,0 +1,71 @@
+---
+title: 'Building a draft system for markdown files in Next.js'
+publishedAt: '16-12-2025'
+summary: 'Running a blog throug filesystem is great, althrough accidental commits of unfinished posts are bound to happen sooner or later. Here’s how I implemented a draft system using MDX frontmatter'
+categories: [Engineering, Next.js, Authentication']
+draft: true
+---
+ As I am building a [Notion-like app called Skriuw](https://skriuw.vercel.app) that has rich text editing capabilities I opted for Markdown files in my [repository](https://github.com/remcostoeten/remcostoeten.nl) so to change things up.
+
+ Works wonderfull, only downside being if you are neurodivergent you'll end up with dozens of unfinished posts in your repository which you don't want to lose nor want to publish so I decided to implement a draft system.
+
+I had just implemented authentication, GitHub OAuth only for me to access a private admin route for analytics and metrics. I signed up, and added some logic in the _middleware_ proxy that simply checks `if (email === proccess.env.MY_EMAIL).. access granted`. Now for the harder task.
+
+## The Goal
+1. Mark a post as a draft via frontmatter.
+2. Hide drafts from the public blog listing.
+3. Allow me (the admin) to preview them while logged in.
+4. Prevent direct access to draft URLs by unauthorized users.
+
+## Step 1: Parsing the Frontmatter
+The first step was updating my MDX parser to recognize a `draft` field.
+
+```typescript
+// src/utils/utils.ts
+case 'draft':
+ metadata.draft = value.toLowerCase() === 'true'
+ break
+```
+
+I also split my blog fetching logic into `getBlogPosts()` (public) and `getAllBlogPosts()` (admin/internal).
+
+## Step 2: The Security Layer
+I created a simple server-only utility to check for admin status. Since I'm using the `better-auth` admin plugin, it's as simple as:
+
+```typescript
+export async function isAdmin() {
+ const session = await auth.api.getSession({ headers: await headers() })
+ return session?.user?.role === 'admin' || session?.user?.email === process.env.MY_EMAIL
+}
+```
+
+## Step 3: Protecting the Routes
+In my dynamic blog route `[...slug]`, I added a check before rendering. If the post is a draft and the user isn't an admin, they get a `404`.
+
+I define the admin variable `const isAdmin = await isAdmin()` and use that to simply conditionally fetch either all posts or only public posts.
+
+```bash
+let allPosts
+if (isAdmin) {
+ allPosts = getAllBlogPosts()
+} else {
+ allPosts = getBlogPosts()
+}
+```
+Or if you prefer the ternary operator:
+```typescript
+const allPosts = isAdmin ? getAllBlogPosts() : getBlogPosts()
+```
+And then it's simply a matter of checking if the post exists and if it's a draft.
+```typescript
+const post = allPosts.find((p) => p.slug === slug)
+
+if (!post || (post.metadata.draft && !isAdmin)) {
+ notFound()
+}
+```
+
+### End result
+Added [a banner](https://github.com/remcostoeten/remcostoeten.nl/blob/master/src/components/blog/posts-client.tsx) indicating the post is a draft and a "DRAFT" badge in the listing which only shows up when logged in.
+
+Full code can be found [here](https://github.com/remcostoeten/remcostoeten.nl/blob/master/src/components/blog/posts-client.tsx).
\ No newline at end of file
diff --git a/src/app/blog/posts/engineering/notice-components-example.mdx b/src/app/blog/posts/engineering/notice-components-example.mdx
new file mode 100644
index 00000000..2528b0a3
--- /dev/null
+++ b/src/app/blog/posts/engineering/notice-components-example.mdx
@@ -0,0 +1,92 @@
+---
+title: "Notice Components Demo"
+publishedAt: "2024-12-20"
+summary: "Demonstration of the new notice components with different styles and content including links."
+categories: ["Engineering", "Components"]
+tags: ["mdx", "components", "ui"]
+draft: true
+---
+
+# Notice Components Demo
+
+This post demonstrates the various notice components available in our MDX setup.
+
+## Basic Notice Types
+
+
+This is a regular information notice with an icon and blue styling. Perfect for general information and tips.
+
+
+
+This is a warning notice with amber coloring. Use this for cautions and important considerations.
+
+
+
+This is an alert notice with red styling. Use for critical information and error conditions.
+
+
+
+This is a success notice with green coloring. Perfect for confirming successful operations or positive outcomes.
+
+
+
+This is a neutral notice with slate coloring. Use for general announcements and neutral information.
+
+
+
+This is a regular notice with gray coloring. Similar to neutral but with a slightly different tone.
+
+
+## Notice with Links
+
+You can include links inside notice components using standard Markdown syntax:
+
+
+Check out the [Next.js documentation](https://nextjs.org/docs) for more information about MDX components and custom component setup.
+
+
+
+This API will change in the next version. Please see the [migration guide](https://example.com/migration) for details on how to update your code.
+
+
+
+Congratulations! You've completed the tutorial. You can now move on to the [advanced section](/blog/advanced-topics).
+
+
+## Mixed Content Examples
+
+
+A security vulnerability has been discovered in version 2.0.0. Please update immediately to version 2.0.1.
+
+See the [security advisory](https://example.com/security/2024-001) for more details and upgrade instructions.
+
+
+
+Scheduled maintenance will occur on December 25, 2024 from 2:00 AM to 4:00 AM UTC. During this time, some services may be temporarily unavailable.
+
+Check the [status page](https://status.example.com) for real-time updates.
+
+
+## Using Individual Components
+
+You can also use the individual notice components for more semantic clarity:
+
+
+You can use the individual ``, ``, etc. components directly in your MDX files for better readability.
+
+
+
+Consider using dynamic imports for large components that are not immediately needed to improve initial load performance.
+
+
+
+We've just added dark mode support! You can toggle it using the theme switcher in the navigation bar.
+
+
+## Custom Styling
+
+The notice components also accept additional CSS classes for custom styling:
+
+
+This notice has custom styling applied using the `className` prop. You can add any Tailwind CSS classes you need.
+
\ No newline at end of file
diff --git a/src/app/blog/posts/engineering/spotify-oauth-guide.md b/src/app/blog/posts/engineering/spotify-oauth-guide.md
new file mode 100644
index 00000000..95801f3f
--- /dev/null
+++ b/src/app/blog/posts/engineering/spotify-oauth-guide.md
@@ -0,0 +1,378 @@
+---
+title: "Spotify OAuth2 Setup Tutorial: Working Redirects, Tokens, and Refresh Logic"
+publishedAt: "20-12-2025"
+summary: "A guide for setting up Spotify OAuth2 to obtain access and refresh tokens and API integration."
+categories: ["Engineering", "Spotify API", "OAuth2", "Guide"]
+tags: ["Engineering", "Authentication", "OAuth2", "Guide", "Next.js"]
+slug: "spotify-oauth2-working-setup"
+---
+Accessing the Spotify API requires configuring OAuth2. It is not overly complex, although Spotify adds a few extra steps compared to providers like GitHub or Google. These differences cost me hours of debugging due to browser caching and redirect quirks, so I’m documenting the full workflow to save you time.
+
+### In this guide we will
+
+- create a Spotify developer application
+- configure redirect and callback URLs
+- obtain the client ID and client secret
+- implement refresh token generation
+- use the access token to make authenticated API calls
+
+The examples use Next.js for convenience, though the concepts translate cleanly to Node.js or any backend framework. You only need minimal familiarity with Next.js to follow along.
+
+---
+
+## Spotify Developer Account
+
+You will need a Spotify developer account to create an OAuth2 application. Navigate to the developer dashboard and click “Create app”, then fill in the fields:
+
+- **Name**: any label for your integration
+- **Description**: a short explanation
+- **Website**: optional for development
+- **Redirect URIs**: the callback endpoint in your project
+
+
+If you don't have an API setup yet, I'll be showing how to [implement the API routes here](#routes).
+
+Most implementations use something like:
+
+```bash
+/api/spotify/callback
+```
+
+### Important: Spotify rejects localhost
+
+This is the first part which is new for most. Setting the url to `http://localhost:3000` will not work.
+
+Instead register the loopback IP form:
+
+```bash
+http://127.0.0.1:3000/api/spotify/callback
+```
+
+*Both map to your machine, but Spotify validates them differently. `localhost` is a hostname that depends on DNS resolution. `127.0.0.1` is an explicit IP address and always resolves to the local interface, which is why Spotify accepts it. Make sure your OAuth redirect in your code matches exactly what you register in the developer dashboard.*
+
+Last question is: Which API/SDKs are you planning to use? Fill in your usecase (most likely web) and click "Save". After having pressed save you'll see your client ID, and secret if you press "View client secret". Copy these, and add to your `.env`or `.env.local` file like so:
+
+```bash
+SPOTIFY_CLIENT_ID=your-client-id
+SPOTIFY_CLIENT_SECRET=your-client-secret
+```
+
+
+It doesn't matter whether you use quotes around your environment variable values. All of these work:
+- `SPOTIFY_CLIENT_ID=value`
+- `SPOTIFY_CLIENT_ID="value"`
+- `SPOTIFY_CLIENT_ID='value'`
+
+*If your API calls for different variable names than these two, obviously change those.*
+Next we will configure the authorization code flow, exchanging the temporary code for both an access token and a refresh token.
+
+### API Routes {#routes}
+
+Now you'll have to implement the API route which you registered in the developer dashboard. Like I mentioned this implementation is following Next.js but you should just register an api in your desired framework.
+
+Create the api route
+```bash
+touch src/app/api/spotify/callback/route.ts
+## or app/api/spotify/callback/route.ts if no src dir
+```
+And insert your improved GET request handler:
+
+
+- Environment validation at startup fails fast if credentials are missing
+- Helper function reduces URL construction duplication
+- Better error handling with fallbacks for JSON parsing
+- Consistent redirect URI usage from environment variables
+
+```typescript
+import { NextResponse } from 'next/server';
+import { NextRequest } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+const SPOTIFY_ACCOUNTS_BASE = 'https://accounts.spotify.com';
+
+// Validate required environment variables at startup
+const requiredEnv = {
+ clientId: process.env.SPOTIFY_CLIENT_ID,
+ clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
+ redirectUri: process.env.SPOTIFY_REDIRECT_URI || 'http://127.0.0.1:3000/api/spotify/callback'
+};
+
+if (!requiredEnv.clientId || !requiredEnv.clientSecret) {
+ throw new Error('Missing required Spotify OAuth credentials');
+}
+
+function createRedirectUrl(baseUrl: string, params: Record) {
+ const url = new URL(baseUrl);
+ Object.entries(params).forEach(([key, value]) => {
+ url.searchParams.set(key, value);
+ });
+ return url;
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const code = searchParams.get('code');
+ const error = searchParams.get('error');
+
+ // Handle OAuth errors
+ if (error) {
+ return NextResponse.redirect(createRedirectUrl('/', { error }));
+ }
+
+ if (!code) {
+ return NextResponse.redirect(createRedirectUrl('/', { error: 'no_code' }));
+ }
+
+ // Exchange code for tokens
+ const authString = `${requiredEnv.clientId}:${requiredEnv.clientSecret}`;
+ const base64Auth = Buffer.from(authString).toString('base64');
+
+ const tokenResponse = await fetch(`${SPOTIFY_ACCOUNTS_BASE}/api/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Authorization': `Basic ${base64Auth}`
+ },
+ body: new URLSearchParams({
+ grant_type: 'authorization_code',
+ code,
+ redirect_uri: requiredEnv.redirectUri
+ })
+ });
+
+ if (!tokenResponse.ok) {
+ const errorData = await tokenResponse.json().catch(() => ({}));
+ return NextResponse.redirect(
+ createRedirectUrl('/', {
+ error: 'token_exchange_failed',
+ details: errorData.error_description || errorData.error || 'unknown'
+ })
+ );
+ }
+
+ const { refresh_token, access_token } = await tokenResponse.json();
+
+ // Success redirect with tokens
+ return NextResponse.redirect(
+ createRedirectUrl('/dev/spotify', {
+ success: 'true',
+ refresh_token: refresh_token || '',
+ access_token: access_token || ''
+ })
+ );
+
+ } catch (error) {
+ console.error('Error in Spotify callback:', error);
+ return NextResponse.redirect(
+ createRedirectUrl('/', { error: 'unknown_error' })
+ );
+ }
+}```
+
+## Testing Your OAuth Setup
+
+To test your OAuth setup and easily generate tokens, visit the `/dev/spotify` page in your application. This interactive page provides:
+
+- Step-by-step OAuth flow guidance
+- Automatic generation of authorization URLs
+- Token display with copy functionality
+- Error handling and debugging information
+- Environment variable requirements
+
+Simply navigate to `http://127.0.0.1:3000/dev/spotify` after setting up your environment variables to test the complete OAuth flow.
+
+---
+
+## Making API Calls
+
+Once you have your tokens, you can make authenticated requests to the Spotify API. Here's how to use the access token and refresh it when needed:
+
+### Using the Access Token
+
+```typescript
+async function fetchSpotifyData(accessToken: string) {
+ const response = await fetch('https://api.spotify.com/v1/me/player/currently-playing', {
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Spotify API error: ${response.status}`);
+ }
+
+ return await response.json();
+}
+```
+
+### Refreshing the Access Token
+
+Access tokens expire after 1 hour, so you'll need to refresh them using the refresh token:
+
+```typescript
+export async function POST(request: NextRequest) {
+ try {
+ const { refresh_token } = await request.json();
+
+ if (!refresh_token) {
+ return NextResponse.json({ error: 'Refresh token required' }, { status: 400 });
+ }
+
+ const clientId = process.env.SPOTIFY_CLIENT_ID;
+ const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
+
+ if (!clientId || !clientSecret) {
+ return NextResponse.json({ error: 'Missing credentials' }, { status: 500 });
+ }
+
+ const authString = `${clientId}:${clientSecret}`;
+ const base64Auth = Buffer.from(authString).toString('base64');
+
+ const response = await fetch('https://accounts.spotify.com/api/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Authorization': `Basic ${base64Auth}`
+ },
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ refresh_token
+ })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ return NextResponse.json({
+ error: 'Token refresh failed',
+ details: errorData.error_description || errorData.error
+ }, { status: 400 });
+ }
+
+ const data = await response.json();
+
+ return NextResponse.json({
+ access_token: data.access_token,
+ expires_in: data.expires_in,
+ refresh_token: data.refresh_token || refresh_token // Spotify may return a new refresh token
+ });
+
+ } catch (error) {
+ console.error('Error refreshing token:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+```
+
+## Common Pitfalls
+
+### 1. Browser Caching Issues
+
+Spotify's OAuth flow can get cached by browsers, especially during development. Use these strategies to avoid cache-related issues:
+
+- Always use incognito/private browsing for testing
+- Add `show_dialog=true` to force the consent screen
+- Clear browser cookies and localStorage if needed
+
+### 2. Mismatched Redirect URIs
+
+This is the most common issue. The redirect URI in your code must exactly match what's registered in the Spotify Developer Dashboard:
+
+- ✅ `http://127.0.0.1:3000/api/spotify/callback`
+- ❌ `http://localhost:3000/api/spotify/callback`
+
+### 3. Scope Issues
+
+Make sure you request all necessary scopes upfront. Spotify doesn't allow incremental scope requests.
+
+### 4. Token Storage
+
+Never store tokens in client-side code or commit them to version control:
+
+- ✅ Store refresh tokens securely on the server
+- ✅ Use environment variables for client credentials
+- ❌ Don't store tokens in localStorage or cookies
+
+## Production Considerations
+
+For production deployments:
+
+1. **Use HTTPS**: All redirect URIs must use HTTPS in production
+2. **Secure Storage**: Use a database or secure key management for refresh tokens
+3. **Rate Limiting**: Implement rate limiting for your API endpoints
+4. **Error Handling**: Provide user-friendly error messages
+5. **Token Rotation**: Implement refresh token rotation for better security
+
+## Complete Example
+
+Here's a complete example of a Spotify API service:
+
+```typescript
+class SpotifyAPIService {
+ private accessToken: string | null = null;
+ private tokenExpiry: number = 0;
+
+ async getCurrentlyPlaying(refreshToken: string): Promise {
+ const accessToken = await this.getAccessToken(refreshToken);
+
+ const response = await fetch('https://api.spotify.com/v1/me/player/currently-playing', {
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`
+ }
+ });
+
+ if (response.status === 204) {
+ return null; // No track currently playing
+ }
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch currently playing: ${response.status}`);
+ }
+
+ return await response.json();
+ }
+
+ private async getAccessToken(refreshToken: string): Promise {
+ // Return cached token if still valid
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
+ return this.accessToken;
+ }
+
+ // Refresh the token
+ const response = await fetch('/api/spotify/refresh', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ refresh_token: refreshToken })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to refresh access token');
+ }
+
+ const data = await response.json();
+ this.accessToken = data.access_token;
+ this.tokenExpiry = Date.now() + (data.expires_in * 1000);
+
+ return this.accessToken;
+ }
+}
+```
+
+## Conclusion
+
+Setting up Spotify OAuth2 requires attention to detail, especially with redirect URIs and token management. The key takeaways are:
+
+- Always use `127.0.0.1` instead of `localhost` for development
+- Store refresh tokens securely and handle token expiration
+- Implement proper error handling and user feedback
+- Test thoroughly in different browsers and environments
+
+With this setup, you can now integrate Spotify's rich API into your applications, from music players to analytics dashboards.
+
+
+Ready to build more? Check out my other guides on [API integration patterns](/blog/topics/api-integration) and [Next.js authentication](/blog/topics/auth).
+
+
+
+
diff --git a/src/app/blog/posts/over-engineering-my-site.mdx b/src/app/blog/posts/over-engineering-my-site.mdx
index 46f1a072..300544ed 100644
--- a/src/app/blog/posts/over-engineering-my-site.mdx
+++ b/src/app/blog/posts/over-engineering-my-site.mdx
@@ -2,7 +2,7 @@
title: 'New site 🎉 Self-induced neuronal deficiency is an over-engineering catalyst'
publishedAt: '2025-10-06'
summary: 'Building a simple personal website that spiraled into over-engineering territory: custom npm packages, monorepo setup, multiple APIs integration, and several scrapped features along the way.'
-categories: [Engineering, Design, Ramblings]
+categories: [Engineering, Design]
---
It started as a simple idea: *"I just need a place to put my thoughts."*
diff --git a/src/app/blog/posts/productivity/mac-to-linux.mdx b/src/app/blog/posts/productivity/mac-to-linux.mdx
new file mode 100644
index 00000000..b74db72b
--- /dev/null
+++ b/src/app/blog/posts/productivity/mac-to-linux.mdx
@@ -0,0 +1,172 @@
+
+---
+title: 'How switching from Mac to Linux improved my productivity'
+publishedAt: '2025-12-18'
+summary: "After years of being forced to use a Mac at work, I finally made the switch to Linux and will be sharing some tooling and workflows that I found helpful."
+categories: [Developer Experience, Engineering, Productivity]
+tags: [Linux, Productivity, Workflow]
+---
+
+I have owned a 2009 MacBook, a 2017 Pro, an M2 Pro, a fully spec'd M3 Pro, and an M3 Air. The displays, battery life, and build quality are excellent.
+
+After moving my personal machine from Windows to Linux, macOS started to feel slow. I disabled animations and tried extra tooling, but switching spaces, alt-tabbing, and managing stacked windows always dragged. Mission Control only added visual noise.
+
+The missing piece: a responsive window manager. On Linux, keyboard-driven tiling made everything immediate. That contrast pushed me toward a workflow focused on speed and clarity.
+
+Over the past two years I've been building my personal dotfiles which contain countless scripts and tools to make my workflow more efficient. Most can be reproduced on Mac, but I just like bashing the operating system. My dotfiles could not save me from dying inside from the forced animations and other agony.
+
+My dotfiles are available on [GitHub](https://github.com/remcostoeten/dotfiles) and fully open source except for a private environment variables sub directory. I've built a CLI tool through [OpenTUI](https://github.com/sst/opentui) which can be used to install my dotfiles. Last time I timed a fresh installation of Ubuntu, from booting the USB, to installing the ISO, to curling the dotfiles and running the installer I was fully setup with all my tools, programs and dotfiles in less than 15 minutes. Yes Stow exists, but I like learning new things and reinventing the wheel.
+
+My entire workflow is built around using my keyboard exclusively. I rarely use my mouse anymore, except for inaccessible websites. I've tried to automate or ease tasks which I found myself doing a lot. Starting off with a simple program that's a staple, [Zoxide](https://github.com/ajeetkumar/zoxide).
+
+## Zoxide
+
+This is a program that allows you to navigate to directories without typing the full path. It registers your directories and holds an internal knowledge ranking them based on how often you visit them giving the most visited directories the highest priority. It allows you to type `z $STRING` to navigate to the directory that most closely matches the string.
+
+I've been working on [Skriuw](https://github.com/remcostoeten/skriuw) which sits at `/home/remcostoeten/development/active/skriuw`. When I launch my terminal I have it open in `/home/remcostoeten/.config/dotfiles`. Normally you'd have to do:
+
+```bash
+# pwd .config/dotfiles
+cd ../../development/active/skriuw
+# or
+cd ~/development/active/skriuw
+```
+
+Thanks to Zoxide I can simply type `z sk` which triggers `zoxide`, and checks what folder resembles `sk` the most. But that's not all. Skriuw is a monorepo, which has a lot of subdirectories. The web app is at `/home/remcostoeten/development/active/skriuw/apps/web`, typing `z web` will probably fail due to there being a lot of directories with `web` on my machine. What I can do is simply type `z sk w` which will resolve to `/home/remcostoeten/development/active/skriuw/apps/web`. Need the `migrations` folder which sits in `packages/db/migrations`? Works exactly the same.
+
+```bash
+~/.config/dotfiles
+❯ pwd
+/home/remco-stoeten/.config/dotfiles
+
+~/.config/dotfiles
+❯ z sk db
+
+skriuwde/packages/db on daddy [$!?] via v22.21.0
+❯ pwd
+/home/remco-stoeten/projects/skriuwde/packages/db
+```
+
+## Aliases and Command Chaining
+
+I am a big fan of aliases, having bound `rmall` to remove `node_modules` and `.next` (or vite equivalent), `i` to `bun install`, `b` to `bun build`, `rs` to preview the build allows me to chain such combinations:
+
+```bash
+❯ pwd
+/home/remco-stoeten/.config/dotfiles
+
+dotfiles on master
+❯ z .nl ; rmall ; i ; b ; rs
+
+🧹 Starting cleanup...
+
+⚠ Warning: Could not create backup: ENOTDIR: not a directory, mkdir '/home/remco-stoeten/.config/dotfiles/scripts/rmall/backups/latest'
+✓ Removed directory: node_modules
+✓ Removed directory: .next
+✓ Removed file: bun.lock
+
+==================================================
+✓ Successfully removed: 3
+==================================================
+
+✨ Cleanup completed successfully!
+[0.08ms] ".env.local", ".env"
+bun install v1.2.22 (6bafe260)
+warn: incorrect peer dependency "next@16.0.10"
+
++ @tanstack/react-query-devtools@5.91.1
++ @types/react@19.2.7
++ @types/react-dom@19.2.3
++ @radix-ui/react-collapsible@1.1.12
++ @radix-ui/react-separator@1.1.8
++ @tailwindcss/postcss@4.1.18
++ @tailwindcss/typography@0.5.19
++ @tanstack/react-query@5.90.12
++ @types/node@20.11.17
++ @vercel/analytics@1.6.1
++ @vercel/speed-insights@1.3.1
++ clsx@2.1.1
++ framer-motion@12.23.26
++ geist@1.2.2
++ lucide-react@0.555.0 (v0.561.0 available)
++ next@16.0.10
++ next-mdx-remote@5.0.0
++ postcss@8.5.6
++ react@19.2.3
++ react-dom@19.2.3
++ react-icons@5.5.0
++ react-markdown@10.1.0
++ react-syntax-highlighter@16.1.0
++ remark-gfm@4.0.1
++ tailwind-merge@3.4.0
++ tailwindcss@4.1.18
++ tailwindcss-animate@1.0.7
++ tw-animate-css@1.4.0
++ typescript@5.9.3
+
+239 packages installed [554.00ms]
+$ next build
+ ▲ Next.js 16.0.10 (Turbopack)
+ - Environments: .env.local, .env
+
+ Creating an optimized production build ...
+ ✓ Compiled successfully in 8.2s
+ ✓ Finished TypeScript in 7.7s
+ ✓ Collecting page data using 15 workers in 1833.0ms
+ ✓ Generating static pages using 15 workers (50/50) in 4.3s
+ ✓ Finalizing page optimization in 33.2ms
+
+Route (app) Revalidate Expire
+┌ ○ /
+├ ○ /_not-found
+├ ○ /api/github/activity 1m 1y
+├ ƒ /api/github/commits
+├ ƒ /api/github/contributions
+├ ƒ /api/github/events
+├ ƒ /api/spotify/auth-url
+├ ƒ /api/spotify/callback
+├ ƒ /api/spotify/now-playing
+├ ƒ /api/spotify/recent
+├ ƒ /api/spotify/token
+├ ○ /blog
+├ ● /blog/[...slug]
+│ ├ /blog/developer-experience/00-intro
+│ ├ /blog/developer-experience/01-environment-validator
+│ ├ /blog/developer-experience/02-new-site-over-engineering
+│ └ /blog/over-engineering-my-site
+├ ○ /blog/categories
+├ ● /blog/categories/[category]
+│ ├ /blog/categories/engineering
+│ ├ /blog/categories/developer experience
+│ ├ /blog/categories/typescript
+│ └ [+11 more paths]
+├ ○ /blog/categories/opengraph-image
+├ ○ /blog/categories/twitter-image
+├ ○ /blog/opengraph-image
+├ ○ /blog/topics
+├ ● /blog/topics/[topic]
+│ ├ /blog/topics/engineering
+│ ├ /blog/topics/developer experience
+│ ├ /blog/topics/typescript
+│ └ [+11 more paths]
+├ ○ /blog/topics/opengraph-image
+├ ○ /blog/topics/twitter-image
+├ ○ /blog/twitter-image
+├ ƒ /og
+├ ○ /robots.txt
+├ ƒ /rss
+└ ○ /sitemap.xml
+
+
+○ (Static) prerendered as static content
+● (SSG) prerendered as static HTML (uses generateStaticParams)
+ƒ (Dynamic) server-rendered on demand
+
+$ next start
+ ▲ Next.js 16.0.10
+ - Local: http://localhost:3000
+ - Network: http://192.168.1.238:3000
+
+ ✓ Starting...
+ ✓ Ready in 385ms
+```
diff --git a/src/app/blog/posts/responsive-typography.mdx b/src/app/blog/posts/responsive-typography.mdx
deleted file mode 100644
index 45c43d39..00000000
--- a/src/app/blog/posts/responsive-typography.mdx
+++ /dev/null
@@ -1,153 +0,0 @@
----
-title: 'Responsive Typography That Actually Works'
-publishedAt: '2024-09-28'
-summary: 'Tired of media queries for font sizes? Let''s explore modern CSS techniques for fluid typography that scales seamlessly across devices.'
-categories: [CSS, Design, Frontend]
----
-
-Typography is the foundation of good design. But getting it right across all screen sizes? That's where things get interesting. Let's dive into fluid typography using CSS `clamp()`.
-
-## The Old Way: Media Queries
-
-Remember writing this?
-
-```css
-h1 {
- font-size: 2rem; /* Mobile */
-}
-
-@media (min-width: 768px) {
- h1 {
- font-size: 2.5rem; /* Tablet */
- }
-}
-
-@media (min-width: 1024px) {
- h1 {
- font-size: 3rem; /* Desktop */
- }
-}
-```
-
-This works, but it creates "jumps" in font size at specific breakpoints. Not exactly smooth.
-
-## The Modern Way: CSS Clamp()
-
-Enter `clamp()` - the CSS function that lets you set a minimum, preferred, and maximum value:
-
-```css
-h1 {
- font-size: clamp(2rem, 5vw, 3rem);
-}
-```
-
-This reads as: "Use 2rem at minimum, prefer 5% of the viewport width, but never exceed 3rem."
-
-## The Fluid Typography Formula
-
-For truly scalable typography, use this formula:
-
-```css
-/* Base font size that scales with viewport */
-:root {
- --fluid-min-width: 320;
- --fluid-max-width: 1140;
- --fluid-screen: 100vw;
- --fluid-bp: calc(
- (var(--fluid-screen) - var(--fluid-min-width) / 16 * 1rem) /
- (var(--fluid-max-width) - var(--fluid-min-width))
- );
-}
-
-/* Apply to headings */
-h1 {
- font-size: clamp(2rem, 1.5rem + var(--fluid-bp) * 1.5, 3rem);
- line-height: clamp(2.4rem, 1.8rem + var(--fluid-bp) * 1.8, 3.6rem);
-}
-```
-
-## Type Scale Systems
-
-Consistent typography needs a scale. Here's a modular scale approach:
-
-```css
-:root {
- --type-scale: 1.25; /* Major third */
- --base-size: 1rem;
-
- --text-xs: calc(var(--base-size) / var(--type-scale) / var(--type-scale));
- --text-sm: calc(var(--base-size) / var(--type-scale));
- --text-base: var(--base-size);
- --text-lg: calc(var(--base-size) * var(--type-scale));
- --text-xl: calc(var(--base-size) * var(--type-scale) * var(--type-scale));
- --text-2xl: calc(var(--base-size) * var(--type-scale) * var(--type-scale) * var(--type-scale));
-}
-```
-
-## Accessibility Considerations
-
-Always respect user preferences:
-
-```css
-@media (prefers-reduced-motion: reduce) {
- /* Remove fluid animations */
- * {
- transition: none !important;
- }
-}
-
-/* Support larger text sizes */
-@media (min-resolution: 120dpi) {
- :root {
- --base-size: 1.125rem;
- }
-}
-```
-
-## Practical Tips
-
-1. **Start with mobile** - Design for the smallest screen first
-2. **Test extremes** - Check both very small and very large screens
-3. **Use relative units** - `rem` for accessibility, `vw` for viewport scaling
-4. **Consider line height** - It should scale with font size
-5. **Don't forget about contrast** - Larger text needs less contrast, smaller needs more
-
-## The Complete System
-
-Here's a production-ready typography system:
-
-```css
-:root {
- /* Fluid viewport calculation */
- --fluid-min-width: 320;
- --fluid-max-width: 1140;
- --fluid-bp: calc(
- (100vw - var(--fluid-min-width) * 1px) /
- (var(--fluid-max-width) - var(--fluid-min-width))
- );
-
- /* Type scale */
- --scale: 1.25;
- --base-size: clamp(1rem, 0.9rem + var(--fluid-bp) * 0.1, 1.125rem);
-
- /* Generate sizes */
- --text-xs: calc(var(--base-size) / var(--scale) / var(--scale));
- --text-sm: calc(var(--base-size) / var(--scale));
- --text-base: var(--base-size);
- --text-lg: calc(var(--base-size) * var(--scale));
- --text-xl: calc(var(--base-size) * var(--scale) * var(--scale));
- --text-2xl: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale));
- --text-3xl: calc(var(--base-size) * var(--scale) * var(--scale) * var(--scale) * var(--scale));
-}
-
-/* Apply with semantic classes */
-.text-fluid {
- font-size: var(--text-base);
-}
-
-h1 { font-size: var(--text-3xl); }
-h2 { font-size: var(--text-2xl); }
-h3 { font-size: var(--text-xl); }
-```
-
-This approach gives you typography that's truly responsive, accessible, and maintainable. No more breakpoint jumping!
\ No newline at end of file
diff --git a/src/app/blog/posts/spaces-vs-tabs.mdx b/src/app/blog/posts/spaces-vs-tabs.mdx
deleted file mode 100644
index 8345f442..00000000
--- a/src/app/blog/posts/spaces-vs-tabs.mdx
+++ /dev/null
@@ -1,28 +0,0 @@
----
-title: 'Spaces vs. Tabs: The Indentation Debate Continues'
-publishedAt: '2024-04-08'
-summary: 'Explore the enduring debate between using spaces and tabs for code indentation, and why this choice matters more than you might think.'
-categories: [Engineering, Best Practices]
----
-
-The debate between using spaces and tabs for indentation in coding may seem trivial to the uninitiated, but it is a topic that continues to inspire passionate discussions among developers. This seemingly minor choice can affect code readability, maintenance, and even team dynamics.
-
-Let's delve into the arguments for both sides and consider why this debate remains relevant in the software development world.
-
-## The Case for Spaces
-
-Advocates for using spaces argue that it ensures consistent code appearance across different editors, tools, and platforms. Because a space is a universally recognized character with a consistent width, code indented with spaces will look the same no matter where it's viewed. This consistency is crucial for maintaining readability and avoiding formatting issues when code is shared between team members or published online.
-
-Additionally, some programming languages and style guides explicitly recommend spaces for indentation, suggesting a certain number of spaces (often two or four) per indentation level. Adhering to these recommendations can be essential for projects that aim for best practices in code quality and readability.
-
-## The Case for Tabs
-
-On the other side of the debate, proponents of tabs highlight the flexibility that tabs offer. Because the width of a tab can be adjusted in most text editors, individual developers can choose how much indentation they prefer to see, making the code more accessible and comfortable to read on a personal level. This adaptability can be particularly beneficial in teams with diverse preferences regarding code layout.
-
-Tabs also have the advantage of semantic meaning. A tab is explicitly meant to represent indentation, whereas a space is used for many purposes within code. This distinction can make automated parsing and manipulation of code simpler, as tools can more easily recognize and adjust indentation levels without confusing them with spaces used for alignment.
-
-## Hybrid Approaches and Team Dynamics
-
-The debate often extends into discussions about hybrid approaches, where teams might use tabs for indentation and spaces for alignment within lines, attempting to combine the best of both worlds. However, such strategies require clear team agreements and disciplined adherence to coding standards to prevent formatting chaos.
-
-Ultimately, the choice between spaces and tabs often comes down to team consensus and project guidelines. In environments where collaboration and code sharing are common, agreeing on a standard that everyone follows is more important than the individual preferences of spaces versus tabs. Modern development tools and linters can help enforce these standards, making the choice less about technical limitations and more about team dynamics and coding philosophy.
\ No newline at end of file
diff --git a/src/app/blog/posts/static-typing.mdx b/src/app/blog/posts/static-typing.mdx
deleted file mode 100644
index 01e202f6..00000000
--- a/src/app/blog/posts/static-typing.mdx
+++ /dev/null
@@ -1,53 +0,0 @@
----
-title: 'The Power of Static Typing in Programming'
-publishedAt: '2024-04-07'
-summary: 'In the ever-evolving landscape of software development, the debate between dynamic and static typing continues to be a hot topic.'
-categories: [Engineering, TypeScript]
----
-
-In the ever-evolving landscape of software development, the debate between dynamic and static typing continues to be a hot topic. While dynamic typing offers flexibility and rapid development, static typing brings its own set of powerful advantages that can significantly improve the quality and maintainability of code. In this post, we'll explore why static typing is crucial for developers, accompanied by practical examples through markdown code snippets.
-
-## Improved Code Quality and Safety
-
-One of the most compelling reasons to use static typing is the improvement it brings to code quality and safety. By enforcing type checks at compile time, static typing catches errors early in the development process, reducing the chances of runtime errors.
-
-```ts
-function greet(name: string): string {
- return `Hello, ${name}!`
-}
-
-// This will throw an error at compile time, preventing potential runtime issues.
-let message: string = greet(123)
-```
-
-## Enhanced Readability and Maintainability
-
-Static typing makes code more readable and maintainable. By explicitly declaring types, developers provide a clear contract of what the code does, making it easier for others (or themselves in the future) to understand and modify the codebase.
-
-## Facilitates Tooling and Refactoring
-
-Modern IDEs leverage static typing to offer advanced features like code completion, refactoring, and static analysis. These tools can automatically detect issues, suggest fixes, and safely refactor code, enhancing developer productivity and reducing the likelihood of introducing bugs during refactoring.
-
-```csharp
-// Refactoring example: Renaming a method in C#
-public class Calculator {
- public int Add(int a, int b) {
- return a + b;
- }
-}
-
-// After refactoring `Add` to `Sum`, all references are automatically updated.
-public class Calculator {
- public int Sum(int a, int b) {
- return a + b;
- }
-}
-```
-
-## Performance Optimizations
-
-Static typing can lead to better performance. Since types are known at compile time, compilers can optimize the generated code more effectively. This can result in faster execution times and lower resource consumption.
-
-## Conclusion
-
-Static typing offers numerous benefits that contribute to the development of robust, efficient, and maintainable software. By catching errors early, enhancing readability, facilitating tooling, and enabling optimizations, static typing is an invaluable asset for developers. As the software industry continues to mature, the importance of static typing in ensuring code quality and performance cannot be overstated. Whether you're working on a large-scale enterprise application or a small project, embracing static typing can lead to better software development outcomes.
diff --git a/src/app/blog/posts/tutorials/react/hooks-patterns.mdx b/src/app/blog/posts/tutorials/react/hooks-patterns.mdx
deleted file mode 100644
index 6b601c1d..00000000
--- a/src/app/blog/posts/tutorials/react/hooks-patterns.mdx
+++ /dev/null
@@ -1,255 +0,0 @@
----
-title: 'Advanced React Hooks Patterns'
-publishedAt: '2024-08-15'
-summary: 'Explore powerful React Hooks patterns that will level up your component architecture and state management.'
----
-
-React Hooks revolutionized how we write components, but there's more to them than just `useState` and `useEffect`. Let's explore some advanced patterns that will make your React code more maintainable and powerful.
-
-## 1. Custom Hook Composition
-
-The real power of hooks comes from composing them:
-
-```jsx
-// Instead of this:
-function UserProfile() {
- const [user, setUser] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- fetchUser().then(setUser).catch(setError).finally(() => setLoading(false));
- }, []);
-
- // ... component logic
-}
-
-// Do this:
-function UserProfile() {
- const { user, loading, error } = useUser();
-
- // ... component logic
-}
-
-function useUser() {
- const [state, setState] = useState({
- user: null,
- loading: true,
- error: null
- });
-
- useEffect(() => {
- fetchUser()
- .then(user => setState({ user, loading: false, error: null }))
- .catch(error => setState({ user: null, loading: false, error }));
- }, []);
-
- return state;
-}
-```
-
-## 2. The State Reducer Pattern
-
-For complex state logic, use reducers:
-
-```jsx
-function useCounter(initialValue = 0) {
- const [state, dispatch] = useReducer(counterReducer, {
- count: initialValue,
- history: [initialValue]
- });
-
- const actions = useMemo(() => ({
- increment: () => dispatch({ type: 'INCREMENT' }),
- decrement: () => dispatch({ type: 'DECREMENT' }),
- reset: () => dispatch({ type: 'RESET', payload: initialValue }),
- set: (value) => dispatch({ type: 'SET', payload: value })
- }), []);
-
- return { ...state, ...actions };
-}
-
-function counterReducer(state, action) {
- switch (action.type) {
- case 'INCREMENT':
- return {
- ...state,
- count: state.count + 1,
- history: [...state.history, state.count + 1]
- };
- case 'DECREMENT':
- return {
- ...state,
- count: state.count - 1,
- history: [...state.history, state.count - 1]
- };
- case 'RESET':
- return {
- ...state,
- count: action.payload,
- history: [action.payload]
- };
- case 'SET':
- return {
- ...state,
- count: action.payload,
- history: [...state.history, action.payload]
- };
- default:
- return state;
- }
-}
-```
-
-## 3. The Context + Hook Pattern
-
-Create type-safe contexts with custom hooks:
-
-```jsx
-// ThemeContext.jsx
-const ThemeContext = createContext();
-
-function ThemeProvider({ children }) {
- const [theme, setTheme] = useState('light');
-
- const toggleTheme = () => {
- setTheme(prev => prev === 'light' ? 'dark' : 'light');
- };
-
- const value = {
- theme,
- toggleTheme,
- isDark: theme === 'dark'
- };
-
- return (
-
- {children}
-
- );
-}
-
-function useTheme() {
- const context = useContext(ThemeContext);
- if (!context) {
- throw new Error('useTheme must be used within ThemeProvider');
- }
- return context;
-}
-
-// Usage
-function Header() {
- const { theme, toggleTheme, isDark } = useTheme();
-
- return (
-
-
-
- );
-}
-```
-
-## 4. The Higher-Order Hook Pattern
-
-Wrap existing hooks with additional functionality:
-
-```jsx
-function useLocalStorage(key, initialValue) {
- // Get stored value or use initial
- const [storedValue, setStoredValue] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- return initialValue;
- }
- });
-
- // Return a wrapped version of useState's setter
- const setValue = useCallback((value) => {
- try {
- // Allow value to be a function like useState
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
- } catch (error) {
- console.error(error);
- }
- }, [key, storedValue]);
-
- return [storedValue, setValue];
-}
-
-// Usage
-function App() {
- const [name, setName] = useLocalStorage('name', 'Anonymous');
-
- return (
- setName(e.target.value)}
- placeholder="Enter your name"
- />
- );
-}
-```
-
-## 5. The Async Hook Pattern
-
-Handle async operations cleanly:
-
-```jsx
-function useAsync(asyncFunction, dependencies = []) {
- const [state, setState] = useState({
- data: null,
- loading: true,
- error: null
- });
-
- useEffect(() => {
- let isCancelled = false;
-
- asyncFunction()
- .then(data => {
- if (!isCancelled) {
- setState({ data, loading: false, error: null });
- }
- })
- .catch(error => {
- if (!isCancelled) {
- setState({ data: null, loading: false, error });
- }
- });
-
- return () => {
- isCancelled = true;
- };
- }, dependencies);
-
- return state;
-}
-
-// Usage
-function UserProfile({ userId }) {
- const { data: user, loading, error } = useAsync(
- () => fetchUser(userId),
- [userId]
- );
-
- if (loading) return ;
- if (error) return ;
- return ;
-}
-```
-
-## Best Practices
-
-1. **Start with the component, extract to hook** - Don't over-engineer upfront
-2. **Use TypeScript** - Better autocompletion and error catching
-3. **Handle cleanup** - Return cleanup functions from effects
-4. **Memoize expensive operations** - Use `useMemo` and `useCallback`
-5. **Keep hooks focused** - Single responsibility principle applies
-
-These patterns will help you write more maintainable, testable, and reusable React code. The key is to think in terms of composition and separation of concerns.
\ No newline at end of file
diff --git a/src/app/blog/posts/vim.mdx b/src/app/blog/posts/vim.mdx
deleted file mode 100644
index 2006e0eb..00000000
--- a/src/app/blog/posts/vim.mdx
+++ /dev/null
@@ -1,40 +0,0 @@
----
-title: 'Embracing Vim: The Unsung Hero of Code Editors'
-publishedAt: '2024-04-09'
-summary: 'Discover why Vim, with its steep learning curve, remains a beloved tool among developers for editing code efficiently and effectively.'
-categories: [Engineering, Tools]
----
-
-In the world of software development, where the latest and greatest tools frequently capture the spotlight, Vim stands out as a timeless classic. Despite its age and initial complexity, Vim has managed to retain a devoted following of developers who swear by its efficiency, versatility, and power.
-
-This article delves into the reasons behind Vim's enduring appeal and why it continues to be a great tool for coding in the modern era.
-
-## Efficiency and Speed
-
-At the heart of Vim's philosophy is the idea of minimizing keystrokes to achieve maximum efficiency.
-
-Unlike other text editors where the mouse is often relied upon for navigation and text manipulation, Vim's keyboard-centric design allows developers to perform virtually all coding tasks without leaving the home row. This not only speeds up coding but also reduces the risk of repetitive strain injuries.
-
-## Highly Customizable
-
-Vim can be extensively customized to suit any developer's preferences and workflow. With a vibrant ecosystem of plugins and a robust scripting language, users can tailor the editor to their specific needs, whether it's programming in Python, writing in Markdown, or managing projects.
-
-This level of customization ensures that Vim remains relevant and highly functional for a wide range of programming tasks and languages.
-
-## Ubiquity and Portability
-
-Vim is virtually everywhere. It's available on all major platforms, and because it's lightweight and terminal-based, it can be used on remote servers through SSH, making it an indispensable tool for sysadmins and developers working in a cloud-based environment.
-
-The ability to use the same editor across different systems without a graphical interface is a significant advantage for those who need to maintain a consistent workflow across multiple environments.
-
-## Vibrant Community
-
-Despite—or perhaps because of—its learning curve, Vim has cultivated a passionate and active community. Online forums, dedicated websites, and plugins abound, offering support, advice, and improvements.
-
-This community not only helps newcomers climb the steep learning curve but also continually contributes to Vim's evolution, ensuring it remains adaptable and up-to-date with the latest programming trends and technologies.
-
-## Conclusion
-
-Vim is not just a text editor; it's a way of approaching coding with efficiency and thoughtfulness. Its steep learning curve is a small price to pay for the speed, flexibility, and control it offers.
-
-For those willing to invest the time to master its commands, Vim proves to be an invaluable tool that enhances productivity and enjoyment in coding. In an age of ever-changing development tools, the continued popularity of Vim is a testament to its enduring value and utility.
diff --git a/src/app/topics/[topic]/page.tsx b/src/app/blog/topics/[topic]/page.tsx
similarity index 94%
rename from src/app/topics/[topic]/page.tsx
rename to src/app/blog/topics/[topic]/page.tsx
index 1fa7e75e..d7aa7ef8 100644
--- a/src/app/topics/[topic]/page.tsx
+++ b/src/app/blog/topics/[topic]/page.tsx
@@ -4,6 +4,8 @@ import Link from 'next/link'
import { notFound } from 'next/navigation'
import { ArrowLeft, Hash, Calendar, ArrowUpRight } from 'lucide-react'
+export const dynamic = 'force-dynamic'
+
export async function generateStaticParams() {
const categories = getAllCategories()
return categories.map((cat) => ({
@@ -14,7 +16,7 @@ export async function generateStaticParams() {
export async function generateMetadata({ params }: { params: Promise<{ topic: string }> }) {
const { topic } = await params
const decodedTopic = decodeURIComponent(topic)
-
+
return {
title: `${decodedTopic.charAt(0).toUpperCase() + decodedTopic.slice(1)} Posts`,
description: `Browse all posts about ${decodedTopic}.`,
@@ -25,7 +27,7 @@ export default async function TopicPage({ params }: { params: Promise<{ topic: s
const { topic } = await params
const decodedTopic = decodeURIComponent(topic)
const posts = getBlogPostsByCategory(decodedTopic)
-
+
if (posts.length === 0) {
notFound()
}
@@ -34,16 +36,14 @@ export default async function TopicPage({ params }: { params: Promise<{ topic: s
return (
- {/* Back link */}
-
Back to topics
- {/* Header */}
@@ -56,7 +56,6 @@ export default async function TopicPage({ params }: { params: Promise<{ topic: s
-
-
-
+
+
+
+
+
+
+
+ {children}
+
)
-}
+}
\ No newline at end of file
diff --git a/src/app/metadata.ts b/src/app/metadata.ts
new file mode 100644
index 00000000..10fad499
--- /dev/null
+++ b/src/app/metadata.ts
@@ -0,0 +1,5 @@
+import { homeMetadata } from '@/core/metadata/home'
+
+export const metadata = {
+ ...homeMetadata,
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
index d31ca67b..16093b7e 100644
--- a/src/app/not-found.tsx
+++ b/src/app/not-found.tsx
@@ -1,10 +1,106 @@
+import Link from 'next/link'
+import { Home, FileText, Search, ArrowLeft } from 'lucide-react'
+import { baseUrl } from './sitemap'
+
export default function NotFound() {
+ const structuredData = {
+ "@context": "https://schema.org",
+ "@type": "WebPage",
+ "name": "404 - Page Not Found",
+ "description": "The page you're looking for could not be found. Return to the homepage or browse the blog.",
+ "url": `${baseUrl}/404`,
+ "mainEntityOfPage": {
+ "@type": "WebPage",
+ "@id": `${baseUrl}/404`
+ }
+ }
+
return (
-
-
- 404 - Page Not Found
-
-
The page you are looking for does not exist.
+ <>
+
+
+
+ {/* 404 Graphic */}
+
+
+ 404
+
+
+
+
+ {/* Error Message */}
+
+
+ Page not found
+
+
+ The page you're looking for seems to have vanished into the digital void.
+ Don't worry though, let's get you back on track.
+
+
+
+ {/* Action Buttons */}
+
+
+
+ Back to Home
+
+
+
+
+ Browse Blog
+
+
+
+ {/* Helpful Suggestions */}
+
+
Maybe you were looking for:
+
+
+
+
+ Latest blog posts
+
+
+
+ Browse by category
+
+
+
+ Popular topics
+
+
+
+ About me
+
+
+
+
+ {/* Search Hint */}
+
+
+
+ Tip: Use the search bar to find specific content
+
+
+
+
+ >
)
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e1303a87..af1801df 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,20 +1,42 @@
-import { BlogPosts } from '@/components/posts'
-import { Intro } from '@/components/intro'
-import { ActivitySection } from '@/components/ActivitySection'
-import WorkExperienceDemo from '@/components/work-experience-demo'
+import { BlogPosts } from '@/components/blog/posts';
+import { Intro } from '@/components/home/hero';
+import { ActivitySection } from '@/components/landing/activity/section';
+import { TechStackCloud } from '@/components/landing/tech-stack-cloud';
+import { Section } from '@/components/ui/section';
+import { homeMetadata } from '@/core/metadata'
+import nextDynamic from 'next/dynamic'
+
+const WorkExperienceDemo = nextDynamic(() => import('@/components/home/work-experience'), {
+ loading: () => null // Don't show loading state - appears instantly usually
+})
+
+export const dynamic = 'force-dynamic'
+
+export { homeMetadata as metadata }
export default function Page() {
return (
-
- {/* Intro - no border, just content */}
-
-
- {/* Main content sections with consistent bordered design */}
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+ {/* Tech Stack - Above Blog */}
+
+
+
+
+
+
+
+
-
+ >
)
}
+
diff --git a/src/app/posthog-demo/page.tsx b/src/app/posthog-demo/page.tsx
new file mode 100644
index 00000000..893d62de
--- /dev/null
+++ b/src/app/posthog-demo/page.tsx
@@ -0,0 +1,52 @@
+import { PostHogDemo } from '@/components/demo/posthog-demo'
+
+export default function PostHogDemoPage() {
+ return (
+
+
+
+
PostHog Analytics Demo
+
+ This page demonstrates the PostHog analytics integration. Interact with the demo component below to see events being captured.
+
+
+
+
+
+
+
How it works
+
+
Client-side Tracking:
+
+
Page views are automatically captured on route changes
+
Button clicks and custom events are tracked using the usePostHog hook
+
User identification and feature flags are available client-side
+
Session replay is automatically enabled (can be configured)
+
+
+
Server-side Tracking:
+
+
API routes can capture events using posthog-node
+
Server actions can track business logic events
+
Feature flags can be evaluated server-side
+
All server events require manual shutdown() to flush
+
+
+
Next Steps:
+
+
Update your .env.local with your actual PostHog API key
+
Check your PostHog dashboard to see captured events
+
Add custom events to your existing components and API routes
+
Set up feature flags in your PostHog dashboard
+
+
+
+
+
+ )
+}
+
+export const metadata = {
+ title: 'PostHog Demo | Remco Stoeten',
+ description: 'Demo page showing PostHog analytics integration',
+}
\ No newline at end of file
diff --git a/src/app/posthog.ts b/src/app/posthog.ts
new file mode 100644
index 00000000..f28d2d8f
--- /dev/null
+++ b/src/app/posthog.ts
@@ -0,0 +1,10 @@
+import { PostHog } from 'posthog-node'
+
+export default function PostHogClient() {
+ const posthogClient = new PostHog(process.env.POSTHOG_KEY!, {
+ host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
+ flushAt: 1, // Flush immediately
+ flushInterval: 0, // No batching delay
+ })
+ return posthogClient
+}
\ No newline at end of file
diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx
new file mode 100644
index 00000000..4440ab16
--- /dev/null
+++ b/src/app/privacy/page.tsx
@@ -0,0 +1,10 @@
+import PrivacyContent from './privacy-content'
+
+export const metadata = {
+ title: 'Privacy Policy',
+ description: 'Privacy policy for remcostoeten.nl - How we collect, use, and protect your data.',
+}
+
+export default function PrivacyPage() {
+ return
+}
\ No newline at end of file
diff --git a/src/app/privacy/privacy-content.tsx b/src/app/privacy/privacy-content.tsx
new file mode 100644
index 00000000..57b70366
--- /dev/null
+++ b/src/app/privacy/privacy-content.tsx
@@ -0,0 +1,214 @@
+'use client';
+
+import { Shield, Eye, Database, Users, Mail, Cookie } from 'lucide-react'
+import { Section, SubSection, TimelineItem } from '@/components/ui/section'
+
+export default function PrivacyContent() {
+ const lastUpdated = new Date().toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ })
+
+ return (
+
+
+
+ Privacy Policy
+
+
+ Last updated: {lastUpdated}
+
+
+
+
+
+
+
+ This Privacy Policy explains how I collect, use, and protect your information when you visit remcostoeten.nl.
+ I believe in transparency and only collect data that's necessary to improve your experience on this site.
+
+
+
+
+
+
+
+
+
+ I use PostHog to understand how people interact with my site. This includes:
+
+
+
Pages you visit and how long you stay
+
General location (country/city level, not precise)
+
Browser type and screen size
+
Interaction with blog posts and features
+
+
+ This data is anonymized and never linked to your personal identity.
+
+
+
+
+
+
+
+ When you sign in with GitHub to leave blog reactions or comment:
+
+
+
Your GitHub username and profile picture
+
Email address (from your GitHub profile)
+
Your blog reactions and comments
+
+
+ I only store what's necessary for authentication and personalization.
+
+
+
+
+
+
+
+ When you contact me through the contact form, I receive your email address and message.
+ This data is only used to respond to your inquiry.
+
+
+
+
+
+
+
+ Basic technical information like IP address, browser headers, and cookies are collected
+ for security and site functionality.
+
+
+
+
+
+
+
+
+
+ Analytics help me understand what content is valuable and where to improve the user experience.
+
+
+ GitHub authentication allows me to show your profile picture and remember your blog reactions.
+
+
+ I only use your contact information to respond to your messages.
+
+
+ Basic data helps me keep the site secure and running smoothly.
+
+
+
+
+
+
+
+ Your data is stored securely on:
+
+
+
Vercel: Analytics and site hosting (EU/US data centers)
+
Neon: User data and blog interactions (EU data centers)
+
GitHub: OAuth authentication data
+
+
+ I take reasonable security measures to protect your data, including encryption for data transmission
+ and limited access to personal information.
+
+
+
+
+
+
+
+
+
Required for the site to function:
+
+
Authentication tokens (if logged in)
+
Theme preference (light/dark mode)
+
Session management
+
+
+
+
+
+
+
+ PostHog cookies for anonymous usage tracking. You can opt out by disabling cookies
+ in your browser or using the privacy settings.
+
+
+
+
+
+
+
+
+
+ Under GDPR and other privacy laws, you have the right to:
+
+
+
Access: Request a copy of your personal data
+
Correction: Request correction of inaccurate data
+
Deletion: Request deletion of your personal data
+
Portability: Request your data in a machine-readable format
+
+
+ To exercise these rights, email me at stoetenremco [dot] rs [at] gmail [dot] com.
+
+
+
+
+
+
+
+ This site uses the following third-party services:
+
+
+
GitHub: OAuth authentication
+
PostHog: Analytics and user behavior tracking
+
Vercel: Website hosting and performance monitoring
+
Neon: Database hosting for user data
+
+
+ Each service has its own privacy policy and data handling practices.
+
+
+
+
+
+
+
+ If you have questions about this Privacy Policy or how I handle your data,
+ please contact me:
+
+
+ stoetenremco [dot] rs [at] gmail [dot] com
+
+
+ I'll respond to your privacy concerns within 7 days.
+
- );
-}
-
-export default function SpotifySetupPage() {
- return (
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx
new file mode 100644
index 00000000..77631e63
--- /dev/null
+++ b/src/app/terms/page.tsx
@@ -0,0 +1,10 @@
+import TermsContent from './terms-content'
+
+export const metadata = {
+ title: 'Terms of Service',
+ description: 'Terms of service for remcostoeten.nl - Rules and guidelines for using this website.',
+}
+
+export default function TermsPage() {
+ return
+}
\ No newline at end of file
diff --git a/src/app/terms/terms-content.tsx b/src/app/terms/terms-content.tsx
new file mode 100644
index 00000000..6461e192
--- /dev/null
+++ b/src/app/terms/terms-content.tsx
@@ -0,0 +1,259 @@
+'use client';
+
+import { FileText, Users, Shield, AlertTriangle, Mail, Github } from 'lucide-react'
+import { Section, SubSection, TimelineItem } from '@/components/ui/section'
+
+export default function TermsContent() {
+ const lastUpdated = new Date().toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ })
+
+ return (
+
+
+
+ Terms of Service
+
+
+ Last updated: {lastUpdated}
+
+
+
+
+
+
+
+ Welcome to remcostoeten.nl. These Terms of Service govern your use of my personal website and blog.
+ By accessing or using this site, you agree to these terms.
+
+
+ This is a personal portfolio and blog. I'm not a company - just a developer sharing my work and thoughts.
+
+
+
+
+
+
+
+ You're welcome to read, share, and discuss blog posts and content. Linking is appreciated!
+
+
+ Code snippets and examples are provided for learning. Check individual licenses for usage terms.
+
+
+ Leave thoughtful comments and reactions on blog posts using GitHub authentication.
+
+
+ Reach out with questions, collaboration opportunities, or feedback.
+
+
+
+
+
+
+
+ Don't copy and republish entire articles without permission. Excerpts with attribution are fine.
+
+
+ No spamming, harassment, or inappropriate behavior in comments or contact forms.
+
+
+ Don't use scrapers, bots, or automated tools without my permission.
+
+
+ Don't attempt to reverse engineer or extract proprietary site features.
+
+
+
+
+
+
+
+
+
+ Blog posts, tutorials, and original content are my intellectual property.
+
+
+
Written content is copyright protected
+
Code examples may have specific licenses
+
Design and layout are proprietary
+
+
+
+
+
+
+
+ When you leave comments or reactions:
+
+
+
You retain ownership of your content
+
You grant me permission to display it on this site
+
You're responsible for what you post
+
+
+
+
+
+
+
+ Limited quoting and sharing for educational, commentary, or criticism purposes
+ is welcome under fair use principles, provided proper attribution is given.
+
+
+
+
+
+
+
+
+
+ This site uses GitHub OAuth for authentication. By signing in:
+
+
+
You're connecting your GitHub account to this site
+
Your public profile information is displayed with your comments
+
You agree to GitHub's Terms of Service
+
You can revoke access anytime from your GitHub settings
+
+
+ I only access the minimum information needed for authentication and personalization.
+
+
+
+
+
+
+
+ Your privacy matters. My Privacy Policy explains:
+
+
+
What data I collect and why
+
How I use and protect your information
+
Your rights regarding your data
+
+
+ Using this site means you consent to the data collection practices described in the Privacy Policy.
+
+
+
+
+
+
+
+
+
+ I do my best to provide accurate information, but content may contain errors or become outdated.
+ Technical tutorials might not work in all environments. Use at your own risk.
+
+
+
+
+
+
+
+ This is a personal blog, not professional advice. Content reflects my personal opinions
+ and experiences, not professional guidance.
+
+
+
+
+
+
+
+
+
+ This is a personal website, not a commercial service. I try to keep it running smoothly,
+ but:
+
+
+
No uptime guarantees or service level agreements
+
Temporary downtime for maintenance may occur
+
Features may change or be removed without notice
+
+
+ This service is provided "as is" without warranties.
+
+
+
+
+
+
+
+ To the fullest extent permitted by law:
+
+
+
I'm not liable for damages arising from site use
+
I'm not responsible for third-party content or links
+
I'm not liable for code issues in tutorials
+
+
+ This is a personal project - use it responsibly and at your own risk.
+
+
+
+
+
+
+
+ I may update these Terms occasionally. Changes are effective immediately upon posting.
+ Continued use of the site means you accept the updated terms.
+
+
+ Major changes will be announced in a blog post or site notice.
+
+
+
+
+
+
+
+ Questions about these Terms or want to report violations?
+
+
+ stoetenremco [dot] rs [at] gmail [dot] com
+
+
+ I'm reasonable and happy to discuss concerns about site usage or content.
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/topics/metadata.ts b/src/app/topics/metadata.ts
deleted file mode 100644
index 0b8e8979..00000000
--- a/src/app/topics/metadata.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const metadata = {
- title: 'Topics',
- description: 'Browse blog posts by topic, category, or tag.',
-}
\ No newline at end of file
diff --git a/src/components/ActivityHoverCard.tsx b/src/components/ActivityHoverCard.tsx
deleted file mode 100644
index e7adc5be..00000000
--- a/src/components/ActivityHoverCard.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-'use client';
-
-import React from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Music, GitBranch, Calendar, Clock, User, ExternalLink, Code, Disc } from 'lucide-react';
-import type { CommitData } from '@/hooks/use-github';
-import type { SpotifyTrack } from '@/core/spotify-service';
-import { getRelativeTime } from '@/core/spotify-service';
-
-interface ActivityHoverCardProps {
- type: 'github' | 'spotify';
- data: CommitData | SpotifyTrack;
- isVisible: boolean;
- position: { x: number; y: number };
-}
-
-const cardVariants = {
- initial: {
- opacity: 0,
- scale: 0.98,
- y: -8
- },
- animate: {
- opacity: 1,
- scale: 1,
- y: 0,
- transition: {
- duration: 0.2
- }
- },
- exit: {
- opacity: 0,
- scale: 0.98,
- y: -8,
- transition: { duration: 0.15 }
- }
-};
-
-export const ActivityHoverCard = React.memo(({ type, data, isVisible, position }: ActivityHoverCardProps) => {
- if (!isVisible) return null;
-
- const isGitHub = type === 'github';
- const commit = data as CommitData & { projectName?: string; color?: string };
- const track = data as SpotifyTrack;
-
- // Calculate dynamic positioning to keep card in viewport and follow mouse
- const getCardPosition = () => {
- const cardWidth = 340; // Approximate card width
- const cardHeight = 200; // Approximate card height
- const offset = 20; // Distance from cursor
-
- let x = position.x + offset;
- let y = position.y + offset;
-
- // Check if card would go off right edge
- if (x + cardWidth > window.innerWidth - 20) {
- x = position.x - cardWidth - offset;
- }
-
- // Check if card would go off left edge
- if (x < 20) {
- x = 20;
- }
-
- // Check if card would go off bottom edge
- if (y + cardHeight > window.innerHeight - 20) {
- y = position.y - cardHeight - offset;
- }
-
- // Check if card would go off top edge
- if (y < 20) {
- y = 20;
- }
-
- return { x, y };
- };
-
- const cardPosition = getCardPosition();
-
- return (
-
-
-