Теперь, когда мой личный сайт находится в Vercel и написан на Next.js, я решил переработать свою страницу сейчас, используя различные социальные API. Я начал с того, что просмотрел различные платформы, которые я регулярно использую, и отследил те, которые предоставляют доступ к API или RSS-каналы. Для тех, у кого есть API, я написал код для доступа к своим данным через указанные API, для тех, у кого есть только фиды, я использовал @extractus/feed-extractor, чтобы преобразовать их в ответы JSON.
Шаблон /now
в моем каталоге pages
выглядит следующим образом:
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 <Spin className="my-2 flex justify-center" /> return ( <> <PageSEO title={`Now - ${siteMetadata.author}`} description={siteMetadata.description.now} /> <div className="divide-y divide-gray-200 dark:divide-gray-700"> <div className="space-y-2 pt-6 pb-8 md:space-y-5"> <h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14"> Now </h1> </div> <div className="pt-12"> <h3 className="text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14"> Currently </h3> <div className="pl-5 md:pl-10"> <Status status={status} /> <p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100"> <MapPinIcon className="mr-1 inline h-6 w-6" /> Living in Camarillo, California with my beautiful family, 4 rescue dogs and a guinea pig. </p> <p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100"> <CodeBracketIcon className="mr-1 inline h-6 w-6" /> Working at <Link className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" href="https://hashicorp.com" target="_blank" rel="noopener noreferrer" > HashiCorp </Link> </p> <p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100"> <MegaphoneIcon className="mr-1 inline h-6 w-6" /> Rooting for the{` `} <Link className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" href="https://lakers.com" target="_blank" rel="noopener noreferrer" > Lakers </Link> , for better or worse. </p> </div> <h3 className="pt-6 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14"> Making </h3> <div className="pl-5 md:pl-10"> <p className="mt-2 text-lg leading-7 text-gray-500 dark:text-gray-100"> <CommandLineIcon className="mr-1 inline h-6 w-6" /> Hacking away on random projects like this page, my <Link className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" href="/blog" passHref > blog </Link> and whatever else I can find time for. </p> </div> <Artists artists={artists} /> <Albums albums={albums} /> <Reading books={books} /> <Movies movies={movies} /> <TV tv={tv} /> <p className="pt-8 text-center text-xs text-gray-900 dark:text-gray-100"> (This is a{' '} <Link className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" href="https://nownownow.com/about" target="_blank" rel="noopener noreferrer" > now page </Link> , and if you have your own site, <Link className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" href="https://nownownow.com/about" target="_blank" rel="noopener noreferrer" > you should make one, too </Link> .) </p> </div> </div> </> ) }
Вы увидите, что верхняя часть в основном статична, с текстом, стилизованным с помощью Tailwind, и соответствующими значками из пакета Hero Icons. Мы также экспортируем экземпляр getStaticProps
, который перепроверяется каждый час и вызывает метод из моего каталога lib
под названием loadNowData
. loadNowData
принимает строку с разделителями-запятыми в качестве аргумента, чтобы указать, какие свойства я хочу вернуть в составном объекте из этого метода 1. Метод выглядит так 2:
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 = `http://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 = `http://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 = `http://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 }
Отдельные медиа-компоненты текущей страницы просты и презентативны, например, Albums.tsx
:
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 <Spin className="my-12 flex justify-center" /> return ( <> <h3 className="pt-4 pb-4 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-2xl sm:leading-10 md:text-4xl md:leading-14"> Listening: albums </h3> <div className="grid grid-cols-2 gap-2 md:grid-cols-4"> {albums?.map((album) => ( <Cover key={album.mbid} media={album} type="album" /> ))} </div> </> ) } export default Albums
Этот компонент и Artists.tsx
используют Cover.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 ( <Link className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400" href={media.url} target="_blank" rel="noopener noreferrer" title={media.name} > <div className="relative"> <div className="absolute left-0 top-0 h-full w-full rounded-lg border border-primary-500 bg-cover-gradient dark:border-gray-500"></div> <div className="absolute left-1 bottom-2 drop-shadow-md"> <div className="px-1 text-xs font-bold text-white">{media.name}</div> <div className="px-1 text-xs text-white"> {type === 'album' ? media.artist.name : `${media.playcount} plays`} </div> </div> <ImageWithFallback src={image(media)} alt={media.name} className="rounded-lg" width="350" height="350" /> </div> </Link> ) } export default Cover
Все компоненты этой страницы можно посмотреть на GitHub. Каждый из них использует объект из объекта loadNowData
и отображает его на странице. Страница также периодически перепроверяется через маршрут API, который просто вызывает этот же метод:
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) }
И, со всем этим, у нас есть страница с небольшим трафиком, которая обновляется (за некоторыми исключениями), когда я рассказываю о своих привычках использования Last.fm, Trakt, Letterboxd, Oku и так далее.