← Back to Archive

How I Built My Personal Site with React and Cloudflare

I recently built this site, my personal website.

Tech Stack

A modern web stack leveraging React and Cloudflare’s ecosystem:

Core: React 19, TypeScript, Vite, Tailwind CSS

Libraries:

  • react-router for routing
  • react-query for data fetching and caching
  • react-markdown for blog content
  • motion for animations
  • react-icons for UI elements
  • tailwindcss for styling
  • three.js for 3D graphics
  • jotai for state management

Dev Tools: ESLint, Prettier, Wrangler, TypeScript

Infrastructure: Cloudflare Pages, Workers, Analytics, DNS, and CDN

Typography: Github Mona Sans, Hubot Sans, JetBrains Mono

Status and Mood Widgets

Spotify Widget

One of my favorite features is the Spotify widget used on the homepage. I avoided CORS problems by not calling the Spotify API directly, instead using Cloudflare Workers to fetch and cache the data.

export async function getCurrentTrack({
  env,
}: {
  env: SpotifyCredentials;
}): Promise<SpotifyTrack> {
  const CLIENT_ID = env.SPOTIFY_CLIENT_ID;
  const CLIENT_SECRET = env.SPOTIFY_CLIENT_SECRET;
  const REFRESH_TOKEN = env.SPOTIFY_REFRESH_TOKEN;

  const accessToken = await getAccessToken({
    clientId: env.SPOTIFY_CLIENT_ID,
    clientSecret: env.SPOTIFY_CLIENT_SECRET,
    refreshToken: env.SPOTIFY_REFRESH_TOKEN,
  });

  // Get currently playing
  const spotifyResponse = await fetch(
    "https://api.spotify.com/v1/me/player/currently-playing",
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    }
  );

  if (spotifyResponse.status === 204 || spotifyResponse.status > 400) {
    // return not playing
  }

  const data: {
    is_playing: boolean;
    item: {
      name: string;
      artists: { name: string }[];
      album: { name: string; images: { url: string }[] };
    };
  } = await spotifyResponse.json();

  return new Response(
    JSON.stringify({
      isPlaying: data.is_playing,
      title: data.item.name,
      artist: data.item.artists[0].name,
      album: data.item.album.name,
      albumArt: data.item.album.images[0].url,
    }),
    {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=30",
        "CDN-Cache-Control": "max-age=30",
      },
    }
  );
}

Mood Widget

The Mood widget displays my current activity based on the time of day in Copenhagen. It uses a time-based system to show different activities and moods throughout weekdays and weekends. For example, during work hours it might show “Martin is coding away 😉⌨️”, while during evenings it could display “Martin is Gaming 😄🎮”.

Weather Widget

A real-time weather display for my location using the OpenWeatherMap API through Cloudflare Workers. It shows current temperature, feels-like temperature, humidity, wind conditions, and cloud coverage. The widget includes intelligent caching with stale-while-revalidate strategy and comprehensive error handling for rate limits. Data is cached for 30 minutes with a 4-hour stale grace period.

interface WeatherData {
  weather: [{
    id: number;
    main: string;
    description: string;
    icon: string;
  }];
  main: {
    temp: number;
    feels_like: number;
    humidity: number;
  };
  wind: {
    speed: number;
    deg: number;
  };
}

Moon Phase Widget

Shows the current lunar phase using the SunCalc library. It displays the current moon phase (e.g., “Waxing Crescent”), along with a countdown to the next full moon in days and hours. The widget updates automatically and includes dynamic icons representing the current phase.

These widgets are built using Framer Motion for smooth animations and are designed to be responsive and visually consistent with the site’s theme.