chore: additional formatting w/prettier

This commit is contained in:
Cory Dransfeldt 2023-03-25 14:09:26 -07:00
parent ea75e585e1
commit ee77555c32
No known key found for this signature in database
39 changed files with 1544 additions and 1584 deletions

View file

@ -15,16 +15,16 @@ My next.js api looks like this:
```typescript
export default async function handler(req: any, res: any) {
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
const KEY_BLOG = process.env.API_KEY_WEBMENTIONS_BLOG_CORYD_DEV
const DOMAIN = req.query.domain
const TARGET = req.query.target
const data = await fetch(
`https://webmention.io/api/mentions.jf2?token=${
DOMAIN === 'coryd.dev' ? KEY_CORYD : KEY_BLOG
}${TARGET ? `&target=${TARGET}` : ''}&per-page=1000`
).then((response) => response.json())
res.json(data)
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
const KEY_BLOG = process.env.API_KEY_WEBMENTIONS_BLOG_CORYD_DEV
const DOMAIN = req.query.domain
const TARGET = req.query.target
const data = await fetch(
`https://webmention.io/api/mentions.jf2?token=${DOMAIN === 'coryd.dev' ? KEY_CORYD : KEY_BLOG}${
TARGET ? `&target=${TARGET}` : ''
}&per-page=1000`
).then((response) => response.json())
res.json(data)
}
```
@ -34,91 +34,85 @@ This is called on the client side as follows:
```javascript
document.addEventListener('DOMContentLoaded', (event) => {
;(function () {
const formatDate = (date) => {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear()
;(function () {
const formatDate = (date) => {
var d = new Date(date),
month = '' + (d.getMonth() + 1),
day = '' + d.getDate(),
year = d.getFullYear()
if (month.length < 2) month = '0' + month
if (day.length < 2) day = '0' + day
if (month.length < 2) month = '0' + month
if (day.length < 2) day = '0' + day
return [month, day, year].join('-')
}
const webmentionsWrapper = document.getElementById('webmentions')
const webmentionsLikesWrapper = document.getElementById('webmentions-likes-wrapper')
const webmentionsBoostsWrapper = document.getElementById('webmentions-boosts-wrapper')
const webmentionsCommentsWrapper = document.getElementById('webmentions-comments-wrapper')
if (webmentionsWrapper && window) {
try {
fetch('https://utils.coryd.dev/api/webmentions?domain=blog.coryd.dev')
.then((response) => response.json())
.then((data) => {
const mentions = data.children
if (mentions.length === 0 || window.location.pathname === '/') {
webmentionsWrapper.remove()
return
}
let likes = ''
let boosts = ''
let comments = ''
mentions.map((mention) => {
if (
mention['wm-property'] === 'like-of' &&
mention['wm-target'].includes(window.location.href)
) {
likes += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
}
if (
mention['wm-property'] === 'repost-of' &&
mention['wm-target'].includes(window.location.href)
) {
boosts += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
}
if (
mention['wm-property'] === 'in-reply-to' &&
mention['wm-target'].includes(window.location.href)
) {
comments += `<div class="webmention-comment"><a href="${
mention.url
}" rel="noopener noreferrer"><div class="webmention-comment-top"><img class="avatar" src="${
mention.author.photo
}" alt="${mention.author.name}" /><div class="time">${formatDate(
mention.published
)}</div></div><div class="comment-body">${
mention.content.text
}</div></a></div>`
}
})
webmentionsLikesWrapper.innerHTML = ''
webmentionsLikesWrapper.insertAdjacentHTML('beforeEnd', likes)
webmentionsBoostsWrapper.innerHTML = ''
webmentionsBoostsWrapper.insertAdjacentHTML('beforeEnd', boosts)
webmentionsCommentsWrapper.innerHTML = ''
webmentionsCommentsWrapper.insertAdjacentHTML('beforeEnd', comments)
webmentionsWrapper.style.opacity = 1
if (likes === '')
document.getElementById('webmentions-likes').innerHTML === ''
if (boosts === '')
document.getElementById('webmentions-boosts').innerHTML === ''
if (comments === '')
document.getElementById('webmentions-comments').innerHTML === ''
if (likes === '' && boosts === '' && comments === '')
webmentionsWrapper.remove()
})
} catch (e) {
webmentionsWrapper.remove()
return [month, day, year].join('-')
}
const webmentionsWrapper = document.getElementById('webmentions')
const webmentionsLikesWrapper = document.getElementById('webmentions-likes-wrapper')
const webmentionsBoostsWrapper = document.getElementById('webmentions-boosts-wrapper')
const webmentionsCommentsWrapper = document.getElementById('webmentions-comments-wrapper')
if (webmentionsWrapper && window) {
try {
fetch('https://utils.coryd.dev/api/webmentions?domain=blog.coryd.dev')
.then((response) => response.json())
.then((data) => {
const mentions = data.children
if (mentions.length === 0 || window.location.pathname === '/') {
webmentionsWrapper.remove()
return
}
}
})()
let likes = ''
let boosts = ''
let comments = ''
mentions.map((mention) => {
if (
mention['wm-property'] === 'like-of' &&
mention['wm-target'].includes(window.location.href)
) {
likes += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
}
if (
mention['wm-property'] === 'repost-of' &&
mention['wm-target'].includes(window.location.href)
) {
boosts += `<a href="${mention.url}" rel="noopener noreferrer"><img class="avatar" src="${mention.author.photo}" alt="${mention.author.name}" /></a>`
}
if (
mention['wm-property'] === 'in-reply-to' &&
mention['wm-target'].includes(window.location.href)
) {
comments += `<div class="webmention-comment"><a href="${
mention.url
}" rel="noopener noreferrer"><div class="webmention-comment-top"><img class="avatar" src="${
mention.author.photo
}" alt="${mention.author.name}" /><div class="time">${formatDate(
mention.published
)}</div></div><div class="comment-body">${mention.content.text}</div></a></div>`
}
})
webmentionsLikesWrapper.innerHTML = ''
webmentionsLikesWrapper.insertAdjacentHTML('beforeEnd', likes)
webmentionsBoostsWrapper.innerHTML = ''
webmentionsBoostsWrapper.insertAdjacentHTML('beforeEnd', boosts)
webmentionsCommentsWrapper.innerHTML = ''
webmentionsCommentsWrapper.insertAdjacentHTML('beforeEnd', comments)
webmentionsWrapper.style.opacity = 1
if (likes === '') document.getElementById('webmentions-likes').innerHTML === ''
if (boosts === '') document.getElementById('webmentions-boosts').innerHTML === ''
if (comments === '') document.getElementById('webmentions-comments').innerHTML === ''
if (likes === '' && boosts === '' && comments === '') webmentionsWrapper.remove()
})
} catch (e) {
webmentionsWrapper.remove()
}
}
})()
})
```
@ -128,18 +122,18 @@ The webmentions HTML shell is as follows:
```html
<div id="webmentions" class="background-purple container">
<div id="webmentions-likes">
<h2><i class="fa-solid fa-fw fa-star"></i> Likes</h2>
<div id="webmentions-likes-wrapper"></div>
</div>
<div id="webmentions-boosts">
<h2><i class="fa-solid fa-fw fa-rocket"></i> Boosts</h2>
<div id="webmentions-boosts-wrapper"></div>
</div>
<div id="webmentions-comments">
<h2><i class="fa-solid fa-fw fa-comment"></i> Comments</h2>
<div id="webmentions-comments-wrapper"></div>
</div>
<div id="webmentions-likes">
<h2><i class="fa-solid fa-fw fa-star"></i> Likes</h2>
<div id="webmentions-likes-wrapper"></div>
</div>
<div id="webmentions-boosts">
<h2><i class="fa-solid fa-fw fa-rocket"></i> Boosts</h2>
<div id="webmentions-boosts-wrapper"></div>
</div>
<div id="webmentions-comments">
<h2><i class="fa-solid fa-fw fa-comment"></i> Comments</h2>
<div id="webmentions-comments-wrapper"></div>
</div>
</div>
```

