redirecter for ao3 that adds opengraph metadata

AHAHA

Changed files
+87 -60
src
app
api
series
[seriesId]
works
[workId]
chapters
[chapterId]
generator
locked
series
[seriesId]
preview
works
[workId]
chapters
[chapterId]
preview
preview
lib
+1 -1
src/app/api/series/[seriesId]/route.js
···
export async function GET(req, ctx) {
const { seriesId } = await ctx.params
const { archive } = await req.nextUrl.searchParams
-
if (archive) setArchiveBaseUrl(`https://${archive}`)
+
if (archive) setArchiveBaseUrl(archive)
const series = await getSeries({seriesId: seriesId})
if (archive) resetArchiveBaseUrl()
return Response.json(series)
+3 -2
src/app/api/works/[workId]/chapters/[chapterId]/route.js
···
export async function GET(req, ctx) {
const { workId, chapterId } = await ctx.params
-
const { archive } = await req.nextUrl.searchParams
-
if (archive) setArchiveBaseUrl(`https://${archive}`)
+
const params = await req.nextUrl.searchParams
+
const archive = params.get('archive')
+
if (archive) setArchiveBaseUrl(archive)
const work = await getWork({workId: workId, chapterId: chapterId})
if (archive) resetArchiveBaseUrl()
return Response.json(work)
+4 -5
src/app/api/works/[workId]/route.js
···
import { getWork } from "@fujocoded/ao3.js"
-
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
-
-
export const dynamic = 'force-static'
+
import { setArchiveBaseUrl, getArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
export async function GET(req, ctx) {
const { workId } = await ctx.params
-
const { archive } = await req.nextUrl.searchParams
-
if (archive) setArchiveBaseUrl(`https://${archive}`)
+
const params = await req.nextUrl.searchParams
+
const archive = params.get('archive')
+
if (archive) setArchiveBaseUrl(archive)
const work = await getWork({workId: workId})
if (archive) resetArchiveBaseUrl()
return Response.json(work)
+16 -2
src/app/generator/page.js
···
if (url === '') return
const workMatch = /\/works\/(?<workId>[0-9]+)(?:\/chapters\/(?<chapterId>[0-9]+))?$/
const seriesMatch = /\/series\/(?<seriesId>[0-9]+)$/
-
const baseurl = /https:\/\/(?<domain>[a-z0-9\-\.]+)\//
+
const baseurl = /(?<domain>https:\/\/[a-z0-9\-\.]+)\//
const domainMatch = url.match(baseurl)
if (!domainMatch) return
setDomain(domainMatch.groups.domain)
-
const domainParam = domain && !(["ao3.org", "archiveofourown.org", "archive.transformativeworks.org"].includes(domain)) ? '' : `?archive=${domain}`
+
const domainParam = domain && !(["ao3.org", "archiveofourown.org", "archive.transformativeworks.org"].includes(domain)) ? `?archive=${domain}` : ''
if (workMatch.test(url)) {
const match = url.match(workMatch)
const resp = match.groups.chapterId ? await fetch(`/api/works/${match.groups.workId}/chapters/${match.groups.chapterId}${domainParam}`) : await fetch(`/api/works/${match.groups.workId}${domainParam}`)
+
if (!resp) return
const data = await resp.json()
setAddr(match.groups.chapterId ? `works/${match.groups.workId}/chapters/${match.groups.chapterId}` : `works/${match.groups.workId}`)
setWorkData(data)
} else if (seriesMatch.test(url)) {
const match = url.match(seriesMatch)
const resp = await fetch(`/api/series/${match.groups.seriesId}${domainParam}`)
+
if (!resp) return
const data = await resp.json()
setAddr(`series/${match.groups.seriesId}`)
setWorkData(data)
···
useEffect(() => {
const fn = async () => {
if (!addr) return;
+
if (workData.locked) {
+
const image = await fetch('/locked')
+
if (image.status !== 200) return;
+
const imageBlob = await image.blob()
+
const reader = new FileReader()
+
reader.onloadend = () => {
+
console.log(reader.result)
+
setImgData(reader.result)
+
}
+
return
+
}
const params = new URLSearchParams(props)
const image = await fetch(`/${addr}/preview?${params.toString()}&archive=${domain}`)
if (image.status !== 200) return;
···
const reader = new FileReader()
reader.onloadend = () => {
setImgData(reader.result)
+
console.log(imgData)
}
reader.readAsDataURL(imageBlob)
}
+13
src/app/locked/route.js
···
+
import OGImageLocked from "@/lib/ogimagelocked.js"
+
+
export const size = {
+
width: 1600,
+
height: 900,
+
}
+
+
export const contentType = 'image/png'
+
+
export async function GET(req, _ctx) {
+
const props = await req.nextUrl.searchParams
+
return OGImageLocked({theme: props.has('theme') ? props.get('theme') : process.env.DEFAULT_THEME})
+
}
+1 -1
src/app/series/[seriesId]/preview/route.js
···
height: 900,
}
-
export const contentType = 'image/webp'
+
export const contentType = 'image/png'
export async function GET(req, ctx) {
const { seriesId } = await ctx.params
+6 -6
src/app/works/[workId]/chapters/[chapterId]/preview/route.js
···
-
import { getWork } from "@fujocoded/ao3.js"
import querystring from 'node:querystring'
import sanitizeData from "@/lib/sanitizeData.js"
import OGImage from "@/lib/ogimage.js"
···
height: 900,
}
-
export const contentType = 'image/webp'
+
export const contentType = 'image/png'
export async function GET(req, ctx) {
const { workId, chapterId } = await ctx.params
-
const p = await req.nextUrl.searchParams
-
const props = querystring.parse(p.toString())
-
const addr = `works/${workId}/chapters/${chapterId}`
-
const data = await getWork({workId: workId, chapterId: chapterId})
+
const props = await req.nextUrl.searchParams
+
const work = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
+
const data = await data.json()
+
const addr = `${props.has('archive') ? props.get('archive').replace("https://", "")+"/" : 'archiveofourown.org/'}works/${workId}/chapters/${chapterId}`
+
const domainParam = props.has('archive') ? `?archive=${props.get('archive')}` : ''
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
const theme = imageParams.theme
const baseFont = baseFonts[imageParams.baseFont].displayName
+6 -13
src/app/works/[workId]/preview/route.js
···
-
import { getWork } from "@fujocoded/ao3.js"
-
import { cache } from "react"
-
import querystring from 'node:querystring'
import sanitizeData from "@/lib/sanitizeData.js"
import OGImage from "@/lib/ogimage.js"
import baseFonts from "@/lib/baseFonts.js"
···
height: 900,
}
-
export const contentType = 'image/webp'
-
-
const getItem = cache(async (workId) => {
-
const work = await getWork({workId: workId})
-
return work
-
})
+
export const contentType = 'image/png'
export async function GET(req, ctx) {
const { workId } = await ctx.params
-
const p = await req.nextUrl.searchParams
-
const props = querystring.parse(p.toString())
-
const addr = `works/${workId}`
-
const data = await getItem(workId)
+
const props = await req.nextUrl.searchParams
+
const addr = `${props.has('archive') ? props.get('archive').replace("https://", "")+"/" : 'archiveofourown.org/'}works/${workId}`
+
const domainParam = props.has('archive') ? `?archive=${props.get('archive')}` : ''
+
const work = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
+
const data = await work.json()
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
const theme = imageParams.theme
const baseFont = baseFonts[imageParams.baseFont].displayName
+19 -14
src/lib/ogimage.js
···
}}
>
<div
+
style={{
+
textTransform: "uppercase",
+
display: "flex",
+
justifyContent: "center",
+
color: theme.accent,
+
alignItems: "center",
+
display: "flex",
+
justifyContent: "center",
+
alignItems: "center",
+
textAlign: "center"
+
}}
+
>
+
{image.topLine}
+
</div>
+
<div
style={{
-
textTransform: "uppercase",
display: "flex",
justifyContent: "center",
-
gap: 10,
-
color: theme.accent,
-
alignItems: "center"
-
}}
+
alignItems: "center",
+
gap: 10
+
}}
>
-
<div
-
style={{
-
display: "flex"
-
}}
-
>
-
{image.topLine}
-
</div>
-
{image.props.rating && image.rating === 'E' && (<Explicit fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
{image.props.rating && image.rating === 'M' && (<Mature fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
{image.props.rating && image.rating === 'T' && (<Teen fg={theme.accentColor} bg={theme.accent} width={28} height={28} />)}
···
color: theme.accent2
}}
>
-
{image.props.wordcount && `${image.words} words • `}{(image.props.chapters && image.chapterCount !== null) && `${image.chapterCount} chapters • `}{image.props.postedAt && `posted on ${image.postedAt} • `}{image.props.updatedAt && `updated on ${image.updatedAt} • `}{process.env.ARCHIVE}/{addr}
+
{image.props.wordcount && `${image.words} words • `}{(image.props.chapters && image.chapterCount !== null) && `${image.chapterCount} chapters • `}{image.props.postedAt && `posted on ${image.postedAt} • `}{image.props.updatedAt && `updated on ${image.updatedAt} • `}{addr}
</div>
</div>
</div>
+7 -7
src/lib/ogimagelocked.js
···
import { ImageResponse } from "next/og"
-
import General from "@/icons/locked.js"
+
import themes from "@/lib/themes.js"
+
import Lock from "@/icons/lock.js"
export default async function OGImageLocked ({ theme }) {
+
const themeData = themes[theme]
return new ImageResponse(
(
<div
···
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
-
color: theme.color,
-
backgroundColor: theme.background,
-
fontFamily: baseFont,
+
color: themeData.color,
+
backgroundColor: themeData.background,
fontSize: 24,
padding: 20,
width: "100%",
height: "100%",
}}
>
-
<Locked bg={theme.background} fg={theme.color} width={480} height={480} />
+
<Lock bg={themeData.background} fg={themeData.color} width={480} height={480} />
</div>
-
),
-
opts
+
)
)
}
+11 -9
src/lib/sanitizeData.js
···
import DOM from "fauxdom"
import { readFile } from 'node:fs/promises'
+
import querystring from 'node:querystring'
import { join } from 'node:path'
import themes from '@/lib/themes.js'
import baseFonts from '@/lib/baseFonts.js'
import titleFonts from '@/lib/titleFonts.js'
-
const getWork = async (workId) => {
-
const x = await fetch('https://veryroundbird.house')
-
const data = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}`)
+
const getWork = async (workId, archive = null) => {
+
const domainParam = archive ? `?archive=${archive}` : ''
+
const data = await fetch(`http://${process.env.DOMAIN}/api/works/${workId}${domainParam}`)
const work = await data.json()
return work
}
-
const getHighestRating = async (works) => {
+
const getHighestRating = async (works, archive = null) => {
+
console.log('get rating')
const ratings = await Promise.all(works.map(async (w) => {
-
const work = await getWork(w.id)
+
const work = await getWork(w.id, archive)
return work.rating
}))
if (ratings.includes("Not Rated")) {
···
}
const getHighestWarning = async (works, archive = null) => {
+
console.log('get warning')
const warnings = await Promise.all(works.map(async (w) => {
const work = await getWork(w.id)
return work.tags.warnings
}))
-
console.log(warnings)
const warningsUnique = warnings.reduce((a, b) => { return a.concat(b) }).filter((w, i) => { return i === warnings.indexOf(w) })
if (warningsUnique.length === 1 && warningsUnique[0] === "Creator Chose Not To Use Archive Warnings") {
return "CNTW"
···
}
const getCategory = async (works, archive = null) => {
+
console.log('get category')
const categories = await Promise.all(works.map(async (w) => {
const work = await getWork(w.id)
return work.category
···
}
export default async function sanitizeData ({ type, data, props}) {
-
const propsParsed = sanitizeProps(props)
-
const archive = propsParsed.archive
+
const propsParsed = sanitizeProps(querystring.parse(props.toString()))
+
const archive = props.has('archive') ? props.get('archive') : 'https://archiveofourown.org'
const baseFont = propsParsed.baseFont ? propsParsed.baseFont : process.env.DEFAULT_BASE_FONT
const baseFontData = baseFonts[baseFont]
const titleFont = propsParsed.titleFont ? propsParsed.titleFont : process.env.DEFAULT_TITLE_FONT
···
/(<([^>]+)>)/ig,
"",
).split("\n")
-
console.log(data)
const titleString = type === 'work' ? data.title : data.name
const chapterString = data.chapterInfo ? (data.chapterInfo.name
? data.chapterInfo.name