+ {/* image tiles */}
+
+ {[...Array(18)].map((_, i) => {
+ const image = imagesArray.at(i % imagesArray.length)
+ return (
+
+ {image &&

}
+
+ )
+ })}
+ {/* background overlay */}
+
+
+ {/* foreground text & images */}
+
+
+ JOIN THE CONVERSATION
+
+
+ {imagesAcross.map((image, i) => {
+ return (
+
+

+
+ )
+ })}
+
+
+ {record?.name || 'Starter Pack'}
+
+
+ on Bluesky
+
+
+
+ )
+}
diff --git a/bskyogcard/src/config.ts b/bskyogcard/src/config.ts
new file mode 100644
index 0000000000..fafa18e743
--- /dev/null
+++ b/bskyogcard/src/config.ts
@@ -0,0 +1,40 @@
+import {envInt, envStr} from '@atproto/common'
+
+export type Config = {
+ service: ServiceConfig
+}
+
+export type ServiceConfig = {
+ port: number
+ version?: string
+ appviewUrl: string
+ originVerify?: string
+}
+
+export type Environment = {
+ port?: number
+ version?: string
+ appviewUrl?: string
+ originVerify?: string
+}
+
+export const readEnv = (): Environment => {
+ return {
+ port: envInt('CARD_PORT'),
+ version: envStr('CARD_VERSION'),
+ appviewUrl: envStr('CARD_APPVIEW_URL'),
+ originVerify: envStr('CARD_ORIGIN_VERIFY'),
+ }
+}
+
+export const envToCfg = (env: Environment): Config => {
+ const serviceCfg: ServiceConfig = {
+ port: env.port ?? 3000,
+ version: env.version,
+ appviewUrl: env.appviewUrl ?? 'https://api.bsky.app',
+ originVerify: env.originVerify,
+ }
+ return {
+ service: serviceCfg,
+ }
+}
diff --git a/bskyogcard/src/context.ts b/bskyogcard/src/context.ts
new file mode 100644
index 0000000000..f92651cafb
--- /dev/null
+++ b/bskyogcard/src/context.ts
@@ -0,0 +1,44 @@
+import {readFileSync} from 'node:fs'
+
+import {AtpAgent} from '@atproto/api'
+import * as path from 'path'
+import {fileURLToPath} from 'url'
+
+import {Config} from './config.js'
+
+const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
+
+export type AppContextOptions = {
+ cfg: Config
+ appviewAgent: AtpAgent
+ fonts: {name: string; data: Buffer}[]
+}
+
+export class AppContext {
+ cfg: Config
+ appviewAgent: AtpAgent
+ fonts: {name: string; data: Buffer}[]
+ abortController = new AbortController()
+
+ constructor(private opts: AppContextOptions) {
+ this.cfg = this.opts.cfg
+ this.appviewAgent = this.opts.appviewAgent
+ this.fonts = this.opts.fonts
+ }
+
+ static async fromConfig(cfg: Config, overrides?: Partial