+4
-1
.env.example
+4
-1
.env.example
+12
-12
src/app/generator/page.js
+12
-12
src/app/generator/page.js
···
114
114
<div className="input-field">
115
115
<label htmlFor="features">Features:</label>
116
116
<ul>
117
-
<li><label><input type="checkbox" name="features[]" value="category" onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li>
118
-
<li><label><input type="checkbox" name="features[]" value="rating" onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li>
119
-
<li><label><input type="checkbox" name="features[]" value="warnings" onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li>
120
-
<li><label><input type="checkbox" name="features[]" value="chartags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li>
121
-
<li><label><input type="checkbox" name="features[]" value="reltags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li>
122
-
<li><label><input type="checkbox" name="features[]" value="freetags" onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li>
123
-
<li><label><input type="checkbox" name="features[]" value="summary" onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li>
124
-
<li><label><input type="checkbox" name="features[]" value="wordcount" onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li>
125
-
<li><label><input type="checkbox" name="features[]" value="chapters" onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li>
117
+
<li><label><input type="checkbox" name="features[]" value="category" defaultChecked={props.category} onChange={e => updateProp(e.target.value, e.target.checked)} /> Category</label></li>
118
+
<li><label><input type="checkbox" name="features[]" value="rating" defaultChecked={props.rating} onChange={e => updateProp(e.target.value, e.target.checked)} /> Rating</label></li>
119
+
<li><label><input type="checkbox" name="features[]" value="warnings" defaultChecked={props.warnings} onChange={e => updateProp(e.target.value, e.target.checked)} /> Archive Warnings</label></li>
120
+
<li><label><input type="checkbox" name="features[]" value="charTags" defaultChecked={props.charTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Character Tags</label></li>
121
+
<li><label><input type="checkbox" name="features[]" value="relTags" defaultChecked={props.relTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Relationship Tags</label></li>
122
+
<li><label><input type="checkbox" name="features[]" value="freetags" defaultChecked={props.freeTags} onChange={e => updateProp(e.target.value, e.target.checked)} /> Free Tags</label></li>
123
+
<li><label><input type="checkbox" name="features[]" value="summary" defaultChecked={props.summary} onChange={e => updateProp(e.target.value, e.target.checked)} /> Summary</label></li>
124
+
<li><label><input type="checkbox" name="features[]" value="wordcount" defaultChecked={props.wordcount} onChange={e => updateProp(e.target.value, e.target.checked)} /> Wordcount</label></li>
125
+
<li><label><input type="checkbox" name="features[]" value="chapters" defaultChecked={props.chapters} onChange={e => updateProp(e.target.value, e.target.checked)} /> Chapters</label></li>
126
126
</ul>
127
127
</div>
128
128
<div className="input-field">
129
129
<label htmlFor="summaryOptions">Summary Type</label>
130
130
<ul>
131
-
<li><label><input type="radio" name="summaryType" value="basic" onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li>
132
-
<li><label><input type="radio" name="summaryType" value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li>
133
-
<li><label><input type="radio" name="summaryType" value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li>
131
+
<li><label><input type="radio" name="summaryType" value="basic" defaultChecked={props.summaryType === 'basic'} onChange={e => updateProp(e.target.name, e.target.value)} /> Story Summary</label></li>
132
+
<li><label><input type="radio" name="summaryType" defaultChecked={props.summaryType === 'chapter'} value="chapter" onChange={e => updateProp(e.target.name, e.target.value)} /> Chapter Summary (if available)</label></li>
133
+
<li><label><input type="radio" name="summaryType" defaultChecked={props.summaryType === 'custom'} value="custom" onChange={e => updateProp(e.target.name, e.target.value)} /> Custom Summary</label></li>
134
134
</ul>
135
135
</div>
136
136
{props.summaryType === 'custom' && (
+25
src/app/series/[seriesId]/preview/route.js
+25
src/app/series/[seriesId]/preview/route.js
···
1
+
import { getSeries } from "@fujocoded/ao3.js"
2
+
import sanitizeData from "@/lib/sanitizeData.js"
3
+
import OGImage from "@/lib/ogimage.js"
4
+
import baseFonts from "@/lib/baseFonts.js"
5
+
import titleFonts from "@/lib/titleFonts.js"
6
+
7
+
export const size = {
8
+
width: 1600,
9
+
height: 900,
10
+
}
11
+
12
+
export const contentType = 'image/webp'
13
+
14
+
export async function GET(req, ctx) {
15
+
const { seriesId } = await ctx.params
16
+
const props = await req.nextUrl.searchParams
17
+
const addr = `series/${seriesId}`
18
+
const data = await getSeries({seriesId: seriesId})
19
+
const imageParams = await sanitizeData({type: 'series', data: data, props: props})
20
+
const theme = imageParams.theme
21
+
const baseFont = baseFonts[imageParams.baseFont].displayName
22
+
const titleFont = titleFonts[imageParams.titleFont].displayName
23
+
const opts = imageParams.opts
24
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
25
+
}
+25
src/app/works/[workId]/chapters/[chapterId]/preview/route.js
+25
src/app/works/[workId]/chapters/[chapterId]/preview/route.js
···
1
+
import { getWork } from "@fujocoded/ao3.js"
2
+
import sanitizeData from "@/lib/sanitizeData.js"
3
+
import OGImage from "@/lib/ogimage.js"
4
+
import baseFonts from "@/lib/baseFonts.js"
5
+
import titleFonts from "@/lib/titleFonts.js"
6
+
7
+
export const size = {
8
+
width: 1600,
9
+
height: 900,
10
+
}
11
+
12
+
export const contentType = 'image/webp'
13
+
14
+
export async function GET(req, ctx) {
15
+
const { workId, chapterId } = await ctx.params
16
+
const props = await req.nextUrl.searchParams
17
+
const addr = `works/${workId}/chapters/${chapterId}`
18
+
const data = await getWork({workId: workId, chapterId: chapterId})
19
+
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
20
+
const theme = imageParams.theme
21
+
const baseFont = baseFonts[imageParams.baseFont].displayName
22
+
const titleFont = titleFonts[imageParams.titleFont].displayName
23
+
const opts = imageParams.opts
24
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
25
+
}
+10
-170
src/app/works/[workId]/preview/route.js
+10
-170
src/app/works/[workId]/preview/route.js
···
1
1
import { getWork } from "@fujocoded/ao3.js"
2
-
import DOM from "fauxdom"
3
-
import { ImageResponse } from "next/og"
4
-
import { readFile } from 'node:fs/promises'
5
-
import { join } from 'node:path'
6
-
import themes from '../../../themes.js'
7
-
import baseFonts from '../../../baseFonts.js'
8
-
import titleFonts from '../../../titleFonts.js'
2
+
import sanitizeData from "@/lib/sanitizeData.js"
3
+
import OGImage from "@/lib/ogimage.js"
4
+
import baseFonts from "@/lib/baseFonts.js"
5
+
import titleFonts from "@/lib/titleFonts.js"
9
6
10
7
export const size = {
11
8
width: 1600,
···
19
16
const props = await req.nextUrl.searchParams
20
17
const addr = `works/${workId}`
21
18
const data = await getWork({workId: workId})
22
-
const baseFontData = baseFonts[props.has('baseFont') ? props.get('baseFont') : 'bricolagegrotesque']
23
-
const titleFontData = titleFonts[props.has('titleFont') ? props.get('titleFont') : 'stacksansnotch']
24
-
const themeData = props.has('theme') ? themes[props.get('theme')] : themes['ao3']
25
-
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
26
-
return {
27
-
name: baseFontData.displayName,
28
-
data: await readFile(
29
-
join(process.cwd(), bf.path)
30
-
),
31
-
style: bf.style,
32
-
weight: bf.weight
33
-
}
34
-
})).then(x => x)
35
-
const tfs = await Promise.all(titleFontData.defs.map(async (tf) => {
36
-
return {
37
-
name: titleFontData.displayName,
38
-
data: await readFile(
39
-
join(process.cwd(), tf.path)
40
-
),
41
-
style: tf.style,
42
-
weight: tf.weight
43
-
}
44
-
})).then(x => x)
45
-
const authorsFormatted = data.authors
46
-
? data.authors.map((a) => {
47
-
if (a.anonymous) return "Anonymous"
48
-
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
49
-
return a.username
50
-
})
51
-
: []
52
-
const authorString = authorsFormatted.length > 1
53
-
? authorsFormatted.slice(0, -1).join(", ") + " & " +
54
-
authorsFormatted.slice(-1)[0]
55
-
: authorsFormatted[0]
56
-
const summaryDOM = new DOM(props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : data.summary), {decodeEntities: true});
57
-
const summaryFormatted = summaryDOM.innerHTML.replace("<br />", "\n").replace(
58
-
/(<([^>]+)>)/ig,
59
-
"",
60
-
).split("\n")
61
-
const titleString = `<b>${data.title}</b> by ${authorString}`
62
-
const chapterString = data.chapterInfo ? (data.chapterInfo.name
63
-
? data.chapterInfo.name
64
-
: "Chapter " + data.chapterInfo.index) : ''
65
-
const chapterCountString = data.chapters
66
-
? ' | <b>Chapters:</b> '+data.chapters.published+' / '+(
67
-
data.chapters.total
68
-
? data.chapters.total
69
-
: '?'
70
-
)
71
-
: ''
72
-
const fandomString = (data.fandoms.length > 1 ? (data.fandoms.length <= 5 ? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1) : data.fandoms.join(", ")+" (+"+(data.fandoms.length - 4)+")") : data.fandoms[0]).toUpperCase()
73
-
const headingString = `<span size='16pt'>${fandomString}</span>\n${titleString}${chapterString !== '' ? "\n<span size='36pt'><i>"+chapterString+"</i></span></span>" : ''}`
74
-
const opts = {
75
-
fonts: bfs.concat(tfs)
76
-
}
77
-
console.log(themeData)
78
-
console.log(baseFontData)
79
-
console.log(titleFontData)
80
-
return new ImageResponse(
81
-
(
82
-
<div
83
-
style={{
84
-
display: "flex",
85
-
flexDirection: "column",
86
-
color: themeData.color,
87
-
backgroundColor: themeData.background,
88
-
fontFamily: baseFontData.displayName,
89
-
fontSize: 24,
90
-
padding: 40,
91
-
width: "100%",
92
-
height: "100%",
93
-
}}
94
-
>
95
-
<div
96
-
style={{
97
-
display: "flex",
98
-
flexDirection: "column",
99
-
marginBottom: 20
100
-
}}
101
-
>
102
-
<div
103
-
style={{
104
-
textTransform: "uppercase",
105
-
display: "flex",
106
-
justifyContent: "center",
107
-
color: themeData.accent
108
-
}}
109
-
>
110
-
{fandomString}
111
-
</div>
112
-
<div
113
-
style={{
114
-
fontSize: 54,
115
-
justifyContent: "center",
116
-
fontFamily: titleFontData.displayName,
117
-
fontWeight: "bold"
118
-
}}
119
-
>
120
-
{data.title}
121
-
</div>
122
-
<div
123
-
style={{
124
-
fontSize: 42,
125
-
justifyContent: "center",
126
-
fontFamily: titleFontData.displayName
127
-
}}
128
-
>
129
-
{`by ${authorString}`}
130
-
</div>
131
-
<div
132
-
style={{
133
-
fontStyle: "italic",
134
-
fontSize: 36,
135
-
fontFamily: titleFontData.displayName
136
-
}}
137
-
>
138
-
{chapterString}
139
-
</div>
140
-
</div>
141
-
<div
142
-
style={{
143
-
backgroundColor: themeData.descBackground,
144
-
padding: 20,
145
-
display: "flex",
146
-
flexDirection: "column",
147
-
flexGrow: 1,
148
-
color: themeData.descColor,
149
-
alignItems: "flex-end"
150
-
}}
151
-
>
152
-
<div
153
-
style={{
154
-
display: "flex",
155
-
flexDirection: "column",
156
-
flexGrow: 1,
157
-
width: '100%'
158
-
}}
159
-
>
160
-
{summaryFormatted.map(l => (
161
-
<div
162
-
style={{
163
-
width: "100%",
164
-
marginBottom: 10
165
-
}}
166
-
>
167
-
{l}
168
-
</div>
169
-
))}
170
-
</div>
171
-
<div
172
-
style={{
173
-
textAlign: "right",
174
-
fontSize: 18,
175
-
color: themeData.accent2
176
-
}}
177
-
>
178
-
{`https://archiveofourown.org/${addr}`}
179
-
</div>
180
-
</div>
181
-
</div>
182
-
),
183
-
opts
184
-
)
19
+
const imageParams = await sanitizeData({type: 'work', data: data, props: props})
20
+
const theme = imageParams.theme
21
+
const baseFont = baseFonts[imageParams.baseFont].displayName
22
+
const titleFont = titleFonts[imageParams.titleFont].displayName
23
+
const opts = imageParams.opts
24
+
return OGImage({theme: theme, baseFont: baseFont, titleFont: titleFont, image: imageParams, addr: addr, opts: opts})
185
25
}
+498
src/lib/baseFonts.js
+498
src/lib/baseFonts.js
···
1
+
const baseFonts = {
2
+
opensans: {
3
+
displayName: 'Open Sans',
4
+
defs: [
5
+
{
6
+
path: '/fonts/OpenSans-Regular.ttf',
7
+
style: 'normal',
8
+
weight: 400
9
+
},
10
+
{
11
+
path: '/fonts/OpenSans-Italic.ttf',
12
+
style: 'italic',
13
+
weight: 400
14
+
},
15
+
{
16
+
path: '/fonts/OpenSans-Bold.ttf',
17
+
style: 'normal',
18
+
weight: 700
19
+
},
20
+
{
21
+
path: '/fonts/OpenSans-BoldItalic.ttf',
22
+
style: 'italic',
23
+
weight: 700
24
+
}
25
+
]
26
+
},
27
+
bricolagegrotesque: {
28
+
displayName: 'Bricolage Grotesque',
29
+
defs: [
30
+
{
31
+
path: '/fonts/BricolageGrotesque-Regular.ttf',
32
+
style: 'normal',
33
+
weight: 400
34
+
},
35
+
{
36
+
path: '/fonts/BricolageGrotesque-Bold.ttf',
37
+
style: 'normal',
38
+
weight: 700
39
+
}
40
+
]
41
+
},
42
+
spacemono: {
43
+
displayName: 'Space Mono',
44
+
defs: [
45
+
{
46
+
path: '/fonts/SpaceMono-Regular.ttf',
47
+
style: 'normal',
48
+
weight: 400
49
+
},
50
+
{
51
+
path: '/fonts/SpaceMono-Italic.ttf',
52
+
style: 'italic',
53
+
weight: 400
54
+
},
55
+
{
56
+
path: '/fonts/SpaceMono-Bold.ttf',
57
+
style: 'normal',
58
+
weight: 700
59
+
},
60
+
{
61
+
path: '/fonts/SpaceMono-BoldItalic.ttf',
62
+
style: 'italic',
63
+
weight: 700
64
+
}
65
+
]
66
+
},
67
+
inconsolata: {
68
+
displayName: 'Inconsolata',
69
+
defs: [
70
+
{
71
+
path: '/fonts/Inconsolata.otf',
72
+
style: 'normal'
73
+
}
74
+
]
75
+
},
76
+
bitter: {
77
+
displayName: 'Bitter',
78
+
defs: [
79
+
{
80
+
path: '/fonts/Bitter-Regular.otf',
81
+
style: 'normal',
82
+
weight: 400
83
+
},
84
+
{
85
+
path: '/fonts/Bitter-Italic.otf',
86
+
style: 'italic',
87
+
weight: 400
88
+
},
89
+
{
90
+
path: '/fonts/Bitter-Bold.otf',
91
+
style: 'normal',
92
+
weight: 700
93
+
},
94
+
{
95
+
path: '/fonts/Bitter-BoldItalic.otf',
96
+
style: 'italic',
97
+
weight: 700
98
+
}
99
+
]
100
+
},
101
+
archivo: {
102
+
displayName: 'Archivo',
103
+
defs: [
104
+
{
105
+
path: '/fonts/Archivo-Regular.ttf',
106
+
style: 'normal',
107
+
weight: 400
108
+
},
109
+
{
110
+
path: '/fonts/Archivo-Italic.ttf',
111
+
style: 'italic',
112
+
weight: 400
113
+
},
114
+
{
115
+
path: '/fonts/Archivo-Bold.ttf',
116
+
style: 'normal',
117
+
weight: 700
118
+
},
119
+
{
120
+
path: '/fonts/Archivo-BoldItalic.ttf',
121
+
style: 'italic',
122
+
weight: 700
123
+
}
124
+
]
125
+
},
126
+
outfit: {
127
+
displayName: 'Outfit',
128
+
defs: [
129
+
{
130
+
path: '/fonts/outfit-regular-webfont.woff2',
131
+
style: 'normal',
132
+
weight: 400
133
+
},
134
+
{
135
+
path: '/fonts/outfit-italic-webfont.woff2',
136
+
style: 'italic',
137
+
weight: 400
138
+
}
139
+
]
140
+
},
141
+
notosans: {
142
+
displayName: 'Noto Sans',
143
+
defs: [
144
+
{
145
+
path: '/fonts/NotoSans-Regular.ttf',
146
+
style: 'normal',
147
+
weight: 400
148
+
},
149
+
{
150
+
path: '/fonts/NotoSans-Italic.ttf',
151
+
style: 'italic',
152
+
weight: 400
153
+
},
154
+
{
155
+
path: '/fonts/NotoSans-Bold.ttf',
156
+
style: 'normal',
157
+
weight: 700
158
+
},
159
+
{
160
+
path: '/fonts/NotoSans-BoldItalic.ttf',
161
+
style: 'italic',
162
+
weight: 700
163
+
}
164
+
]
165
+
},
166
+
alegreya: {
167
+
displayName: 'Alegreya',
168
+
defs: [
169
+
{
170
+
path: '/fonts/Alegreya-Regular.otf',
171
+
style: 'normal',
172
+
weight: 400
173
+
},
174
+
{
175
+
path: '/fonts/Alegreya-Italic.otf',
176
+
style: 'italic',
177
+
weight: 400
178
+
},
179
+
{
180
+
path: '/fonts/Alegreya-Bold.otf',
181
+
style: 'normal',
182
+
weight: 700
183
+
},
184
+
{
185
+
path: '/fonts/Alegreya-BoldItalic.otf',
186
+
style: 'italic',
187
+
weight: 700
188
+
}
189
+
]
190
+
},
191
+
alegreyasans: {
192
+
displayName: 'Alegreya Sans',
193
+
defs: [
194
+
{
195
+
path: '/fonts/AlegreyaSans-Regular.otf',
196
+
style: 'normal',
197
+
weight: 400
198
+
},
199
+
{
200
+
path: '/fonts/AlegreyaSans-Italic.otf',
201
+
style: 'italic',
202
+
weight: 400
203
+
},
204
+
{
205
+
path: '/fonts/AlegreyaSans-Bold.otf',
206
+
style: 'normal',
207
+
weight: 700
208
+
},
209
+
{
210
+
path: '/fonts/AlegreyaSans-BoldItalic.otf',
211
+
style: 'italic',
212
+
weight: 700
213
+
}
214
+
]
215
+
},
216
+
stacksanstext: {
217
+
displayName: 'Stack Sans Text',
218
+
defs: [
219
+
{
220
+
path: '/fonts/StackSansText-Regular.ttf',
221
+
style: 'normal',
222
+
weight: 400
223
+
},
224
+
{
225
+
path: '/fonts/StackSansText-Bold.ttf',
226
+
style: 'normal',
227
+
weight: 700
228
+
}
229
+
],
230
+
},
231
+
momotrustsans: {
232
+
displayName: 'Momo Trust Sans',
233
+
defs: [
234
+
{
235
+
path: '/fonts/MomoTrustSans-Regular.ttf',
236
+
style: 'normal',
237
+
weight: 400
238
+
},
239
+
{
240
+
path: '/fonts/MomoTrustSans-Bold.ttf',
241
+
style: 'normal',
242
+
weight: 700
243
+
}
244
+
]
245
+
},
246
+
montserrat: {
247
+
displayName: 'Montserrat',
248
+
defs: [
249
+
{
250
+
path: '/fonts/Montserrat-Regular.otf',
251
+
style: 'normal',
252
+
weight: 400
253
+
},
254
+
{
255
+
path: '/fonts/Montserrat-Italic.otf',
256
+
style: 'italic',
257
+
weight: 400
258
+
},
259
+
{
260
+
path: '/fonts/Montserrat-Bold.otf',
261
+
style: 'normal',
262
+
weight: 700
263
+
},
264
+
{
265
+
path: '/fonts/Montserrat-BoldItalic.otf',
266
+
style: 'italic',
267
+
weight: 700
268
+
}
269
+
]
270
+
},
271
+
robotoslab: {
272
+
displayName: 'Roboto Slab',
273
+
defs: [
274
+
{
275
+
path: '/fonts/RobotoSlab-Regular.ttf',
276
+
style: 'normal',
277
+
weight: 400
278
+
},
279
+
{
280
+
path: '/fonts/RobotoSlab-Bold.ttf',
281
+
style: 'normal',
282
+
weight: 700
283
+
}
284
+
]
285
+
},
286
+
quicksand: {
287
+
displayName: 'Quicksand',
288
+
defs: [
289
+
{
290
+
path: '/fonts/Quicksand-Regular.otf',
291
+
style: 'normal',
292
+
weight: 400
293
+
},
294
+
{
295
+
path: '/fonts/Quicksand-Italic.otf',
296
+
style: 'italic',
297
+
weight: 400
298
+
},
299
+
{
300
+
path: '/fonts/Quicksand-Bold.otf',
301
+
style: 'normal',
302
+
weight: 700
303
+
},
304
+
{
305
+
path: '/fonts/Quicksand-BoldItalic.otf',
306
+
style: 'italic',
307
+
weight: 700
308
+
}
309
+
]
310
+
},
311
+
worksans: {
312
+
displayName: 'Work Sans',
313
+
defs: [
314
+
{
315
+
path: '/fonts/WorkSans-Regular.ttf',
316
+
style: 'normal',
317
+
weight: 400
318
+
},
319
+
{
320
+
path: '/fonts/WorkSans-Italic.ttf',
321
+
style: 'italic',
322
+
weight: 400
323
+
},
324
+
{
325
+
path: '/fonts/WorkSans-Bold.ttf',
326
+
style: 'normal',
327
+
weight: 700
328
+
},
329
+
{
330
+
path: '/fonts/WorkSans-BoldItalic.ttf',
331
+
style: 'italic',
332
+
weight: 700
333
+
}
334
+
]
335
+
},
336
+
notosans: {
337
+
displayName: 'Noto Sans',
338
+
defs: [
339
+
{
340
+
path: '/fonts/NotoSans-Regular.ttf',
341
+
style: 'normal',
342
+
weight: 400
343
+
},
344
+
{
345
+
path: '/fonts/NotoSans-Italic.ttf',
346
+
style: 'italic',
347
+
weight: 400
348
+
},
349
+
{
350
+
path: '/fonts/NotoSans-Bold.ttf',
351
+
style: 'normal',
352
+
weight: 700
353
+
},
354
+
{
355
+
path: '/fonts/NotoSans-BoldItalic.ttf',
356
+
style: 'italic',
357
+
weight: 700
358
+
}
359
+
]
360
+
},
361
+
notoserif: {
362
+
displayName: 'Noto Serif',
363
+
defs: [
364
+
{
365
+
path: '/fonts/NotoSerif-Regular.ttf',
366
+
style: 'normal',
367
+
weight: 400
368
+
},
369
+
{
370
+
path: '/fonts/NotoSerif-Italic.ttf',
371
+
style: 'italic',
372
+
weight: 400
373
+
},
374
+
{
375
+
path: '/fonts/NotoSerif-Bold.ttf',
376
+
style: 'normal',
377
+
weight: 700
378
+
},
379
+
{
380
+
path: '/fonts/NotoSerif-BoldItalic.ttf',
381
+
style: 'italic',
382
+
weight: 700
383
+
}
384
+
]
385
+
},
386
+
librebaskerville: {
387
+
displayName: 'Libre Baskerville',
388
+
defs: [
389
+
{
390
+
path: '/fonts/LibreBaskerville-Regular.otf',
391
+
style: 'normal',
392
+
weight: 400
393
+
},
394
+
{
395
+
path: '/fonts/LibreBaskerville-Italic.otf',
396
+
style: 'italic',
397
+
weight: 400
398
+
},
399
+
{
400
+
path: '/fonts/LibreBaskerville-Bold.otf',
401
+
style: 'normal',
402
+
weight: 700
403
+
}
404
+
]
405
+
},
406
+
ubuntu: {
407
+
displayName: 'Ubuntu',
408
+
defs: [
409
+
{
410
+
path: '/fonts/Ubuntu-Regular.ttf',
411
+
style: 'normal',
412
+
weight: 400
413
+
},
414
+
{
415
+
path: '/fonts/Ubuntu-Italic.ttf',
416
+
style: 'italic',
417
+
weight: 400
418
+
},
419
+
{
420
+
path: '/fonts/Ubuntu-Bold.ttf',
421
+
style: 'normal',
422
+
weight: 700
423
+
},
424
+
{
425
+
path: '/fonts/Ubuntu-BoldItalic.ttf',
426
+
style: 'italic',
427
+
weight: 700
428
+
}
429
+
]
430
+
},
431
+
parkinsans: {
432
+
displayName: 'Parkinsans',
433
+
defs: [
434
+
{
435
+
path: '/fonts/Parkinsans-Regular.ttf',
436
+
style: 'normal',
437
+
weight: 400
438
+
},
439
+
{
440
+
path: '/fonts/Parkinsans-Bold.ttf',
441
+
style: 'normal',
442
+
weight: 700
443
+
}
444
+
]
445
+
},
446
+
lora: {
447
+
displayName: 'Lora',
448
+
defs: [
449
+
{
450
+
path: '/fonts/Lora-Regular.ttf',
451
+
style: 'normal',
452
+
weight: 400
453
+
},
454
+
{
455
+
path: '/fonts/Lora-Italic.ttf',
456
+
style: 'italic',
457
+
weight: 400
458
+
},
459
+
{
460
+
path: '/fonts/Lora-Bold.ttf',
461
+
style: 'normal',
462
+
weight: 700
463
+
},
464
+
{
465
+
path: '/fonts/Lora-BoldItalic.ttf',
466
+
style: 'italic',
467
+
weight: 700
468
+
}
469
+
]
470
+
},
471
+
josefinsans: {
472
+
displayName: 'Josefin Sans',
473
+
defs: [
474
+
{
475
+
path: '/fonts/JosefinSans-Regular.ttf',
476
+
style: 'normal',
477
+
weight: 400
478
+
},
479
+
{
480
+
path: '/fonts/JosefinSans-Italic.ttf',
481
+
style: 'italic',
482
+
weight: 400
483
+
},
484
+
{
485
+
path: '/fonts/JosefinSans-Bold.ttf',
486
+
style: 'normal',
487
+
weight: 700
488
+
},
489
+
{
490
+
path: '/fonts/JosefinSans-BoldItalic.ttf',
491
+
style: 'italic',
492
+
weight: 700
493
+
}
494
+
]
495
+
}
496
+
}
497
+
498
+
export default baseFonts
+112
src/lib/ogimage.js
+112
src/lib/ogimage.js
···
1
+
import { ImageResponse } from "next/og"
2
+
3
+
export default async function OGImage ({ theme, baseFont, titleFont, image, addr, opts }) {
4
+
return new ImageResponse(
5
+
(
6
+
<div
7
+
style={{
8
+
display: "flex",
9
+
flexDirection: "column",
10
+
color: theme.color,
11
+
backgroundColor: theme.background,
12
+
fontFamily: baseFont,
13
+
fontSize: 24,
14
+
padding: 40,
15
+
width: "100%",
16
+
height: "100%",
17
+
}}
18
+
>
19
+
<div
20
+
style={{
21
+
display: "flex",
22
+
flexDirection: "column",
23
+
marginBottom: 20
24
+
}}
25
+
>
26
+
<div
27
+
style={{
28
+
textTransform: "uppercase",
29
+
display: "flex",
30
+
justifyContent: "center",
31
+
color: theme.accent
32
+
}}
33
+
>
34
+
{image.topLine}
35
+
</div>
36
+
<div
37
+
style={{
38
+
fontSize: 54,
39
+
justifyContent: "center",
40
+
fontFamily: titleFont,
41
+
fontWeight: "bold"
42
+
}}
43
+
>
44
+
{image.titleLine}
45
+
</div>
46
+
<div
47
+
style={{
48
+
fontSize: 42,
49
+
display: "flex",
50
+
justifyContent: "center",
51
+
fontFamily: titleFont
52
+
}}
53
+
>
54
+
{`by ${image.authorLine}`}
55
+
</div>
56
+
<div
57
+
style={{
58
+
fontStyle: "italic",
59
+
fontSize: 36,
60
+
fontFamily: titleFont,
61
+
display: "flex",
62
+
justifyContent: "center"
63
+
}}
64
+
>
65
+
{image.chapterLine}
66
+
</div>
67
+
</div>
68
+
<div
69
+
style={{
70
+
backgroundColor: theme.descBackground,
71
+
padding: 20,
72
+
display: "flex",
73
+
flexDirection: "column",
74
+
flexGrow: 1,
75
+
color: theme.descColor,
76
+
alignItems: "flex-end"
77
+
}}
78
+
>
79
+
<div
80
+
style={{
81
+
display: "flex",
82
+
flexDirection: "column",
83
+
flexGrow: 1,
84
+
width: '100%'
85
+
}}
86
+
>
87
+
{image.summary.map(l => (
88
+
<div
89
+
style={{
90
+
width: "100%",
91
+
marginBottom: 10
92
+
}}
93
+
>
94
+
{l}
95
+
</div>
96
+
))}
97
+
</div>
98
+
<div
99
+
style={{
100
+
textAlign: "right",
101
+
fontSize: 18,
102
+
color: theme.accent2
103
+
}}
104
+
>
105
+
{`https://archiveofourown.org/${addr}`}
106
+
</div>
107
+
</div>
108
+
</div>
109
+
),
110
+
opts
111
+
)
112
+
}
+91
src/lib/sanitizeData.js
+91
src/lib/sanitizeData.js
···
1
+
import { getWork } from "@fujocoded/ao3.js"
2
+
import DOM from "fauxdom"
3
+
import { readFile } from 'node:fs/promises'
4
+
import { join } from 'node:path'
5
+
import themes from '@/lib/themes.js'
6
+
import baseFonts from '@/lib/baseFonts.js'
7
+
import titleFonts from '@/lib/titleFonts.js'
8
+
9
+
export default async function sanitizeData ({ type, data, props}) {
10
+
const baseFont = props.has('baseFont') ? props.get('baseFont') : process.env.DEFAULT_BASE_FONT
11
+
const baseFontData = baseFonts[baseFont]
12
+
const titleFont = props.has('titleFont') ? props.get('titleFont') : process.env.DEFAULT_TITLE_FONT
13
+
const titleFontData = titleFonts[titleFont]
14
+
const themeData = props.has('theme') ? themes[props.get('theme')] : themes[process.env.DEFAULT_THEME]
15
+
const parentWork = type === 'work' && data.chapterInfo ? await getWork({workId: data.id}) : null
16
+
const bfs = await Promise.all(baseFontData.defs.map(async (bf) => {
17
+
return {
18
+
name: baseFontData.displayName,
19
+
data: await readFile(
20
+
join(process.cwd(), bf.path)
21
+
),
22
+
style: bf.style,
23
+
weight: bf.weight
24
+
}
25
+
})).then(x => x)
26
+
const tfs = await Promise.all(titleFontData.defs.map(async (tf) => {
27
+
return {
28
+
name: titleFontData.displayName,
29
+
data: await readFile(
30
+
join(process.cwd(), tf.path)
31
+
),
32
+
style: tf.style,
33
+
weight: tf.weight
34
+
}
35
+
})).then(x => x)
36
+
const authorsFormatted = data.authors
37
+
? data.authors.map((a) => {
38
+
if (a.anonymous) return "Anonymous"
39
+
if (a.pseud !== a.username) return `${a.pseud} (${a.username})`
40
+
return a.username
41
+
})
42
+
: []
43
+
const authorString = authorsFormatted.length > 1
44
+
? authorsFormatted.slice(0, -1).join(", ") + " & " +
45
+
authorsFormatted.slice(-1)[0]
46
+
: authorsFormatted[0]
47
+
const summaryContent = type === 'work'
48
+
? (props.get('summaryType') === 'chapter' && data.chapterInfo && data.chapterInfo.summary ? data.chapterInfo.summary : (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : (data.summary ? data.summary : (parentWork ? parentWork.summary : ''))))
49
+
: (props.get('summaryType') === 'custom' && props.has('customSummary') ? props.get('customSummary') : data.notes)
50
+
const summaryDOM = new DOM(summaryContent, {decodeEntities: true});
51
+
const summaryFormatted = summaryDOM.innerHTML.replace(/\<br(?: \/)?\>/g, "\n").replace(
52
+
/(<([^>]+)>)/ig,
53
+
"",
54
+
).split("\n")
55
+
const titleString = type === 'work' ? data.title : data.name
56
+
const chapterString = data.chapterInfo ? (data.chapterInfo.name
57
+
? data.chapterInfo.name
58
+
: "Chapter " + data.chapterInfo.index) : null
59
+
const chapterCountString = data.chapters
60
+
? ' | <b>Chapters:</b> '+data.chapters.published+' / '+(
61
+
data.chapters.total
62
+
? data.chapters.total
63
+
: '?'
64
+
)
65
+
: ''
66
+
const fandomString = type === 'work' ? (
67
+
data.fandoms.length > 1
68
+
? (
69
+
data.fandoms.length <= 5
70
+
? data.fandoms.slice(0, -1).join(", ")+" & "+data.fandoms.slice(-1)
71
+
: data.fandoms.join(", ")+" (+"+(data.fandoms.length - 4)+")"
72
+
)
73
+
: data.fandoms[0]
74
+
) : (
75
+
data.works.map(w => w.fandoms).reduce((a, b) => { return a.concat(b) }).filter((w, i) => { return i === data.works.indexOf(w) })
76
+
)
77
+
return {
78
+
topLine: fandomString,
79
+
titleLine: titleString,
80
+
authorLine: authorString,
81
+
chapterLine: chapterString,
82
+
summary: summaryFormatted,
83
+
url: 'https://archiveofourown.org/',
84
+
theme: themeData,
85
+
baseFont: baseFont,
86
+
titleFont: titleFont,
87
+
opts: {
88
+
fonts: bfs.concat(tfs)
89
+
}
90
+
}
91
+
}
+94
src/lib/themes.js
+94
src/lib/themes.js
···
1
+
const themes = {
2
+
ao3: {
3
+
name: 'AO3',
4
+
background: '#990000',
5
+
color: '#FFFFFF',
6
+
descBackground: '#FFFFFF',
7
+
descColor: '#000000',
8
+
accent: '#FFFFFF',
9
+
accent2: '#990000'
10
+
},
11
+
softEra: {
12
+
name: 'Soft Era',
13
+
background: '#F9F5F5',
14
+
color: '#C8B3B3',
15
+
descBackground: '#F9F5F5',
16
+
descColor: '#414141',
17
+
accent: '#DB90A7',
18
+
accent2: '#EEAABE'
19
+
},
20
+
wildCherry: {
21
+
name: 'Wild Cherry',
22
+
background: '#2B1F32',
23
+
color: '#FFFFFF',
24
+
descBackground: '#FFFFFF',
25
+
descColor: '#2B1F32',
26
+
accent: '#E15D97',
27
+
accent2: '#0AACC5'
28
+
},
29
+
rosePine: {
30
+
name: 'Rosé Pine',
31
+
background: '#191724',
32
+
color: '#e0def4',
33
+
descBackground: '#1f1d2e',
34
+
descColor: '#e0def4',
35
+
accent: '#eb6f92',
36
+
accent2: '#31748f'
37
+
},
38
+
rosePineDawn: {
39
+
name: 'Rosé Pine Dawn',
40
+
background: '#faf4ed',
41
+
color: '#575279',
42
+
descBackground: '#fffaf3',
43
+
descColor: '#575279',
44
+
accent: '#eb6f92',
45
+
accent2: '#286983'
46
+
},
47
+
rosePineMoon: {
48
+
name: 'Rosé Pine Moon',
49
+
background: '#232136',
50
+
color: '#e0def4',
51
+
descBackground: '#2a273f',
52
+
descColor: '#e0def4',
53
+
accent: '#b4637a',
54
+
accent2: '#3e8fb0'
55
+
},
56
+
solarizedLight: {
57
+
name: 'Solarized Light',
58
+
background: '#fdf6e3',
59
+
color: '#b58900',
60
+
descBackground: '#eee8d5',
61
+
descColor: '#002b36',
62
+
accent: '#d33682',
63
+
accent2: '#2aa198'
64
+
},
65
+
solarizedDark: {
66
+
name: 'Solarized Dark',
67
+
background: '#002b36',
68
+
color: '#b58900',
69
+
descBackground: '#073642',
70
+
descColor: '#fdf6e3',
71
+
accent: '#d33682',
72
+
accent2: '#2aa198'
73
+
},
74
+
squidgeworld: {
75
+
name: 'Squidgeworld',
76
+
background: '#b8860b',
77
+
color: '#f5f5dc',
78
+
descBackground: '#f5f5dc',
79
+
color: '#2a2a2a',
80
+
accent: '#fece3f',
81
+
accent2: '#818D4C'
82
+
},
83
+
superlove: {
84
+
name: 'Superlove',
85
+
background: '#df6191',
86
+
color: '#ffffff',
87
+
descBackground: '#FFFFFF',
88
+
color: '#2a2a2a',
89
+
accent: '#F9E4E6',
90
+
accent2: '#a33961'
91
+
}
92
+
}
93
+
94
+
export default themes
+226
src/lib/titleFonts.js
+226
src/lib/titleFonts.js
···
1
+
import baseFonts from "./baseFonts.js"
2
+
3
+
const titleFonts = {
4
+
...baseFonts,
5
+
playfairdisplay: {
6
+
displayName: 'Playfair Display',
7
+
defs: [
8
+
{
9
+
path: '/fonts/Playfair-Regular.ttf',
10
+
style: 'normal',
11
+
weight: 400
12
+
},
13
+
{
14
+
path: '/fonts/Playfair-Italic.ttf',
15
+
style: 'italic',
16
+
weight: 400
17
+
},
18
+
{
19
+
path: '/fonts/Playfair-Bold.ttf',
20
+
style: 'normal',
21
+
weight: 700
22
+
},
23
+
{
24
+
path: '/fonts/Playfair-BoldItalic.ttf',
25
+
style: 'italic',
26
+
weight: 700
27
+
}
28
+
]
29
+
},
30
+
ultra: {
31
+
displayName: 'Ultra',
32
+
defs: [
33
+
{
34
+
path: '/fonts/Ultra-Regular.ttf',
35
+
style: 'normal',
36
+
weight: 400
37
+
}
38
+
]
39
+
},
40
+
stacksansheadline: {
41
+
displayName: 'Stack Sans Headline',
42
+
defs: [
43
+
{
44
+
path: '/fonts/StackSansHeadline-Regular.ttf',
45
+
style: 'normal',
46
+
weight: 400
47
+
},
48
+
{
49
+
path: '/fonts/StackSansHeadline-Bold.ttf',
50
+
style: 'normal',
51
+
weight: 700
52
+
}
53
+
]
54
+
},
55
+
stacksansnotch: {
56
+
displayName: 'Stack Sans Notch',
57
+
defs: [
58
+
{
59
+
path: '/fonts/StackSansNotch-Regular.ttf',
60
+
style: 'normal',
61
+
weight: 400
62
+
},
63
+
{
64
+
path: '/fonts/StackSansNotch-Bold.ttf',
65
+
style: 'normal',
66
+
weight: 700
67
+
}
68
+
]
69
+
},
70
+
titanone: {
71
+
displayName: 'Titan One',
72
+
defs: []
73
+
},
74
+
momotrustdisplay: {
75
+
displayName: 'Momo Trust Display',
76
+
defs: [
77
+
{
78
+
path: '/fonts/MomoTrustDisplay-Regular.ttf',
79
+
style: 'normal',
80
+
weight: 400
81
+
},
82
+
{
83
+
path: '/fonts/MomoTrustDisplay-Bold.ttf',
84
+
style: 'normal',
85
+
weight: 700
86
+
}
87
+
]
88
+
},
89
+
momosignature: {
90
+
displayName: 'Momo Signature',
91
+
defs: [
92
+
{
93
+
path: '/fonts/MomoSignature-Regular.ttf',
94
+
style: 'normal',
95
+
weight: 400
96
+
}
97
+
]
98
+
},
99
+
londrinasketch: {
100
+
displayName: 'Londrina Sketch',
101
+
defs: [
102
+
{
103
+
path: '/fonts/LondrinaSketch-Regular.ttf',
104
+
style: 'normal',
105
+
weight: 400
106
+
}
107
+
]
108
+
},
109
+
londrinashadow: {
110
+
displayName: 'Londrina Shadow',
111
+
defs: [
112
+
{
113
+
path: '/fonts/LondrinaShadow-Regular.ttf',
114
+
style: 'normal',
115
+
weight: 400
116
+
}
117
+
]
118
+
},
119
+
londrinasolid: {
120
+
displayName: 'Londrina Solid',
121
+
defs: [
122
+
{
123
+
path: '/fonts/LondrinaSolid-Regular.ttf',
124
+
style: 'normal',
125
+
weight: 400
126
+
},
127
+
{
128
+
path: '/fonts/LondrinaSolid-Black.ttf',
129
+
style: 'normal',
130
+
weight: 700
131
+
}
132
+
]
133
+
},
134
+
bebasneue: {
135
+
displayName: 'Bebas Neue',
136
+
defs: [
137
+
{
138
+
path: '/fonts/BebasNeue-Regular.ttf',
139
+
style: 'normal',
140
+
weight: 400
141
+
}
142
+
]
143
+
},
144
+
oswald: {
145
+
displayName: 'Oswald',
146
+
defs: [
147
+
{
148
+
path: '/fonts/Oswald-Regular.ttf',
149
+
style: 'normal',
150
+
weight: 400
151
+
},
152
+
{
153
+
path: '/fonts/Oswald-Bold.ttf',
154
+
style: 'normal',
155
+
weight: 700
156
+
}
157
+
]
158
+
},
159
+
archivoblack: {
160
+
displayName: 'Archivo Black',
161
+
defs: [
162
+
{
163
+
path: '/fonts/ArchivoBlack.otf',
164
+
style: 'normal',
165
+
weight: 400
166
+
}
167
+
]
168
+
},
169
+
alfaslabone: {
170
+
displayName: 'Alfa Slab One',
171
+
defs: [
172
+
{
173
+
path: '/fonts/AlfaSlabOne-Regular.ttf',
174
+
style: 'normal',
175
+
weight: 400
176
+
}
177
+
]
178
+
},
179
+
sixtyfour: {
180
+
displayName: 'SixtyFour',
181
+
defs: [
182
+
{
183
+
path: '/fonts/Sixtyfour-Regular.ttf',
184
+
style: 'normal',
185
+
weight: 400
186
+
},
187
+
{
188
+
path: '/fonts/Sixtyfour-Regular.ttf',
189
+
style: 'normal',
190
+
weight: 700
191
+
}
192
+
]
193
+
},
194
+
datalegreyathin: {
195
+
displayName: 'Datalegreya Thin',
196
+
defs: [
197
+
{
198
+
path: '/fonts/Datalegreya-Thin.otf',
199
+
style: 'normal',
200
+
weight: 400
201
+
}
202
+
]
203
+
},
204
+
datalegreyadot: {
205
+
displayName: 'Datalegreya Dot',
206
+
defs: [
207
+
{
208
+
path: '/fonts/Datalegreya-Dot.otf',
209
+
style: 'normal',
210
+
weight: 400
211
+
}
212
+
]
213
+
},
214
+
datalegreyagradient: {
215
+
displayName: 'Datalegreya Gradient',
216
+
defs: [
217
+
{
218
+
path: '/fonts/Datalegreya-Gradient.otf',
219
+
style: 'normal',
220
+
weight: 400
221
+
}
222
+
]
223
+
}
224
+
}
225
+
226
+
export default titleFonts