Back to overview

Show off your Strava stats on your Next.js site (statically!)

Biker riding with Strava active on phone attached to steering wheel
| 5 min read
View count:

I had been playing with the idea to integrate my Strava stats on my website for a while, but never really did any research into it. Last week I decided it was time! I did not want to use the Strava embed, because frankly: it’s ugly. image

Luckily, Strava provides an API with all the information you need to build your own (prettier) widget. You do need to authenticate if you want to use the API, Strava uses OAuth2 for the authentication.

However, before connecting with the API, we have to create a “Strava app” through the the following URL: https://www.strava.com/settings/api

Once your created your app, you will see the following information: image

Most important here is:

The Authorization Callback Domain will not be important for us, since we will not be redirecting a user to a login page to login, we want to show our own stats.

Now this is set up, we can move on to the fun part: communicating with the API, and extracting all the stats we need! Firstly, we will need to get an authorization code from the API. This is a one time process you need to go through. You can go to the following URL in your browser: https://www.strava.com/oauth/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost&scope=read_all (replace YOUR_CLIENT_ID with your unique client ID as seen in the previous section). You should see a screen like this appear: image

Once you clicked on ‘Authorize’ (sorry, my screenshot is in Dutch :D), you will be redirected to a URL much like the following: http://localhost/?state=&code=YOUR_CODE&scope=read,read_all (the actual code will be in the URL instead of YOUR_CODE). This is the code we need to talk to the API.

With this code in hand, we can now request our initial access & refresh token from the API. Do a POST request (I used Postman) to https://www.strava.com/oauth/token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&code=YOUR_CODE&grant_type=authorization_code&scope=read_all (don’t forget to replace the fields with your personal codes). This will return a response looking like this:

{
  "token_type": "Bearer",
  "access_token": "YOUR_ACCESS_TOKEN",
  "athlete": "SUMMARY",
  "refresh_token": "YOUR_REFRESH_TOKEN",
  "expires_at": 1531378346,
  "state": "STRAVA"
}

Because we will want to be refreshing the data we fetch from Strava regularly (daily), we will need to refresh our token for every request to the API. To refresh the token we will need to provide the last access & refresh token (which we received with the API call above).

