redirecter for ao3 that adds opengraph metadata

normalize data better

Changed files
+42 -31
src
app
api
series
[seriesId]
series
[seriesId]
preview
works
lib
+2
src/app/api/series/[seriesId]/route.js
··· 6 6 export async function GET(req, ctx) { 7 7 const { seriesId } = await ctx.params 8 8 const { archive } = await req.nextUrl.searchParams 9 + console.log(seriesId) 10 + console.log(archive) 9 11 const domain = await req.nextUrl.hostname 10 12 const subdomain = domain.split(".").length > 1 ? domain.split(".")[0] : null 11 13 if (subdomain) setArchiveBaseUrl('https://'+subdomain)
+4 -4
src/app/series/[seriesId]/preview/route.js
··· 18 18 const p = await req.nextUrl.searchParams 19 19 const props = querystring.parse(p.toString()) 20 20 const addr = `series/${seriesId}` 21 - if (props.archive) setArchiveBaseUrl('https://'+props.archive) 22 - const data = await getSeries({seriesId: seriesId}) 23 - if (props.archive) resetArchiveBaseUrl() 24 - const imageParams = await sanitizeData({type: 'series', data: data, props: props}) 21 + const domainParam = p && p.has('archive') ? `?archive=${p.get('archive')}` : '' 22 + const work = await fetch(`http://${process.env.DOMAIN}/api/series/${seriesId}${domainParam}`) 23 + const data = await work.json() 24 + const imageParams = await sanitizeData({type: 'series', data: data, props: p}) 25 25 const theme = imageParams.theme 26 26 const baseFont = baseFonts[imageParams.baseFont].displayName 27 27 const titleFont = titleFonts[imageParams.titleFont].displayName
+1 -1
src/app/works/[workId]/opengraph-image.jsx
··· 18 18 const addr = `works/${workId}` 19 19 const data = await getWork({workId: workId}) 20 20 if (data.locked) return OGImageLocked({theme: process.env.DEFAULT_THEME}) 21 - const imageParams = await sanitizeData({type: 'work', data: data, props: defaults}) 21 + const imageParams = await sanitizeData({type: 'work', data: data, props: new URLSearchParams(defaults)}) 22 22 const theme = imageParams.theme 23 23 const baseFont = baseFonts[imageParams.baseFont].displayName 24 24 const titleFont = titleFonts[imageParams.titleFont].displayName
+20 -19
src/lib/ogimage.js
··· 13 13 import NoWarnings from "@/icons/nowarnings.js" 14 14 import Warnings from "@/icons/warnings.js" 15 15 import ChoseNotToWarn from "@/icons/chosenottowarn.js" 16 + import { checkItem, checkToggle } from '@/lib/propUtils.js' 16 17 17 18 export default async function OGImage ({ theme, baseFont, titleFont, image, addr, opts }) { 18 19 console.log(image) ··· 61 62 gap: 10 62 63 }} 63 64 > 64 - {image.props.get('rating') && image.rating === 'E' && (<Explicit fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 65 - {image.props.get('rating') && image.rating === 'M' && (<Mature fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 66 - {image.props.get('rating') && image.rating === 'T' && (<Teen fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 67 - {image.props.get('rating') && image.rating === 'G' && (<General fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 68 - {image.props.get('rating') && image.rating === 'NR' && (<NotRated fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 65 + {checkToggle('rating', image.props) && image.rating === 'E' && (<Explicit fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 66 + {checkToggle('rating', image.props) && image.rating === 'M' && (<Mature fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 67 + {checkToggle('rating', image.props) && image.rating === 'T' && (<Teen fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 68 + {checkToggle('rating', image.props) && image.rating === 'G' && (<General fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 69 + {checkToggle('rating', image.props) && image.rating === 'NR' && (<NotRated fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)} 69 70 70 - {image.props.get('warnings') && image.warning === 'NW' && (<NoWarnings fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)} 71 - {image.props.get('warnings') && image.warning === 'CNTW' && (<ChoseNotToWarn fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)} 72 - {image.props.get('warnings') && image.warning === 'W' && (<Warnings fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)} 71 + {checkToggle('warnings', image.props) && image.warning === 'NW' && (<NoWarnings fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)} 72 + {checkToggle('warnings', image.props) && image.warning === 'CNTW' && (<ChoseNotToWarn fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)} 73 + {checkToggle('warnings', image.props) && image.warning === 'W' && (<Warnings fg={theme.accent2Color} bg={theme.accent2} width={28} height={28} />)} 73 74 74 - {image.props.get('category') && image.category === 'F' && (<Yuri fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 75 - {image.props.get('category') && image.category === 'M' && (<Yaoi fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 76 - {image.props.get('category') && image.category === 'FM' && (<Het fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 77 - {image.props.get('category') && image.category === 'G' && (<Gen fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 78 - {image.props.get('category') && image.category === 'MX' && (<MultiShip fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 79 - {image.props.get('category') && image.category === 'O' && (<OtherShip fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 75 + {checkToggle('category', image.props) && image.category === 'F' && (<Yuri fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 76 + {checkToggle('category', image.props)&& image.category === 'M' && (<Yaoi fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 77 + {checkToggle('category', image.props) && image.category === 'FM' && (<Het fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 78 + {checkToggle('category', image.props) && image.category === 'G' && (<Gen fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 79 + {checkToggle('category', image.props) && image.category === 'MX' && (<MultiShip fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 80 + {checkToggle('category', image.props) && image.category === 'O' && (<OtherShip fg={theme.accent3Color} bg={theme.accent3} width={28} height={28} />)} 80 81 </div> 81 82 <div 82 83 style={{ ··· 127 128 alignItems: "flex-end" 128 129 }} 129 130 > 130 - {image.props.get('charTags') && (<div 131 + {checkToggle('charTags', image.props) && (<div 131 132 style={{ 132 133 display: "flex", 133 134 flexWrap: "wrap", ··· 150 151 </span> 151 152 ))} 152 153 </div>)} 153 - {image.props.get('relTags') && (<div 154 + {checkToggle('relTags', image.props) && (<div 154 155 style={{ 155 156 display: "flex", 156 157 flexWrap: "wrap", ··· 173 174 </span> 174 175 ))} 175 176 </div>)} 176 - {image.props.freeTags && (<div 177 + {checkToggle('freeTags', image.props) && (<div 177 178 style={{ 178 179 display: "flex", 179 180 flexWrap: "wrap", ··· 196 197 </span> 197 198 ))} 198 199 </div>)} 199 - {image.props.get('summary') && (<div 200 + {checkToggle('summary', image.props) && (<div 200 201 style={{ 201 202 display: "flex", 202 203 flexDirection: "column", ··· 225 226 color: theme.accent2 226 227 }} 227 228 > 228 - {image.props.get('wordcount') && `${image.words} words • `}{(image.props.get('chapters') && image.chapterCount !== null) && `${image.chapterCount} chapters • `}{image.props.get('postedAt') && `posted on ${image.postedAt} • `}{image.props.get('updatedAt') && `updated on ${image.updatedAt} • `}{addr} 229 + {checkToggle('wordcount', image.props) && `${image.words} words • `}{(checkToggle('chapters', image.props) && image.chapterCount !== null) && `${image.chapterCount} chapters • `}{checkToggle('postedAt', image.props) && `posted on ${image.postedAt} • `}{checkToggle('updatedAt', image.props) && `updated on ${image.updatedAt} • `}{addr} 229 230 </div> 230 231 </div> 231 232 </div>
+7
src/lib/propUtils.js
··· 1 + export function checkItem (key, props) { 2 + return props.has(key) ? props.get(key) !== '' : false 3 + } 4 + 5 + export function checkToggle (key, props) { 6 + return props.has(key) ? props.get(key) === 'true' : false 7 + }
+8 -7
src/lib/sanitizeData.js
··· 5 5 import themes from '@/lib/themes.js' 6 6 import baseFonts from '@/lib/baseFonts.js' 7 7 import titleFonts from '@/lib/titleFonts.js' 8 + import { checkItem, checkToggle } from '@/lib/propUtils.js' 8 9 9 10 const getWork = async (workId, archive = null) => { 10 11 const domainParam = (archive && archive !== process.env.ARCHIVE) ? `?archive=${archive}` : '' ··· 82 83 } 83 84 84 85 export default async function sanitizeData ({ type, data, props}) { 85 - const archive = props && props.archive ? props.archive : process.env.ARCHIVE 86 + const archive = props && checkItem('archive', props) ? props.get('archive') : process.env.ARCHIVE 86 87 console.log(props) 87 - const baseFont = props.baseFont ? props.baseFont : process.env.DEFAULT_BASE_FONT 88 + const baseFont = checkItem('baseFont', props) ? props.get('baseFont') : process.env.DEFAULT_BASE_FONT 88 89 const baseFontData = baseFonts[baseFont] 89 - const titleFont = props.titleFont ? props.titleFont : process.env.DEFAULT_TITLE_FONT 90 + const titleFont = checkItem('titleFont', props) ? props.get('titleFont') : process.env.DEFAULT_TITLE_FONT 90 91 const titleFontData = titleFonts[titleFont] 91 - const archClean = props.has('archive') ? props.get('archive').replace("https://", '').replace('/', '') : null 92 - const theme = props.has('theme') ? props.get('theme') : (props.has('archive') && !["ao3.org", "archiveofourown.org", "archive.transformativeworks.org"].includes(archClean) && Object.values(siteMap).includes(archClean) ? Object.keys(siteMap)[Object.values(siteMap).indexOf(archClean)] : process.env.DEFAULT_THEME) 92 + const archClean = checkItem('archive', props) ? props.get('archive').replace("https://", '').replace('/', '') : null 93 + const theme = checkItem('theme', props) ? props.get('theme') : (checkItem('archive', props) && !["ao3.org", "archiveofourown.org", "archive.transformativeworks.org"].includes(archClean) && Object.values(siteMap).includes(archClean) ? Object.keys(siteMap)[Object.values(siteMap).indexOf(archClean)] : process.env.DEFAULT_THEME) 93 94 const themeData = themes[theme] 94 95 const parentWork = type === 'work' && data.chapterInfo ? await getWork(data.id, archive) : null 95 96 const bfs = await Promise.all(baseFontData.defs.map(async (bf) => { ··· 127 128 authorsFormatted.slice(-1)[0] 128 129 : authorsFormatted[0]) 129 130 const summaryContent = type === 'work' 130 - ? (props.summaryType === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.summaryType === 'custom' && props.customSummary !== '' ? props.customSummary : (data.summary ? data.summary : (parentWork ? parentWork.summary : '')))) 131 - : (props.summaryType === 'custom' && props.customSummary !== '' ? props.customSummary : data.notes) 131 + ? (props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.get('customSummary') !== '' ? props.get('customSummary') : (data.summary ? data.summary : (parentWork ? parentWork.summary : '')))) 132 + : (props.get('summaryType') === 'custom' && props.get('customSummary') !== '' ? props.get('customSummary') : data.notes) 132 133 const formatter = new Intl.NumberFormat('en-US') 133 134 const words = formatter.format(data.words) 134 135 const summaryDOM = new DOM(summaryContent, {decodeEntities: true})