redirecter for ao3 that adds opengraph metadata

start implementing other archives

Changed files
+46 -12
src
app
api
series
[seriesId]
works
[workId]
chapters
[chapterId]
generator
series
[seriesId]
preview
lib
+5 -1
src/app/api/series/[seriesId]/route.js
···
import { getSeries } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
export const dynamic = 'force-static'
-
export async function GET(_req, ctx) {
+
export async function GET(req, ctx) {
const { seriesId } = await ctx.params
+
const { archive } = await req.nextUrl.searchParams
+
if (archive) setArchiveBaseUrl(`https://${archive}`)
const series = await getSeries({seriesId: seriesId})
+
if (archive) resetArchiveBaseUrl()
return Response.json(series)
}
+5 -1
src/app/api/works/[workId]/chapters/[chapterId]/route.js
···
import { getWork } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
export const dynamic = 'force-static'
-
export async function GET(_req, ctx) {
+
export async function GET(req, ctx) {
const { workId, chapterId } = await ctx.params
+
const { archive } = await req.nextUrl.searchParams
+
if (archive) setArchiveBaseUrl(`https://${archive}`)
const work = await getWork({workId: workId, chapterId: chapterId})
+
if (archive) resetArchiveBaseUrl()
return Response.json(work)
}
+5 -1
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'
-
export async function GET(_req, ctx) {
+
export async function GET(req, ctx) {
const { workId } = await ctx.params
+
const { archive } = await req.nextUrl.searchParams
+
if (archive) setArchiveBaseUrl(`https://${archive}`)
const work = await getWork({workId: workId})
+
if (archive) resetArchiveBaseUrl()
return Response.json(work)
}
+11 -3
src/app/generator/page.js
···
"use client"
import { useEffect, useState } from "react"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
import themes from "@/lib/themes.js"
import baseFonts from "@/lib/baseFonts.js"
import titleFonts from "@/lib/titleFonts.js"
···
const [addr, setAddr] = useState('')
const [imgData, setImgData] = useState(null)
const [props, setProps] = useState(defaults)
+
const [domain, setDomain] = useState('')
const updateProp = (name, value) => {
const newProps = props
···
}
const updateData = async () => {
+
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 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}`
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}`) : await fetch(`/api/works/${match.groups.workId}`)
+
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}`)
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}`)
+
const resp = await fetch(`/api/series/${match.groups.seriesId}${domainParam}`)
const data = await resp.json()
setAddr(`series/${match.groups.seriesId}`)
setWorkData(data)
···
const fn = async () => {
if (!addr) return;
const params = new URLSearchParams(props)
-
const image = await fetch(`/${addr}/preview?${params.toString()}`)
+
const image = await fetch(`/${addr}/preview?${params.toString()}&archive=${domain}`)
if (image.status !== 200) return;
const imageBlob = await image.blob()
const reader = new FileReader()
+3
src/app/series/[seriesId]/preview/route.js
···
import { getSeries } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
import querystring from 'node:querystring'
import sanitizeData from "@/lib/sanitizeData.js"
import OGImage from "@/lib/ogimage.js"
···
const p = await req.nextUrl.searchParams
const props = querystring.parse(p.toString())
const addr = `series/${seriesId}`
+
if (props.archive) setArchiveBaseUrl('https://'+props.archive)
const data = await getSeries({seriesId: seriesId})
+
if (props.archive) resetArchiveBaseUrl()
const imageParams = await sanitizeData({type: 'series', data: data, props: props})
const theme = imageParams.theme
const baseFont = baseFonts[imageParams.baseFont].displayName
+17 -6
src/lib/sanitizeData.js
···
import { getWork } from "@fujocoded/ao3.js"
+
import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls"
import DOM from "fauxdom"
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
···
import baseFonts from '@/lib/baseFonts.js'
import titleFonts from '@/lib/titleFonts.js'
-
const getHighestRating = async (works) => {
+
const getHighestRating = async (works, archive = null) => {
const ratings = await Promise.all(works.map(async (w) => {
+
if (archive) setArchiveBaseUrl('https://'+archive)
const work = await getWork({workId: w.id})
+
if (archive) resetArchiveBaseUrl()
return work.rating
}))
if (ratings.includes("Not Rated")) {
···
return "G"
}
-
const getHighestWarning = async (works) => {
+
const getHighestWarning = async (works, archive = null) => {
const warnings = await Promise.all(works.map(async (w) => {
+
if (archive) setArchiveBaseUrl('https://'+archive)
const work = await getWork({workId: w.id})
+
if (archive) resetArchiveBaseUrl()
return work.tags.warnings
}))
const warningsUnique = warnings.reduce((a, b) => { return a.concat(b) }).filter((w, i) => { return i === warnings.indexOf(w) })
···
return "W"
}
-
const getCategory = async (works) => {
+
const getCategory = async (works, archive = null) => {
const categories = await Promise.all(works.map(async (w) => {
+
if (archive) setArchiveBaseUrl('https://'+archive)
const work = await getWork({workId: w.id})
+
if (archive) resetArchiveBaseUrl()
return work.category
}))
const categoriesJoined = categories.reduce((a, b) => { return a.concat(b) })
···
export default async function sanitizeData ({ type, data, props}) {
const propsParsed = sanitizeProps(props)
+
const archive = propsParsed.archive
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
const titleFontData = titleFonts[titleFont]
const themeData = propsParsed.theme ? themes[propsParsed.theme] : themes[process.env.DEFAULT_THEME]
+
if (archive) setArchiveBaseUrl('https://'+archive)
const parentWork = type === 'work' && data.chapterInfo ? await getWork({workId: data.id}) : null
+
if (archive) resetArchiveBaseUrl()
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
return {
name: baseFontData.displayName,
···
return a.username
})
: []
-
const rating = type === 'work' ? await getHighestRating([data]) : await getHighestRating(data.works)
-
const warning = type === 'work' ? await getHighestWarning([data]) : await getHighestWarning(data.works)
-
const category = type === 'work' ? await getCategory([data]) : await getCategory(data.works)
+
const rating = type === 'work' ? await getHighestRating([data], archive) : await getHighestRating(data.works, archive)
+
const warning = type === 'work' ? await getHighestWarning([data], archive) : await getHighestWarning(data.works, archive)
+
const category = type === 'work' ? await getCategory([data], archive) : await getCategory(data.works, archive)
const authorString = authorsFormatted.length > 1
? authorsFormatted.slice(0, -1).join(", ") + " & " +
authorsFormatted.slice(-1)[0]
···
/(<([^>]+)>)/ig,
"",
).split("\n")
+
console.log(data)
const titleString = type === 'work' ? data.title : data.name
const chapterString = data.chapterInfo ? (data.chapterInfo.name
? data.chapterInfo.name