View file

@ -21,19 +21,19 @@ I'm already exposing my most recently listened tracks and actively read books on
```typescript
export default async function handler(req: any, res: any) {
const KEY = process.env.API_KEY_LASTFM
const METHODS: { [key: string]: string } = {
default: 'user.getrecenttracks',
albums: 'user.gettopalbums',
artists: 'user.gettopartists',
}
const METHOD = METHODS[req.query.type] || METHODS['default']
const data = await fetch(
`http://ws.audioscrobbler.com/2.0/?method=${METHOD}&user=cdme_&api_key=${KEY}&limit=${
req.query.limit || 20
}&format=${req.query.format || 'json'}&period=${req.query.period || 'overall'}`
).then((response) => response.json())
res.json(data)
const KEY = process.env.API_KEY_LASTFM
const METHODS: { [key: string]: string } = {
default: 'user.getrecenttracks',
albums: 'user.gettopalbums',
artists: 'user.gettopartists',
}
const METHOD = METHODS[req.query.type] || METHODS['default']
const data = await fetch(
`http://ws.audioscrobbler.com/2.0/?method=${METHOD}&user=cdme_&api_key=${KEY}&limit=${
req.query.limit || 20
}&format=${req.query.format || 'json'}&period=${req.query.period || 'overall'}`
).then((response) => response.json())
res.json(data)
}
```
@ -45,24 +45,24 @@ Last.fm's API returns album images, but no longer returns artist images. To solv
import siteMetadata from '@/data/siteMetadata'
export default async function handler(req: any, res: any) {
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const ARTIST = req.query.artist
const ALBUM = req.query.album
const MEDIA = ARTIST ? 'artists' : 'albums'
const MEDIA_VAL = ARTIST ? ARTIST : ALBUM
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const ARTIST = req.query.artist
const ALBUM = req.query.album
const MEDIA = ARTIST ? 'artists' : 'albums'
const MEDIA_VAL = ARTIST ? ARTIST : ALBUM
const data = await fetch(`${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`)
.then((response) => {
if (response.status === 200) return `${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`
fetch(
`${host}/api/omg/paste-edit?paste=404-images&editType=append&content=${MEDIA_VAL}`
).then((response) => response.json())
return `${host}/media/404.jpg`
})
.then((image) => image)
res.redirect(data)
const data = await fetch(`${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`)
.then((response) => {
if (response.status === 200) return `${host}/media/${MEDIA}/${MEDIA_VAL}.jpg`
fetch(
`${host}/api/omg/paste-edit?paste=404-images&editType=append&content=${MEDIA_VAL}`
).then((response) => response.json())
return `${host}/media/404.jpg`
})
.then((image) => image)
res.redirect(data)
}
```
@ -73,12 +73,12 @@ import { extract } from '@extractus/feed-extractor'
import siteMetadata from '@/data/siteMetadata'
export default async function handler(req: any, res: any) {
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const url = `${host}/feeds/books`
const result = await extract(url)
res.json(result)
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const url = `${host}/feeds/books`
const result = await extract(url)
res.json(result)
}
```
@ -89,20 +89,20 @@ import { extract } from '@extractus/feed-extractor'
import siteMetadata from '@/data/siteMetadata'
export default async function handler(req: any, res: any) {
const KEY = process.env.API_KEY_TRAKT
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const url = `${host}/feeds/tv?slurm=${KEY}`
const result = await extract(url, {
getExtraEntryFields: (feedEntry) => {
return {
image: feedEntry['media:content']['@_url'],
thumbnail: feedEntry['media:thumbnail']['@_url'],
}
},
})
res.json(result)
const KEY = process.env.API_KEY_TRAKT
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const url = `${host}/feeds/tv?slurm=${KEY}`
const result = await extract(url, {
getExtraEntryFields: (feedEntry) => {
return {
image: feedEntry['media:content']['@_url'],
thumbnail: feedEntry['media:thumbnail']['@_url'],
}
},
})
res.json(result)
}
```
@ -113,12 +113,12 @@ import { extract } from '@extractus/feed-extractor'
import siteMetadata from '@/data/siteMetadata'
export default async function handler(req: any, res: any) {
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const url = `${host}/feeds/movies`
const result = await extract(url)
res.json(result)
const env = process.env.NODE_ENV
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
const url = `${host}/feeds/movies`
const result = await extract(url)
res.json(result)
}
```
@ -133,199 +133,178 @@ import { nowResponseToMarkdown } from '@/utils/transforms'
import { ALBUM_DENYLIST } from '@/utils/constants'
export default async function handler(req: any, res: any) {
const env = process.env.NODE_ENV
const { APP_KEY_OMG, API_KEY_OMG } = process.env
const ACTION_KEY = req.headers.authorization?.split(' ')[1]
const env = process.env.NODE_ENV
const { APP_KEY_OMG, API_KEY_OMG } = process.env
const ACTION_KEY = req.headers.authorization?.split(' ')[1]
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
let host = siteMetadata.siteUrl
if (env === 'development') host = 'http://localhost:3000'
try {
if (ACTION_KEY === APP_KEY_OMG) {
const now = await fetch('https://api.omg.lol/address/cory/pastebin/now.yaml')
.then((res) => res.json())
.then((json) => {
const now = jsYaml.load(json.response.paste.content)
Object.keys(jsYaml.load(json.response.paste.content)).forEach((key) => {
now[key] = listsToMarkdown(now[key])
})
try {
if (ACTION_KEY === APP_KEY_OMG) {
const now = await fetch('https://api.omg.lol/address/cory/pastebin/now.yaml')
.then((res) => res.json())
.then((json) => {
const now = jsYaml.load(json.response.paste.content)
Object.keys(jsYaml.load(json.response.paste.content)).forEach((key) => {
now[key] = listsToMarkdown(now[key])
})
return { now }
})
return { now }
})
const books = await fetch(`${host}/api/books`)
.then((res) => res.json())
.then((json) => {
const data = json.entries
.slice(0, 5)
.map((book: { title: string; link: string }) => {
return {
title: book.title,
link: book.link,
}
})
return {
json: data,
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}) {${getRandomIcon('books')}}`
})
.join('\n'),
}
})
const books = await fetch(`${host}/api/books`)
.then((res) => res.json())
.then((json) => {
const data = json.entries.slice(0, 5).map((book: { title: string; link: string }) => {
return {
title: book.title,
link: book.link,
}
})
return {
json: data,
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}) {${getRandomIcon('books')}}`
})
.join('\n'),
}
})
const movies = await fetch(`${host}/api/movies`)
.then((res) => res.json())
.then((json) => {
const data = json.entries
.slice(0, 5)
.map((movie: { title: string; link: string; description: string }) => {
return {
title: movie.title,
link: movie.link,
desc: movie.description,
}
})
return {
json: data,
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}): ${d.desc} {${getRandomIcon(
'movies'
)}}`
})
.join('\n'),
}
})
const tv = await fetch(`${host}/api/tv`)
.then((res) => res.json())
.then((json) => {
const data = json.entries
.splice(0, 5)
.map(
(episode: {
title: string
link: string
image: string
thumbnail: string
}) => {
return {
title: episode.title,
link: episode.link,
image: episode.image,
thumbnail: episode.thumbnail,
}
}
)
return {
json: data,
html: data
.map((d: any) => {
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div></div><img src='${d.thumbnail}' alt='${d.title}' /></div></a>`
})
.join('\n'),
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}) {${getRandomIcon('tv')}}`
})
.join('\n'),
}
})
const musicArtists = await fetch(
`https://utils.coryd.dev/api/music?type=artists&period=7day&limit=8`
)
.then((res) => res.json())
.then((json) => {
const data = json.topartists.artist.map((a: any) => {
return {
artist: a.name,
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(
a.name
)}`,
image: `${host}/api/media?artist=${a.name
.replace(/\s+/g, '-')
.toLowerCase()}`,
}
})
return {
json: data,
html: data
.map((d: any) => {
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.artist}</div></div><img src='${d.image}' alt='${d.artist}' /></div></a>`
})
.join('\n'),
md: data
.map((d: any) => {
return `- [${d.artist}](${d.link}) {${getRandomIcon('music')}}`
})
.join('\n'),
}
})
const musicAlbums = await fetch(
`https://utils.coryd.dev/api/music?type=albums&period=7day&limit=8`
)
.then((res) => res.json())
.then((json) => {
const data = json.topalbums.album.map((a: any) => ({
title: a.name,
artist: a.artist.name,
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(
a.name
)}`,
image: !ALBUM_DENYLIST.includes(a.name.replace(/\s+/g, '-').toLowerCase())
? a.image[a.image.length - 1]['#text']
: `${host}/api/media?album=${a.name
.replace(/\s+/g, '-')
.toLowerCase()}`,
}))
return {
json: data,
html: data
.map((d: any) => {
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div><div class='text-secondary'>${d.artist}</div></div><img src='${d.image}' alt='${d.title} by ${d.artist}' /></div></a>`
})
.join('\n'),
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}) by ${d.artist} {${getRandomIcon(
'music'
)}}`
})
.join('\n'),
}
})
fetch('https://api.omg.lol/address/cory/now', {
method: 'post',
headers: {
Authorization: `Bearer ${API_KEY_OMG}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: nowResponseToMarkdown({
now,
books,
movies,
tv,
music: {
artists: musicArtists,
albums: musicAlbums,
},
}),
listed: 1,
}),
const movies = await fetch(`${host}/api/movies`)
.then((res) => res.json())
.then((json) => {
const data = json.entries
.slice(0, 5)
.map((movie: { title: string; link: string; description: string }) => {
return {
title: movie.title,
link: movie.link,
desc: movie.description,
}
})
return {
json: data,
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}): ${d.desc} {${getRandomIcon('movies')}}`
})
.join('\n'),
}
})
res.status(200).json({ success: true })
} else {
res.status(401).json({ success: false })
}
} catch (err) {
res.status(500).json({ success: false })
const tv = await fetch(`${host}/api/tv`)
.then((res) => res.json())
.then((json) => {
const data = json.entries
.splice(0, 5)
.map((episode: { title: string; link: string; image: string; thumbnail: string }) => {
return {
title: episode.title,
link: episode.link,
image: episode.image,
thumbnail: episode.thumbnail,
}
})
return {
json: data,
html: data
.map((d: any) => {
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div></div><img src='${d.thumbnail}' alt='${d.title}' /></div></a>`
})
.join('\n'),
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}) {${getRandomIcon('tv')}}`
})
.join('\n'),
}
})
const musicArtists = await fetch(
`https://utils.coryd.dev/api/music?type=artists&period=7day&limit=8`
)
.then((res) => res.json())
.then((json) => {
const data = json.topartists.artist.map((a: any) => {
return {
artist: a.name,
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(a.name)}`,
image: `${host}/api/media?artist=${a.name.replace(/\s+/g, '-').toLowerCase()}`,
}
})
return {
json: data,
html: data
.map((d: any) => {
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.artist}</div></div><img src='${d.image}' alt='${d.artist}' /></div></a>`
})
.join('\n'),
md: data
.map((d: any) => {
return `- [${d.artist}](${d.link}) {${getRandomIcon('music')}}`
})
.join('\n'),
}
})
const musicAlbums = await fetch(
`https://utils.coryd.dev/api/music?type=albums&period=7day&limit=8`
)
.then((res) => res.json())
.then((json) => {
const data = json.topalbums.album.map((a: any) => ({
title: a.name,
artist: a.artist.name,
link: `https://rateyourmusic.com/search?searchterm=${encodeURIComponent(a.name)}`,
image: !ALBUM_DENYLIST.includes(a.name.replace(/\s+/g, '-').toLowerCase())
? a.image[a.image.length - 1]['#text']
: `${host}/api/media?album=${a.name.replace(/\s+/g, '-').toLowerCase()}`,
}))
return {
json: data,
html: data
.map((d: any) => {
return `<div class="container"><a href=${d.link} title='${d.title} by ${d.artist}'><div class='cover'></div><div class='details'><div class='text-main'>${d.title}</div><div class='text-secondary'>${d.artist}</div></div><img src='${d.image}' alt='${d.title} by ${d.artist}' /></div></a>`
})
.join('\n'),
md: data
.map((d: any) => {
return `- [${d.title}](${d.link}) by ${d.artist} {${getRandomIcon('music')}}`
})
.join('\n'),
}
})
fetch('https://api.omg.lol/address/cory/now', {
method: 'post',
headers: {
Authorization: `Bearer ${API_KEY_OMG}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: nowResponseToMarkdown({
now,
books,
movies,
tv,
music: {
artists: musicArtists,
albums: musicAlbums,
},
}),
listed: 1,
}),
})
res.status(200).json({ success: true })
} else {
res.status(401).json({ success: false })
}
} catch (err) {
res.status(500).json({ success: false })
}
}
```
@ -335,14 +314,14 @@ For items displayed from Markdown I'm attaching a random FontAwesome icon (e.g.
```typescript
export const getRandomIcon = (type: string) => {
const icons = {
books: ['book', 'book-bookmark', 'book-open', 'book-open-reader', 'bookmark'],
music: ['music', 'headphones', 'record-vinyl', 'radio', 'guitar', 'compact-disc'],
movies: ['film', 'display', 'video', 'ticket'],
tv: ['tv', 'display', 'video'],
}
const icons = {
books: ['book', 'book-bookmark', 'book-open', 'book-open-reader', 'bookmark'],
music: ['music', 'headphones', 'record-vinyl', 'radio', 'guitar', 'compact-disc'],
movies: ['film', 'display', 'video', 'ticket'],
tv: ['tv', 'display', 'video'],
}
return icons[type][Math.floor(Math.random() * (icons[type].length - 1 - 0))]
return icons[type][Math.floor(Math.random() * (icons[type].length - 1 - 0))]
}
```
@ -351,16 +330,16 @@ As the final step to wrap this up, calls to `/api/now` are made every 8 hours us
```yaml
name: scheduled-cron-job
on:
schedule:
- cron: '0 */8 * * *'
schedule:
- cron: '0 */8 * * *'
jobs:
cron:
runs-on: ubuntu-latest
steps:
- name: scheduled-cron-job
run: |
curl -X POST 'https://utils.coryd.dev/api/now' \
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
cron:
runs-on: ubuntu-latest
steps:
- name: scheduled-cron-job
run: |
curl -X POST 'https://utils.coryd.dev/api/now' \
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
```
This endpoint can also be manually called using another workflow:
@ -369,21 +348,21 @@ This endpoint can also be manually called using another workflow:
name: manual-job
on: [workflow_dispatch]
jobs:
cron:
runs-on: ubuntu-latest
steps:
- name: manual-job
run: |
curl -X POST 'https://utils.coryd.dev/api/now' \
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
cron:
runs-on: ubuntu-latest
steps:
- name: manual-job
run: |
curl -X POST 'https://utils.coryd.dev/api/now' \
-H 'Authorization: Bearer ${{ secrets.ACTION_KEY }}'
```
So far this works seamlessly — if I want to update or add static content I can do so via my yaml paste at paste.lol and the change will roll out in due time.
Questions? Comments? Feel free to get in touch:
- [Email](mailto:hi@coryd.dev)
- [Mastodon](https://social.lol/@cory)
- [Email](mailto:hi@coryd.dev)
- [Mastodon](https://social.lol/@cory)
---

View file

@ -16,100 +16,98 @@ import { SERVICES, TAGS } from './config'
import createMastoPost from './createMastoPost'
export default async function syndicate(init?: string) {
const TOKEN_CORYDDEV_GISTS = process.env.TOKEN_CORYDDEV_GISTS
const GIST_ID_SYNDICATION_CACHE = '406166f337b9ed2d494951757a70b9d1'
const GIST_NAME_SYNDICATION_CACHE = 'syndication-cache.json'
const CLEAN_OBJECT = () => {
const INIT_OBJECT = {}
Object.keys(SERVICES).map((service) => (INIT_OBJECT[service] = []))
return INIT_OBJECT
}
const TOKEN_CORYDDEV_GISTS = process.env.TOKEN_CORYDDEV_GISTS
const GIST_ID_SYNDICATION_CACHE = '406166f337b9ed2d494951757a70b9d1'
const GIST_NAME_SYNDICATION_CACHE = 'syndication-cache.json'
const CLEAN_OBJECT = () => {
const INIT_OBJECT = {}
Object.keys(SERVICES).map((service) => (INIT_OBJECT[service] = []))
return INIT_OBJECT
}
async function hydrateCache() {
const CACHE_DATA = CLEAN_OBJECT()
for (const service in SERVICES) {
const data = await extract(SERVICES[service])
const entries = data?.entries
entries.map((entry: FeedEntry) => CACHE_DATA[service].push(entry.id))
async function hydrateCache() {
const CACHE_DATA = CLEAN_OBJECT()
for (const service in SERVICES) {
const data = await extract(SERVICES[service])
const entries = data?.entries
entries.map((entry: FeedEntry) => CACHE_DATA[service].push(entry.id))
}
await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
'Content-Type': 'application/vnd.github+json',
},
body: JSON.stringify({
gist_id: GIST_ID_SYNDICATION_CACHE,
files: {
'syndication-cache.json': {
content: JSON.stringify(CACHE_DATA),
},
},
}),
})
.then((response) => response.json())
.catch((err) => console.log(err))
}
const DATA = await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`).then(
(response) => response.json()
)
const CONTENT = DATA?.files[GIST_NAME_SYNDICATION_CACHE].content
// rewrite the sync data if init is reset
if (CONTENT === '' || init === 'true') hydrateCache()
if (CONTENT && CONTENT !== '' && !init) {
const existingData = await fetch(
`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`
).then((response) => response.json())
const existingContent = JSON.parse(existingData?.files[GIST_NAME_SYNDICATION_CACHE].content)
for (const service in SERVICES) {
const data = await extract(SERVICES[service], {
getExtraEntryFields: (feedEntry) => {
return {
tags: feedEntry['cd:tags'],
}
},
})
const entries: (FeedEntry & { tags?: string })[] = data?.entries
if (!existingContent[service].includes(entries[0].id)) {
let tags = ''
if (entries[0].tags) {
entries[0].tags
.split(',')
.forEach((a, index) =>
index === 0 ? (tags += `#${toPascalCase(a)}`) : (tags += ` #${toPascalCase(a)}`)
)
tags += ` ${TAGS[service]}`
} else {
tags = TAGS[service]
}
existingContent[service].push(entries[0].id)
createMastoPost(`${entries[0].title} ${entries[0].link} ${tags}`)
await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
'Content-Type': 'application/vnd.github+json',
method: 'PATCH',
headers: {
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
'Content-Type': 'application/vnd.github+json',
},
body: JSON.stringify({
gist_id: GIST_ID_SYNDICATION_CACHE,
files: {
'syndication-cache.json': {
content: JSON.stringify(existingContent),
},
},
body: JSON.stringify({
gist_id: GIST_ID_SYNDICATION_CACHE,
files: {
'syndication-cache.json': {
content: JSON.stringify(CACHE_DATA),
},
},
}),
}),
})
.then((response) => response.json())
.catch((err) => console.log(err))
}
const DATA = await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`).then(
(response) => response.json()
)
const CONTENT = DATA?.files[GIST_NAME_SYNDICATION_CACHE].content
// rewrite the sync data if init is reset
if (CONTENT === '' || init === 'true') hydrateCache()
if (CONTENT && CONTENT !== '' && !init) {
const existingData = await fetch(
`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`
).then((response) => response.json())
const existingContent = JSON.parse(existingData?.files[GIST_NAME_SYNDICATION_CACHE].content)
for (const service in SERVICES) {
const data = await extract(SERVICES[service], {
getExtraEntryFields: (feedEntry) => {
return {
tags: feedEntry['cd:tags'],
}
},
})
const entries: (FeedEntry & { tags?: string })[] = data?.entries
if (!existingContent[service].includes(entries[0].id)) {
let tags = ''
if (entries[0].tags) {
entries[0].tags
.split(',')
.forEach((a, index) =>
index === 0
? (tags += `#${toPascalCase(a)}`)
: (tags += ` #${toPascalCase(a)}`)
)
tags += ` ${TAGS[service]}`
} else {
tags = TAGS[service]
}
existingContent[service].push(entries[0].id)
createMastoPost(`${entries[0].title} ${entries[0].link} ${tags}`)
await fetch(`https://api.github.com/gists/${GIST_ID_SYNDICATION_CACHE}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${TOKEN_CORYDDEV_GISTS}`,
'Content-Type': 'application/vnd.github+json',
},
body: JSON.stringify({
gist_id: GIST_ID_SYNDICATION_CACHE,
files: {
'syndication-cache.json': {
content: JSON.stringify(existingContent),
},
},
}),
})
.then((response) => response.json())
.catch((err) => console.log(err))
}
}
.then((response) => response.json())
.catch((err) => console.log(err))
}
}
}
}
```
@ -119,9 +117,9 @@ Once the cache is hydrated the script will check the feeds available in `lib/syn
```typescript
export const SERVICES = {
'coryd.dev': 'https://coryd.dev/feed.xml',
glass: 'https://glass.photo/coryd/rss',
letterboxd: 'https://letterboxd.com/cdme/rss/',
'coryd.dev': 'https://coryd.dev/feed.xml',
glass: 'https://glass.photo/coryd/rss',
letterboxd: 'https://letterboxd.com/cdme/rss/',
}
```
@ -129,9 +127,9 @@ As we iterate through this object we also attach tags specific to each service u
```typescript
export const TAGS = {
'coryd.dev': '#Blog',
glass: '#Photo #Glass',
letterboxd: '#Movie #Letterboxd',
'coryd.dev': '#Blog',
glass: '#Photo #Glass',
letterboxd: '#Movie #Letterboxd',
}
```
@ -168,7 +166,7 @@ const generateRss = (posts: PostFrontMatter[], page = 'feed.xml') => `
<webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
<lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
<atom:link href="${
siteMetadata.siteUrl
siteMetadata.siteUrl
}/${page}" rel="self" type="application/rss+xml"/>
${posts.map(generateRssItem).join('')}
</channel>
@ -214,18 +212,18 @@ import { MASTODON_INSTANCE } from './config'
const KEY = process.env.API_KEY_MASTODON
const createMastoPost = async (content: string) => {
const formData = new FormData()
formData.append('status', content)
const formData = new FormData()
formData.append('status', content)
const res = await fetch(`${MASTODON_INSTANCE}/api/v1/statuses`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${KEY}`,
},
body: formData,
})
return res.json()
const res = await fetch(`${MASTODON_INSTANCE}/api/v1/statuses`, {
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${KEY}`,
},
body: formData,
})
return res.json()
}
export default createMastoPost
@ -236,16 +234,16 @@ Back at GitHub, this is all kicked off every hour on the hour using the followin
```yaml
name: scheduled-cron-job
on:
schedule:
- cron: '0 * * * *'
schedule:
- cron: '0 * * * *'
jobs:
cron:
runs-on: ubuntu-latest
steps:
- name: scheduled-cron-job
run: |
curl -X POST 'https://coryd.dev/api/syndicate' \
-H 'Authorization: Bearer ${{ secrets.VERCEL_SYNDICATE_KEY }}'
cron:
runs-on: ubuntu-latest
steps:
- name: scheduled-cron-job
run: |
curl -X POST 'https://coryd.dev/api/syndicate' \
-H 'Authorization: Bearer ${{ secrets.VERCEL_SYNDICATE_KEY }}'
```
Now, as I post things elsewhere, they'll make their way back to Mastodon with a simple title, link and tag set. Read them if you'd like, or filter them out altogether.

View file

@ -17,10 +17,10 @@ import Link from 'next/link'
import { PageSEO } from '@/components/SEO'
import { Spin } from '@/components/Loading'
import {
MapPinIcon,
CodeBracketIcon,
MegaphoneIcon,
CommandLineIcon,
MapPinIcon,
CodeBracketIcon,
MegaphoneIcon,
CommandLineIcon,
} from '@heroicons/react/24/solid'
import Status from '@/components/Status'
import Albums from '@/components/media/Albums'
@ -34,111 +34,107 @@ 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,
}
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
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" />
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>
</>
)
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>
</>
)
}
```
@ -151,113 +147,113 @@ 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'
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
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 {}
})
}
// 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 {}
})
}
// 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 {}
})
}
// 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 {}
})
}
// 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)
}
// 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)
}
// 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 {}
})
}
// 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]
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
// unified response
return res
}
```
@ -269,22 +265,22 @@ import { Spin } from '@/components/Loading'
import { Album } from '@/types/api'
const Albums = (props: { albums: Album[] }) => {
const { albums } = props
const { albums } = props
if (!albums) return <Spin className="my-12 flex justify-center" />
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>
</>
)
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
@ -299,44 +295,44 @@ 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
}
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>
)
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
@ -348,11 +344,11 @@ All of the components for this page [can be viewed on GitHub](https://github.com
import loadNowData from '@/lib/now'
export default async function handler(req, res) {
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate')
const endpoints = req.query.endpoints
const response = await loadNowData(endpoints)
res.json(response)
const endpoints = req.query.endpoints
const response = await loadNowData(endpoints)
res.json(response)
}
```

