feat: feed/search/sitemap

This commit is contained in:
Cory Dransfeldt 2024-10-11 19:10:20 -07:00
parent 086cd20788
commit c6d00b2836
No known key found for this signature in database
34 changed files with 198 additions and 535 deletions

View file

@ -1,196 +1,6 @@
import { DateTime } from 'luxon'
import markdownIt from 'markdown-it'
import ics from 'ics'
const BASE_URL = 'https://coryd.dev'
const md = markdownIt()
const normalizeWord = (word) => {
if (!word) return ''
const wordMap = {
'ai': 'AI',
'css': 'CSS',
'ios': 'iOS',
'javascript': 'JavaScript',
'macos': 'macOS',
'tv': 'TV'
}
return wordMap[word?.toLowerCase()] || word?.charAt(0).toUpperCase() + word.slice(1)
}
const tagsToHashtags = (item) => {
const tags = item?.['tags'] || []
if (tags.length) return tags.map(tag => '#' + normalizeWord(tag)).join(' ')
return ''
}
export const processContent = (collection) => {
const siteMapContent = []
const searchIndex = []
const aggregateContent = []
let id = 0
const collectionData = collection.getAll()[0]
const { data } = collectionData
const { posts, links, movies, books, pages, artists, genres, tv, concerts, albumReleases } = data
const parseDate = (date) => {
if (!date) return null
const formats = [
{ method: 'fromISO' },
{ method: 'fromFormat', format: 'yyyy-MM-dd' },
{ method: 'fromFormat', format: 'MM/dd/yyyy' },
{ method: 'fromFormat', format: 'dd-MM-yyyy' }
]
for (const { method, format } of formats) {
const parsedDate = format
? DateTime[method](date, format)
: DateTime[method](date)
if (parsedDate.isValid) return parsedDate
}
return null
}
const absoluteUrl = (url) => new URL(url, BASE_URL).toString()
const isValidUrl = (url) => {
try {
new URL(url)
return true
} catch {
return false
}
}
const addSiteMapContent = (items, getTitle, getDate) => {
const addedUrls = new Set()
if (items) {
items.forEach((item) => {
let url = item?.['permalink'] || item?.['url']
if (!url || addedUrls.has(url)) return
if (!isValidUrl(url)) url = absoluteUrl(url)
if (addedUrls.has(url)) return
const parsedDate = getDate ? parseDate(getDate(item)) : null
const formattedDate = parsedDate ? parsedDate.toFormat("yyyy-MM-dd'T'HH:mm:ssZZ") : null
const content = {
url,
title: getTitle(item),
date: formattedDate
}
siteMapContent.push(content)
addedUrls.add(url)
})
}
}
const addItemToIndex = (items, icon, getUrl, getTitle, getTags) => {
if (items) {
items.forEach((item) => {
const url = getUrl(item)
if (!url) return
const absoluteUrlString = isValidUrl(url) ? url : absoluteUrl(url)
searchIndex.push({
id,
url: absoluteUrlString,
title: `${icon}: ${getTitle(item)}`,
tags: getTags ? getTags(item) : []
})
id++
})
}
}
const addContent = (items, icon, getTitle, getDate) => {
if (items) {
items.forEach((item) => {
let attribution = ''
let hashTags = tagsToHashtags(item) ? ' ' + tagsToHashtags(item) : ''
if (item['type'] === 'album-release') hashTags = ' #Music #NewMusic'
if (item['type'] === 'concert') hashTags = ' #Music #Concert'
if (item?.['author']?.['mastodon']) {
const mastoUrl = new URL(item['author']['mastodon'])
attribution = `${mastoUrl.pathname.replace('/', '')}@${mastoUrl.host}`
} else if (item?.['author']?.['name']) {
attribution = item['author']['name']
}
let url = item['url'] || item['link']
if (url && !isValidUrl(url)) url = absoluteUrl(url)
if (item['type'] === 'concert') url = `${item['artist']?.['url'] ? item['artist']['url'] : BASE_URL + '/music/concerts'}?t=${DateTime.fromISO(item['date']).toMillis()}${item['artist']?.['url'] ? '#concerts' : ''}`
const content = {
url,
title: `${icon}: ${getTitle(item)}${attribution ? ' via ' + attribution : ''}${hashTags}`,
type: item['type']
}
if (item['description']) {
content['description'] = md.render(item['description'])
} else if (item['notes']) {
content['notes'] = md.render(item['notes'])
} else {
content['description'] = ''
}
const date = getDate ? parseDate(getDate(item)) : null
if (date) content['date'] = date
aggregateContent.push(content)
})
}
}
const movieData = movies['movies'].filter((movie) => movie['rating'])
const showData = tv['shows'].filter((show) => show?.['episode']?.['formatted_episode'])
const bookData = books['all'].filter((book) => book['rating'])
addItemToIndex(posts, '📝', (item) => item['url'], (item) => item['title'], (item) => item['tags'])
addItemToIndex(links, '🔗', (item) => item['link'], (item) => item['title'], (item) => item['tags'])
addItemToIndex(artists, '🎙️', (item) => item['url'], (item) => `${item['name']} (${item['country']}) - ${item['genre']?.['name']}`, (item) => `['${item['genre']}']`)
addItemToIndex(genres, '🎵', (item) => item['url'], (item) => item['name'], (item) => item.artists.map(artist => artist['name_string']))
if (movieData) addItemToIndex(movieData, '🎥', (item) => item['url'], (item) => `${item['title']} (${item['rating']})`, (item) => item['tags'])
if (showData) addItemToIndex(showData, '📺', (item) => item['url'], (item) => `${item['title']} (${item['year']})`, (item) => item['tags'])
if (bookData) addItemToIndex(bookData, '📖', (item) => item['url'], (item) => `${item['title']} (${item['rating']})`, (item) => item['tags'])
addContent(posts, '📝', (item) => item['title'], (item) => item['date'])
addContent(links, '🔗', (item) => item['title'], (item) => item['date'])
addContent(books.all.filter((book) => book['status'] === 'finished'), '📖', (item) => `${item['title']}${item['rating'] ? ' (' + item['rating'] + ')' : ''}`, (item) => item['date'])
addContent(movies['movies'], '🎥', (item) => `${item['title']}${item['rating'] ? ' (' + item['rating'] + ')' : ''}`, (item) => item['lastWatched'])
addContent(concerts, '🎤', (item) => `${item['artistNameString'] ? item['artistNameString'] : item['artist']['name']} at ${item['venue']['name'].split(',')[0].trim()}`, (item) => item['date'])
addContent([...albumReleases['current']].reverse(), '📆', (item) => `${item['title']} by ${item['artist']['name']}`, (item) => item['release_date'])
addSiteMapContent(posts, (item) => item['title'], (item) => item['date'])
addSiteMapContent(pages, (item) => item['title'], (item) => item['date'])
addSiteMapContent(artists, (item) => item['name'], (item) => item['date'])
addSiteMapContent(genres, (item) => item['name'], (item) => item['date'])
addSiteMapContent(movies['movies'], (item) => item['title'], (item) => item['date'])
addSiteMapContent(books.all, (item) => item['title'], (item) => item['date'])
addSiteMapContent(tv?.['shows'], (item) => item['title'], (item) => item['date'])
return {
searchIndex,
allContent: aggregateContent.sort((a, b) => {
const dateA = a['date'] ? DateTime.fromISO(a['date']) : DateTime.fromMillis(0)
const dateB = b['date'] ? DateTime.fromISO(b['date']) : DateTime.fromMillis(0)
return dateB - dateA
}),
siteMap: siteMapContent.sort((a, b) => {
const dateA = a['date'] ? DateTime.fromISO(a['date']) : DateTime.fromMillis(0)
const dateB = b['date'] ? DateTime.fromISO(b['date']) : DateTime.fromMillis(0)
return dateB - dateA
})
}
}
export const albumReleasesCalendar = (collection) => {
const collectionData = collection.getAll()[0]
const { data } = collectionData
@ -198,7 +8,7 @@ export const albumReleasesCalendar = (collection) => {
if (!all || all.length === 0) return ''
const events = all.map(album => {
const date = DateTime.fromFormat(album['date'], 'MMMM d, yyyy')
const date = DateTime.fromISO(album['release_date'])
if (!date.isValid) return null
return {

View file

@ -16,22 +16,24 @@ export default {
return DateTime.now().diff(DateTime.fromJSDate(new Date(date)), 'years').years > 3
},
stringToRFC822Date: (dateString) => {
const addLeadingZero = (num) => {
num = num.toString()
while (num.length < 2) num = '0' + num
return num
}
const date = new Date(Date.parse(dateString))
const dayStrings = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const monthStrings = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const timeStamp = Date.parse(dateString)
const date = new Date(timeStamp)
const day = dayStrings[date.getDay()]
const dayNumber = addLeadingZero(date.getDate())
const dayNumber = String(date.getDate()).padStart(2, '0')
const month = monthStrings[date.getMonth()]
const year = date.getFullYear()
const time = `${addLeadingZero(date.getHours())}:${addLeadingZero(date.getMinutes())}:00`
const time = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:00`
const timezone = date.getTimezoneOffset() === 0 ? 'GMT' : 'PT'
return `${day}, ${dayNumber} ${month} ${year} ${time} ${timezone}`
},
stringToRFC3339: (dateString) => {
const timestamp = Date.parse(dateString);
if (!isNaN(timestamp)) {
const date = new Date(timestamp)
return date.toISOString()
} else {
return '';
}
}
}

View file

@ -1,80 +0,0 @@
import { URL } from 'url'
import markdownIt from 'markdown-it'
import markdownItAnchor from 'markdown-it-anchor'
import markdownItFootnote from 'markdown-it-footnote'
import sanitizeHtml from 'sanitize-html'
import truncate from 'truncate-html'
const BASE_URL = 'https://coryd.dev'
export default {
normalizeEntries: (entries, limit) => {
const posts = []
const mdGenerator = () => {
const md = markdownIt({ html: true, linkify: true })
md.use(markdownItAnchor, {
level: [1, 2],
permalink: markdownItAnchor['permalink']['headerLink']({ safariReaderFix: true })
})
md.use(markdownItFootnote)
md.renderer['rules']['footnote_ref'] = (tokens, idx) => `<sup>${tokens[idx]['meta']['id'] + 1}</sup>`
md.renderer['rules']['footnote_block_open'] = () => '<hr class="footnotes-sep">\n<section class="footnotes">\n<ol class="footnotes-list">\n'
md.renderer['rules']['footnote_open'] = (tokens, idx) => `<li id="fn${tokens[idx]['meta']['id'] + 1}" class="footnote-item"> `
md.renderer['rules']['footnote_anchor'] = () => ''
return md
}
const entryData = limit ? entries.slice(0, limit) : entries
entryData.forEach((entry) => {
const md = mdGenerator()
const dateKey = Object.keys(entry).find(key => key.includes('date'))
const {
artist, author, backdrop, content, description, image, link, rating, review,
slug, title, url, tags, type
} = entry
const processedEntry = {
title: title.trim(),
date: new Date(entry[dateKey]),
content: description || ''
}
const feedNote = '<hr/><p>This is a full text feed, but not all content can be rendered perfectly within the feed. If something looks off, feel free to <a href="https://coryd.dev">visit my site</a> for the original post.</p>'
processedEntry['url'] = (url?.includes('http')) ? url : new URL(slug || url, BASE_URL).toString()
if (link) {
processedEntry['title'] = `${title} via ${author?.['name'] || 'Unknown'}`
processedEntry['url'] = link
processedEntry['author'] = {
name: author?.['name'] || 'Unknown',
url: author?.['url'] || '',
mastodon: author?.['mastodon'] || '',
rss: author?.['rss_feed'] || ''
}
processedEntry['excerpt'] = sanitizeHtml(md.render(description || ''))
} else if (['book', 'movie'].includes(type)) {
processedEntry['excerpt'] = sanitizeHtml(md.render(review || description || ''))
} else if (type === 'album-release') {
let sanitizedDescription = sanitizeHtml(md.render(description || ''))
let truncatedDescription = truncate(sanitizedDescription, { length: 500, reserveLastWord: true, ellipsis: '...' })
if (artist?.['name'] && artist?.['url'] && sanitizedDescription.length > 500) truncatedDescription += ` <p><a href="${artist['url']}">Read more about ${artist['name']}</a></p>`
processedEntry['excerpt'] = truncatedDescription
} else if (slug && content) {
processedEntry['excerpt'] = sanitizeHtml(md.render(content) + feedNote, { disallowedTagsMode: 'completelyDiscard' })
} else if (description) {
processedEntry['excerpt'] = description
}
processedEntry['image'] = backdrop || image
if (rating) processedEntry['rating'] = rating
if (tags) processedEntry['tags'] = tags
if (type === 'album-release' && artist) processedEntry['title'] = `${title} by ${artist['name']}`
posts.push(processedEntry)
})
return posts
}
}

View file

@ -1,12 +1,10 @@
import dates from './dates.js'
import feeds from './feeds.js'
import general from './general.js'
import media from './media.js'
import navigation from './navigation.js'
export default {
...dates,
...feeds,
...general,
...media,
...navigation,

View file

@ -27,7 +27,7 @@ export default {
break
case 'album-release':
normalized.alt = `${item['title']} by ${item['artist']['name']}`
normalized.subtext = `${item['artist']['name']} / ${item['date']}`
normalized.subtext = `${item['artist']['name']} / ${item['release_date_formatted']}`
break
case 'movie':
normalized.alt = item['title']