--- date: '2023-02-20' title: 'Building a now page using Next.js and social APIs' draft: false tags: ['Next.js', 'React', 'API'] --- With my personal site now sitting at Vercel and written in Next.js I decided to rework my [now](https://coryd.dev/now) page by leveraging a variety of social APIs. I kicked things off by looking through various platforms I use regularly and tracking down those that provide either API access or RSS feeds. For those with APIs I wrote code to access my data via said APIs, for those with feeds only I've leveraged [@extractus/feed-extractor](https://www.npmjs.com/package/@extractus/feed-extractor) to transform them to JSON responses. The `/now` template in my `pages` directory looks like the following: ```jsx import siteMetadata from '@/data/siteMetadata' import loadNowData from '@/lib/now' import { useJson } from '@/hooks/useJson' import Link from 'next/link' import { PageSEO } from '@/components/SEO' import { Spin } from '@/components/Loading' import { MapPinIcon, CodeBracketIcon, MegaphoneIcon, CommandLineIcon, } from '@heroicons/react/24/solid' import Status from '@/components/Status' import Albums from '@/components/media/Albums' import Artists from '@/components/media/Artists' import Reading from '@/components/media/Reading' import Movies from '@/components/media/Movies' import TV from '@/components/media/TV' const env = process.env.NODE_ENV let host = siteMetadata.siteUrl if (env === 'development') host = 'http://localhost:3000' export async function getStaticProps() { return { props: await loadNowData('status,artists,albums,books,movies,tv'), revalidate: 3600, } } export default function Now(props) { const { response, error } = useJson(`${host}/api/now`, props) const { status, artists, albums, books, movies, tv } = response if (error) return null if (!response) return return ( <>

Now

Currently

Living in Camarillo, California with my beautiful family, 4 rescue dogs and a guinea pig.

Working at HashiCorp

Rooting for the{` `} Lakers , for better or worse.

Making

Hacking away on random projects like this page, my blog and whatever else I can find time for.

(This is a{' '} now page , and if you have your own site, you should make one, too .)

) } ``` You'll see that the top section is largely static, with text styled using Tailwind and associated icons from the [Hero Icons](https://heroicons.com) package. We're also exporting an instance of `getStaticProps` that's revalidated every hour and makes a call to a method exposed from my `lib` directory called `loadNowData`. `loadNowData` takes a comma delimited string as an argument to indicate which properties I want returned in the composed object from that method[^1]. The method looks like this[^2]: ```typescript import { extract } from '@extractus/feed-extractor' import siteMetadata from '@/data/siteMetadata' import { Albums, Artists, Status, TransformedRss } from '@/types/api' import { Tracks } from '@/types/api/tracks' export default async function loadNowData(endpoints?: string) { const selectedEndpoints = endpoints?.split(',') || null const TV_KEY = process.env.API_KEY_TRAKT const MUSIC_KEY = process.env.API_KEY_LASTFM const env = process.env.NODE_ENV let host = siteMetadata.siteUrl if (env === 'development') host = 'http://localhost:3000' let statusJson = null let artistsJson = null let albumsJson = null let booksJson = null let moviesJson = null let tvJson = null let currentTrackJson = null // status if ((endpoints && selectedEndpoints.includes('status')) || !endpoints) { const statusUrl = 'https://api.omg.lol/address/cory/statuses/' statusJson = await fetch(statusUrl) .then((response) => response.json()) .catch((error) => { console.log(error) return {} }) } // artists if ((endpoints && selectedEndpoints.includes('artists')) || !endpoints) { const artistsUrl = `https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day` artistsJson = await fetch(artistsUrl) .then((response) => response.json()) .catch((error) => { console.log(error) return {} }) } // albums if ((endpoints && selectedEndpoints.includes('albums')) || !endpoints) { const albumsUrl = `https://ws.audioscrobbler.com/2.0/?method=user.gettopalbums&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day` albumsJson = await fetch(albumsUrl) .then((response) => response.json()) .catch((error) => { console.log(error) return {} }) } // books if ((endpoints && selectedEndpoints.includes('books')) || !endpoints) { const booksUrl = `${host}/feeds/books` booksJson = await extract(booksUrl).catch((error) => { console.log(error) return {} }) } // movies if ((endpoints && selectedEndpoints.includes('movies')) || !endpoints) { const moviesUrl = `${host}/feeds/movies` moviesJson = await extract(moviesUrl).catch((error) => { console.log(error) return {} }) moviesJson.entries = moviesJson.entries.splice(0, 5) } // tv if ((endpoints && selectedEndpoints.includes('tv')) || !endpoints) { const tvUrl = `${host}/feeds/tv?slurm=${TV_KEY}` tvJson = await extract(tvUrl).catch((error) => { console.log(error) return {} }) tvJson.entries = tvJson.entries.splice(0, 5) } // current track if ((endpoints && selectedEndpoints.includes('currentTrack')) || !endpoints) { const currentTrackUrl = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=cdme_&api_key=${MUSIC_KEY}&limit=1&format=json&period=7day` currentTrackJson = await fetch(currentTrackUrl) .then((response) => response.json()) .catch((error) => { console.log(error) return {} }) } const res: { status?: Status artists?: Artists albums?: Albums books?: TransformedRss movies?: TransformedRss tv?: TransformedRss currentTrack?: Tracks } = {} if (statusJson) res.status = statusJson.response.statuses.splice(0, 1)[0] if (artistsJson) res.artists = artistsJson?.topartists.artist if (albumsJson) res.albums = albumsJson?.topalbums.album if (booksJson) res.books = booksJson?.entries if (moviesJson) res.movies = moviesJson?.entries if (tvJson) res.tv = tvJson?.entries if (currentTrackJson) res.currentTrack = currentTrackJson?.recenttracks?.track?.[0] // unified response return res } ``` The individual media components of the now page are simple and presentational, for example, `Albums.tsx`: ```jsx import Cover from '@/components/media/display/Cover' import { Spin } from '@/components/Loading' import { Album } from '@/types/api' const Albums = (props: { albums: Album[] }) => { const { albums } = props if (!albums) return return ( <>

Listening: albums

{albums?.map((album) => ( ))}
) } export default Albums ``` This component and `Artists.tsx` leverage `Cover.tsx`, which renders music related elements: ```tsx import { Media } from '@/types/api' import ImageWithFallback from '@/components/ImageWithFallback' import Link from 'next/link' import { ALBUM_DENYLIST } from '@/utils/constants' const Cover = (props: { media: Media; type: 'artist' | 'album' }) => { const { media, type } = props const image = (media: Media) => { let img = '' if (type === 'album') img = !ALBUM_DENYLIST.includes(media.name.replace(/\s+/g, '-').toLowerCase()) ? media.image[media.image.length - 1]['#text'] : `/media/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg` if (type === 'artist') img = `/media/artists/${media.name.replace(/\s+/g, '-').toLowerCase()}.jpg` return img } return (
{media.name}
{type === 'album' ? media.artist.name : `${media.playcount} plays`}
) } export default Cover ``` All the components for this page [can be viewed on GitHub](https://github.com/cdransf/coryd.dev/tree/main/components/media). Each one consumes an object from the `loadNowData` object and renders it to the page. The page is also periodically revalidated via an api route that simply calls this same method: ```ts import loadNowData from '@/lib/now' export default async function handler(req, res) { res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate') const endpoints = req.query.endpoints const response = await loadNowData(endpoints) res.json(response) } ``` And, with all of that in place, we have a lightly trafficked page that updates itself (with a few exceptions) as I go about my habits of using Last.fm, Trakt, Letterboxd, Oku and so forth. [^1]: I know about GraphQL, but we're just going to deal with plain old fetch calls here. [^2]: It's also leveraged on the index view of my site to fetch my status, currently playing track and the books I'm currently reading.