View file

@ -13,13 +13,13 @@ My /now page is a series of discreet sections — the **Currently** block is [po
const EleventyFetch = require('@11ty/eleventy-fetch')
module.exports = async function () {
const url = 'https://api.omg.lol/address/cory/statuses/'
const res = EleventyFetch(url, {
duration: '1h',
type: 'json',
})
const status = await res
return status.response.statuses[0]
const url = 'https://api.omg.lol/address/cory/statuses/'
const res = EleventyFetch(url, {
duration: '1h',
type: 'json',
})
const status = await res
return status.response.statuses[0]
}
```
@ -29,14 +29,14 @@ The **Listening: artists** and **Listening: albums** sections draw on data from
const EleventyFetch = require('@11ty/eleventy-fetch')
module.exports = async function () {
const MUSIC_KEY = process.env.API_KEY_LASTFM
const url = `http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
const res = EleventyFetch(url, {
duration: '1h',
type: 'json',
})
const artists = await res
return artists.topartists.artist
const MUSIC_KEY = process.env.API_KEY_LASTFM
const url = `http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=cdme_&api_key=${MUSIC_KEY}&limit=8&format=json&period=7day`
const res = EleventyFetch(url, {
duration: '1h',
type: 'json',
})
const artists = await res
return artists.topartists.artist
}
```
@ -149,13 +149,13 @@ const { extract } = require('@extractus/feed-extractor')
const { AssetCache } = require('@11ty/eleventy-fetch')
module.exports = async function () {
const url = 'https://oku.club/rss/collection/POaRa'
const asset = new AssetCache('books_data')
if (asset.isCacheValid('1h')) return await asset.getCachedValue()
const res = await extract(url).catch((error) => {})
const data = res.entries
await asset.save(data, 'json')
return data
const url = 'https://oku.club/rss/collection/POaRa'
const asset = new AssetCache('books_data')
if (asset.isCacheValid('1h')) return await asset.getCachedValue()
const res = await extract(url).catch((error) => {})
const data = res.entries
await asset.save(data, 'json')
return data
}
```