So we should store our latest access & refresh token somewhere securily.. I opted to do this in Firestore (https://firebase.google.com/docs/firestore), because it’s a simple NOSQL solution, and it has a free tier!

In my Firestore, I added a collection called access_tokens and added a document in there with my initial access_token and refresh_token. image

I have a DB util file which contains the following code to connect and read/write to my Firestore.

import admin from "firebase-admin";

if (!admin.apps.length) {
  try {
    admin.initializeApp({
      credential: admin.credential.cert({
        type: "service_account",
        auth_uri: "https://accounts.google.com/o/oauth2/auth",
        token_uri: "https://oauth2.googleapis.com/token",
        auth_provider_x509_cert_url:
          "https://www.googleapis.com/oauth2/v1/certs",
        client_x509_cert_url:
          "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-j3bwb%40personal-website-e4e38.iam.gserviceaccount.com",
        project_id: process.env.PROJECT_ID,
        private_key_id: process.env.PRIVATE_KEY_ID,
        private_key: process.env.PRIVATE_KEY,
        client_id: process.env.CLIENT_EMAIL,
        client_email: process.env.CLIENT_EMAIL,
      }),
    });
  } catch (error) {
    console.log("Firebase admin initialization error", error.stack);
  }
}
export default admin.firestore();

To link this up to my homepage, I use the built-in getStaticProps function from Next.js (https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation).

In this function I first get the access_tokens from my Firestore, with the old access & refresh token I fetch new tokens from the Strava API.

Once I have the new tokens I can use these to get the stats from my athlete profile! These new tokens I then write to my Firestore for the next fetch.

Lastly, I added a revalidate option to the return of my getStaticProps function, so the data will be refetched every day, so basically Incremental Static Generation (https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration).

export async function getStaticProps(context) {
  const entries = await db.collection("access_tokens").get();
  let [{ access_token, refresh_token }] = entries.docs.map((entry) =>
    entry.data(),
  );
  const resToken = await fetch(
    `https://www.strava.com/api/v3/oauth/token?client_id=${process.env.CLIENT_ID_STRAVA}&client_secret=${process.env.CLIENT_SECRET_STRAVA}&grant_type=refresh_token&refresh_token=${refresh_token}`,
    {
      method: "POST",
    },
  );
  const { access_token: newToken, refresh_token: newRefreshToken } =
    await resToken.json();
  const resStats = await fetch(
    "https://www.strava.com/api/v3/athletes/40229513/stats",
    {
      headers: {
        Authorization: `Bearer ${newToken}`,
      },
    },
  );
  db.collection("access_tokens").doc("CSXyda8OfK75Aw0vtbtZ").update({
    access_token: newToken,
    refresh_token: newRefreshToken,
  });

  const stravaStats = await resStats.json();

  return {
    props: {
      stravaStats,
    },
    revalidate: 86400,
  };
}

The Strava stats you get back from this API call will look something like this:

{
  "biggest_ride_distance": 74704.8,
  "biggest_climb_elevation_gain": 119.4,
  "recent_ride_totals": {
    "count": 9,
    "distance": 375793.09765625,
    "moving_time": 50529,
    "elapsed_time": 54990,
    "elevation_gain": 437.8953437805176,
    "achievement_count": 0
  },
  "all_ride_totals": {
    "count": 17,
    "distance": 652268,
    "moving_time": 93522,
    "elapsed_time": 101368,
    "elevation_gain": 854
  },
  "recent_run_totals": {
    "count": 0,
    "distance": 0,
    "moving_time": 0,
    "elapsed_time": 0,
    "elevation_gain": 0,
    "achievement_count": 0
  },
  "all_run_totals": {
    "count": 43,
    "distance": 319239,
    "moving_time": 97278,
    "elapsed_time": 97837,
    "elevation_gain": 507
  },
  "recent_swim_totals": {
    "count": 0,
    "distance": 0,
    "moving_time": 0,
    "elapsed_time": 0,
    "elevation_gain": 0,
    "achievement_count": 0
  },
  "all_swim_totals": {
    "count": 0,
    "distance": 0,
    "moving_time": 0,
    "elapsed_time": 0,
    "elevation_gain": 0
  },
  "ytd_ride_totals": {
    "count": 12,
    "distance": 458926,
    "moving_time": 61865,
    "elapsed_time": 66791,
    "elevation_gain": 536
  },
  "ytd_run_totals": {
    "count": 11,
    "distance": 70315,
    "moving_time": 19772,
    "elapsed_time": 19897,
    "elevation_gain": 73
  },
  "ytd_swim_totals": {
    "count": 0,
    "distance": 0,
    "moving_time": 0,
    "elapsed_time": 0,
    "elevation_gain": 0
  }
}

I used the all_run_totals and all_ride_totals to build my widget. image

The end result can be found on my website: https://www.thomasledoux.be/#stats. The source code is available on Github: https://github.com/thomasledoux1/website-thomas

If you have any feedback let me know, happy to hear it!

Did you like this post? Check out my latest blog posts:

Screenshot of an analytics dashboard
16 Jan 2024

Basic analytics with Vercel Postgres - Drizzle - Astro

10 min reading time

  • vercel
  • databases
  • typescript
  • astro
Page of a dictionary
15 Nov 2023

A minimal dependency-free translation system for Next.js

6 min reading time

  • nextjs
  • react
  • javascript
Chrome logo repeated in a grid
4 Sept 2023

Developing a Chrome extension to convert the current url to localhost in 1 click

4 min reading time

  • javascript
  • chrome extension
  • html