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
··· 1 1 import { getSeries } from "@fujocoded/ao3.js" 2 + import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 2 3 3 4 export const dynamic = 'force-static' 4 5 5 - export async function GET(_req, ctx) { 6 + export async function GET(req, ctx) { 6 7 const { seriesId } = await ctx.params 8 + const { archive } = await req.nextUrl.searchParams 9 + if (archive) setArchiveBaseUrl(`https://${archive}`) 7 10 const series = await getSeries({seriesId: seriesId}) 11 + if (archive) resetArchiveBaseUrl() 8 12 return Response.json(series) 9 13 }
+5 -1
src/app/api/works/[workId]/chapters/[chapterId]/route.js
··· 1 1 import { getWork } from "@fujocoded/ao3.js" 2 + import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 2 3 3 4 export const dynamic = 'force-static' 4 5 5 - export async function GET(_req, ctx) { 6 + export async function GET(req, ctx) { 6 7 const { workId, chapterId } = await ctx.params 8 + const { archive } = await req.nextUrl.searchParams 9 + if (archive) setArchiveBaseUrl(`https://${archive}`) 7 10 const work = await getWork({workId: workId, chapterId: chapterId}) 11 + if (archive) resetArchiveBaseUrl() 8 12 return Response.json(work) 9 13 }
+5 -1
src/app/api/works/[workId]/route.js
··· 1 1 import { getWork } from "@fujocoded/ao3.js" 2 + import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 2 3 3 4 export const dynamic = 'force-static' 4 5 5 - export async function GET(_req, ctx) { 6 + export async function GET(req, ctx) { 6 7 const { workId } = await ctx.params 8 + const { archive } = await req.nextUrl.searchParams 9 + if (archive) setArchiveBaseUrl(`https://${archive}`) 7 10 const work = await getWork({workId: workId}) 11 + if (archive) resetArchiveBaseUrl() 8 12 return Response.json(work) 9 13 }
+11 -3
src/app/generator/page.js
··· 1 1 "use client" 2 2 3 3 import { useEffect, useState } from "react" 4 + import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 4 5 import themes from "@/lib/themes.js" 5 6 import baseFonts from "@/lib/baseFonts.js" 6 7 import titleFonts from "@/lib/titleFonts.js" ··· 13 14 const [addr, setAddr] = useState('') 14 15 const [imgData, setImgData] = useState(null) 15 16 const [props, setProps] = useState(defaults) 17 + const [domain, setDomain] = useState('') 16 18 17 19 const updateProp = (name, value) => { 18 20 const newProps = props ··· 22 24 } 23 25 24 26 const updateData = async () => { 27 + if (url === '') return 25 28 const workMatch = /\/works\/(?<workId>[0-9]+)(?:\/chapters\/(?<chapterId>[0-9]+))?$/ 26 29 const seriesMatch = /\/series\/(?<seriesId>[0-9]+)$/ 30 + const baseurl = /https:\/\/(?<domain>[a-z0-9\-\.]+)\// 31 + const domainMatch = url.match(baseurl) 32 + if (!domainMatch) return 33 + setDomain(domainMatch.groups.domain) 34 + const domainParam = domain && !(["ao3.org", "archiveofourown.org", "archive.transformativeworks.org"].includes(domain)) ? '' : `?archive=${domain}` 27 35 if (workMatch.test(url)) { 28 36 const match = url.match(workMatch) 29 - const resp = match.groups.chapterId ? await fetch(`/api/works/${match.groups.workId}/chapters/${match.groups.chapterId}`) : await fetch(`/api/works/${match.groups.workId}`) 37 + 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}`) 30 38 const data = await resp.json() 31 39 setAddr(match.groups.chapterId ? `works/${match.groups.workId}/chapters/${match.groups.chapterId}` : `works/${match.groups.workId}`) 32 40 setWorkData(data) 33 41 } else if (seriesMatch.test(url)) { 34 42 const match = url.match(seriesMatch) 35 - const resp = await fetch(`/api/series/${match.groups.seriesId}`) 43 + const resp = await fetch(`/api/series/${match.groups.seriesId}${domainParam}`) 36 44 const data = await resp.json() 37 45 setAddr(`series/${match.groups.seriesId}`) 38 46 setWorkData(data) ··· 47 55 const fn = async () => { 48 56 if (!addr) return; 49 57 const params = new URLSearchParams(props) 50 - const image = await fetch(`/${addr}/preview?${params.toString()}`) 58 + const image = await fetch(`/${addr}/preview?${params.toString()}&archive=${domain}`) 51 59 if (image.status !== 200) return; 52 60 const imageBlob = await image.blob() 53 61 const reader = new FileReader()
+3
src/app/series/[seriesId]/preview/route.js
··· 1 1 import { getSeries } from "@fujocoded/ao3.js" 2 + import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 2 3 import querystring from 'node:querystring' 3 4 import sanitizeData from "@/lib/sanitizeData.js" 4 5 import OGImage from "@/lib/ogimage.js" ··· 17 18 const p = await req.nextUrl.searchParams 18 19 const props = querystring.parse(p.toString()) 19 20 const addr = `series/${seriesId}` 21 + if (props.archive) setArchiveBaseUrl('https://'+props.archive) 20 22 const data = await getSeries({seriesId: seriesId}) 23 + if (props.archive) resetArchiveBaseUrl() 21 24 const imageParams = await sanitizeData({type: 'series', data: data, props: props}) 22 25 const theme = imageParams.theme 23 26 const baseFont = baseFonts[imageParams.baseFont].displayName
+17 -6
src/lib/sanitizeData.js
··· 1 1 import { getWork } from "@fujocoded/ao3.js" 2 + import { setArchiveBaseUrl, resetArchiveBaseUrl } from "@fujocoded/ao3.js/urls" 2 3 import DOM from "fauxdom" 3 4 import { readFile } from 'node:fs/promises' 4 5 import { join } from 'node:path' ··· 6 7 import baseFonts from '@/lib/baseFonts.js' 7 8 import titleFonts from '@/lib/titleFonts.js' 8 9 9 - const getHighestRating = async (works) => { 10 + const getHighestRating = async (works, archive = null) => { 10 11 const ratings = await Promise.all(works.map(async (w) => { 12 + if (archive) setArchiveBaseUrl('https://'+archive) 11 13 const work = await getWork({workId: w.id}) 14 + if (archive) resetArchiveBaseUrl() 12 15 return work.rating 13 16 })) 14 17 if (ratings.includes("Not Rated")) { ··· 23 26 return "G" 24 27 } 25 28 26 - const getHighestWarning = async (works) => { 29 + const getHighestWarning = async (works, archive = null) => { 27 30 const warnings = await Promise.all(works.map(async (w) => { 31 + if (archive) setArchiveBaseUrl('https://'+archive) 28 32 const work = await getWork({workId: w.id}) 33 + if (archive) resetArchiveBaseUrl() 29 34 return work.tags.warnings 30 35 })) 31 36 const warningsUnique = warnings.reduce((a, b) => { return a.concat(b) }).filter((w, i) => { return i === warnings.indexOf(w) }) ··· 37 42 return "W" 38 43 } 39 44 40 - const getCategory = async (works) => { 45 + const getCategory = async (works, archive = null) => { 41 46 const categories = await Promise.all(works.map(async (w) => { 47 + if (archive) setArchiveBaseUrl('https://'+archive) 42 48 const work = await getWork({workId: w.id}) 49 + if (archive) resetArchiveBaseUrl() 43 50 return work.category 44 51 })) 45 52 const categoriesJoined = categories.reduce((a, b) => { return a.concat(b) }) ··· 76 83 77 84 export default async function sanitizeData ({ type, data, props}) { 78 85 const propsParsed = sanitizeProps(props) 86 + const archive = propsParsed.archive 79 87 const baseFont = propsParsed.baseFont ? propsParsed.baseFont : process.env.DEFAULT_BASE_FONT 80 88 const baseFontData = baseFonts[baseFont] 81 89 const titleFont = propsParsed.titleFont ? propsParsed.titleFont : process.env.DEFAULT_TITLE_FONT 82 90 const titleFontData = titleFonts[titleFont] 83 91 const themeData = propsParsed.theme ? themes[propsParsed.theme] : themes[process.env.DEFAULT_THEME] 92 + if (archive) setArchiveBaseUrl('https://'+archive) 84 93 const parentWork = type === 'work' && data.chapterInfo ? await getWork({workId: data.id}) : null 94 + if (archive) resetArchiveBaseUrl() 85 95 const bfs = await Promise.all(baseFontData.defs.map(async (bf) => { 86 96 return { 87 97 name: baseFontData.displayName, ··· 109 119 return a.username 110 120 }) 111 121 : [] 112 - const rating = type === 'work' ? await getHighestRating([data]) : await getHighestRating(data.works) 113 - const warning = type === 'work' ? await getHighestWarning([data]) : await getHighestWarning(data.works) 114 - const category = type === 'work' ? await getCategory([data]) : await getCategory(data.works) 122 + const rating = type === 'work' ? await getHighestRating([data], archive) : await getHighestRating(data.works, archive) 123 + const warning = type === 'work' ? await getHighestWarning([data], archive) : await getHighestWarning(data.works, archive) 124 + const category = type === 'work' ? await getCategory([data], archive) : await getCategory(data.works, archive) 115 125 const authorString = authorsFormatted.length > 1 116 126 ? authorsFormatted.slice(0, -1).join(", ") + " & " + 117 127 authorsFormatted.slice(-1)[0] ··· 126 136 /(<([^>]+)>)/ig, 127 137 "", 128 138 ).split("\n") 139 + console.log(data) 129 140 const titleString = type === 'work' ? data.title : data.name 130 141 const chapterString = data.chapterInfo ? (data.chapterInfo.name 131 142 ? data.chapterInfo.name