View file

@ -15,9 +15,9 @@ Once you've added the appropriate tags from webmention.io, connected your desire
import loadWebmentions from '@/lib/webmentions'
export default async function handler(req, res) {
const target = req.query.target
const response = await loadWebmentions(target)
res.json(response)
const target = req.query.target
const response = await loadWebmentions(target)
res.json(response)
}
```
@ -36,125 +36,119 @@ import Image from 'next/image'
import { formatDate } from '@/utils/formatters'
const WebmentionsCore = () => {
const { asPath } = useRouter()
const { response, error } = useJson(`/api/webmentions?target=${siteMetadata.siteUrl}${asPath}`)
const webmentions = response?.children
const hasLikes =
webmentions?.filter((mention) => mention['wm-property'] === 'like-of').length > 0
const hasComments =
webmentions?.filter((mention) => mention['wm-property'] === 'in-reply-to').length > 0
const boostsCount = webmentions?.filter(
(mention) =>
mention['wm-property'] === 'repost-of' || mention['wm-property'] === 'mention-of'
).length
const hasBoosts = boostsCount > 0
const hasMention = hasLikes || hasComments || hasBoosts
const { asPath } = useRouter()
const { response, error } = useJson(`/api/webmentions?target=${siteMetadata.siteUrl}${asPath}`)
const webmentions = response?.children
const hasLikes = webmentions?.filter((mention) => mention['wm-property'] === 'like-of').length > 0
const hasComments =
webmentions?.filter((mention) => mention['wm-property'] === 'in-reply-to').length > 0
const boostsCount = webmentions?.filter(
(mention) => mention['wm-property'] === 'repost-of' || mention['wm-property'] === 'mention-of'
).length
const hasBoosts = boostsCount > 0
const hasMention = hasLikes || hasComments || hasBoosts
if (error) return null
if (!response) return <Spin className="my-2 flex justify-center" />
const Boosts = () => {
return (
<div className="flex flex-row items-center">
<div className="mr-2 h-5 w-5">
<Rocket />
</div>
{` `}
<span className="text-sm">{boostsCount}</span>
</div>
)
}
const Likes = () => (
<>
<div className="flex flex-row items-center">
<div className="mr-2 h-5 w-5">
<Heart />
</div>
<ul className="ml-2 flex flex-row">
{webmentions?.map((mention) => {
if (mention['wm-property'] === 'like-of')
return (
<li key={mention['wm-id']} className="-ml-2">
<Link
href={mention.url}
target="_blank"
rel="noopener noreferrer"
>
<Image
className="h-10 w-10 rounded-full border border-primary-500 dark:border-gray-500"
src={mention.author.photo}
alt={mention.author.name}
width="40"
height="40"
/>
</Link>
</li>
)
})}
</ul>
</div>
</>
)
const Comments = () => {
return (
<>
{webmentions?.map((mention) => {
if (mention['wm-property'] === 'in-reply-to') {
return (
<Link
className="border-bottom flex flex-row items-center border-gray-100 pb-4"
key={mention['wm-id']}
href={mention.url}
target="_blank"
rel="noopener noreferrer"
>
<Image
className="h-12 w-12 rounded-full border border-primary-500 dark:border-gray-500"
src={mention.author.photo}
alt={mention.author.name}
width="48"
height="48"
/>
<div className="ml-3">
<p className="text-sm">{mention.content?.text}</p>
<p className="mt-1 text-xs">{formatDate(mention.published)}</p>
</div>
</Link>
)
}
})}
</>
)
}
if (error) return null
if (!response) return <Spin className="my-2 flex justify-center" />
const Boosts = () => {
return (
<>
{hasMention ? (
<div className="text-gray-500 dark:text-gray-100">
<h4 className="pt-3 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-2xl md:leading-10 ">
Webmentions
</h4>
{hasBoosts ? (
<div className="pt-2 pb-4">
<Boosts />
</div>
) : null}
{hasLikes ? (
<div className="pt-2 pb-4">
<Likes />
</div>
) : null}
{hasComments ? (
<div className="pt-2 pb-4">
<Comments />
</div>
) : null}
</div>
) : null}
</>
<div className="flex flex-row items-center">
<div className="mr-2 h-5 w-5">
<Rocket />
</div>
{` `}
<span className="text-sm">{boostsCount}</span>
</div>
)
}
const Likes = () => (
<>
<div className="flex flex-row items-center">
<div className="mr-2 h-5 w-5">
<Heart />
</div>
<ul className="ml-2 flex flex-row">
{webmentions?.map((mention) => {
if (mention['wm-property'] === 'like-of')
return (
<li key={mention['wm-id']} className="-ml-2">
<Link href={mention.url} target="_blank" rel="noopener noreferrer">
<Image
className="h-10 w-10 rounded-full border border-primary-500 dark:border-gray-500"
src={mention.author.photo}
alt={mention.author.name}
width="40"
height="40"
/>
</Link>
</li>
)
})}
</ul>
</div>
</>
)
const Comments = () => {
return (
<>
{webmentions?.map((mention) => {
if (mention['wm-property'] === 'in-reply-to') {
return (
<Link
className="border-bottom flex flex-row items-center border-gray-100 pb-4"
key={mention['wm-id']}
href={mention.url}
target="_blank"
rel="noopener noreferrer"
>
<Image
className="h-12 w-12 rounded-full border border-primary-500 dark:border-gray-500"
src={mention.author.photo}
alt={mention.author.name}
width="48"
height="48"
/>
<div className="ml-3">
<p className="text-sm">{mention.content?.text}</p>
<p className="mt-1 text-xs">{formatDate(mention.published)}</p>
</div>
</Link>
)
}
})}
</>
)
}
return (
<>
{hasMention ? (
<div className="text-gray-500 dark:text-gray-100">
<h4 className="pt-3 text-xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:text-2xl md:leading-10 ">
Webmentions
</h4>
{hasBoosts ? (
<div className="pt-2 pb-4">
<Boosts />
</div>
) : null}
{hasLikes ? (
<div className="pt-2 pb-4">
<Likes />
</div>
) : null}
{hasComments ? (
<div className="pt-2 pb-4">
<Comments />
</div>
) : null}
</div>
) : null}
</>
)
}
export default WebmentionsCore
@ -167,22 +161,22 @@ import { useEffect, useState } from 'react'
import useSWR from 'swr'
export const useJson = (url: string, props?: any) => {
const [response, setResponse] = useState<any>({})
const [response, setResponse] = useState<any>({})
const fetcher = (url: string) =>
fetch(url)
.then((res) => res.json())
.catch()
const { data, error } = useSWR(url, fetcher, { fallbackData: props, refreshInterval: 30000 })
const fetcher = (url: string) =>
fetch(url)
.then((res) => res.json())
.catch()
const { data, error } = useSWR(url, fetcher, { fallbackData: props, refreshInterval: 30000 })
useEffect(() => {
setResponse(data)
}, [data, setResponse])
useEffect(() => {
setResponse(data)
}, [data, setResponse])
return {
response,
error,
}
return {
response,
error,
}
}
```
@ -195,8 +189,8 @@ import dynamic from 'next/dynamic'
import { Spin } from '@/components/Loading'
const Webmentions = dynamic(() => import('@/components/webmentions/WebmentionsCore'), {
ssr: false,
loading: () => <Spin className="my-2 flex justify-center" />,
ssr: false,
loading: () => <Spin className="my-2 flex justify-center" />,
})
export default Webmentions

View file

@ -5,9 +5,10 @@ draft: false
tags: ['.env', '11ty', 'eleventy']
---
**dotenv-flow:**
**dotenv-flow:**
> **dotenv-flow** extends **dotenv** adding the ability to have multiple `.env*` files like `.env.development`, `.env.test` and `.env.production`, also allowing defined variables to be overwritten individually in the appropriate `.env*.local` file.
The Eleventy docs recommend the `dotenv` package for working with `.env` files[^1], but I've found `dotenv-flow` to be a bit more useful inasmuch as support for `.env*` file patterns make development more convenient.<!-- excerpt -->
[^1]: Which is awesome — it works perfectly.
[^1]: Which is awesome — it works perfectly.

View file

@ -14,24 +14,24 @@ To update my feeds ([feed.xml](https://coryd.dev/feed.xml) and [follow.xml](http
```yaml
name: Scheduled Vercel build
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
schedule:
- cron: '0 * * * *'
schedule:
- cron: '0 * * * *'
jobs:
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
```
{% endraw %}
@ -47,22 +47,22 @@ If you need to manually trigger a build, you can do so using a workflow with a {
```yaml
name: Manual Vercel build
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on: [workflow_dispatch]
jobs:
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
```
{% endraw %}

View file

@ -17,16 +17,16 @@ I'm fetching data from [webmention.io](https://webmention.io) at build time in `
const EleventyFetch = require('@11ty/eleventy-fetch')
module.exports = async function () {
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
const url = `https://webmention.io/api/mentions.jf2?token=${KEY_CORYD}&per-page=1000`
const res = EleventyFetch(url, {
duration: '1h',
type: 'json',
})
const webmentions = await res
return {
mentions: webmentions.children,
}
const KEY_CORYD = process.env.API_KEY_WEBMENTIONS_CORYD_DEV
const url = `https://webmention.io/api/mentions.jf2?token=${KEY_CORYD}&per-page=1000`
const res = EleventyFetch(url, {
duration: '1h',
type: 'json',
})
const webmentions = await res
return {
mentions: webmentions.children,
}
}
```
@ -35,29 +35,30 @@ I have cache duration set to `1h` and a scheduled build operating on approximate
```yaml
name: Scheduled Vercel build
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
schedule:
- cron: '0 * * * *'
schedule:
- cron: '0 * * * *'
jobs:
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
cron:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
```
When the build runs, it renders any mentions of a given post via a [liquid.js](https://liquidjs.com/) template that looks like this:
{% raw %}
```liquid
{% if webmentions %}
<div class="border-t border-gray-200 mt-12 pt-14 dark:border-gray-700">
@ -130,6 +131,7 @@ When the build runs, it renders any mentions of a given post via a [liquid.js](h
</div>
{% endif %}
```
{% endraw %}
This conditionally displays different mention types based on the available data after being passed through the `webmentionsByUrl` filter which I shamelessly lifted from [Robb](https://github.com/rknightuk/rknight.me/blob/8e2a5c5f886cae6c04add7893b8bf8a2d6295ddf/config/filters.js#L48-L84).