Skip to content

Commit caf47a4

Browse files
committed
feat(og): implement Open Graph image generation for photos
- Added OgModule with OgController and OgService to handle Open Graph image requests. - Integrated Satori and Resvg for rendering images based on photo metadata. - Created OgTemplate for structuring the Open Graph image layout. - Enhanced error handling for photo retrieval and image generation processes. - Updated package dependencies to include @resvg/resvg-js and satori for image processing. Signed-off-by: Innei <tukon479@gmail.com>
1 parent 44f9cbb commit caf47a4

File tree

13 files changed

+1343
-818
lines changed

13 files changed

+1343
-818
lines changed

apps/ssr/src/app/og/[photoId]/route.tsx

Lines changed: 53 additions & 811 deletions
Large diffs are not rendered by default.

apps/web/plugins/vite/feed-sitemap.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { readFileSync } from 'node:fs'
22

33
import type { PhotoManifestItem } from '@afilmory/builder'
4-
import { generateRSSFeed } from '@afilmory/utils'
4+
import { tsImport } from 'tsx/esm/api'
55
import type { Plugin } from 'vite'
66

77
import type { SiteConfig } from '../../../../site.config'
88
import { MANIFEST_PATH } from './__internal__/constants'
99

10+
const { generateRSSFeed } = await tsImport('@afilmory/utils', import.meta.url)
11+
1012
export function createFeedSitemapPlugin(siteConfig: SiteConfig): Plugin {
1113
return {
1214
name: 'feed-sitemap-generator',

apps/web/src/components/ui/photo-viewer/ExifPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const ExifPanel: FC<{
7272
style={{
7373
pointerEvents: visible ? 'auto' : 'none',
7474
backgroundImage:
75-
'linear-gradient(to bottom right, color-mix(in srgb, var(--color-background) 98%, transparent), color-mix(in srgb, var(--color-background) 95%, transparent))',
75+
'linear-gradient(to bottom right, rgba(var(--color-materialMedium)), rgba(var(--color-materialThick)), transparent)',
7676
boxShadow:
7777
'0 8px 32px color-mix(in srgb, var(--color-accent) 8%, transparent), 0 4px 16px color-mix(in srgb, var(--color-accent) 6%, transparent), 0 2px 8px rgba(0, 0, 0, 0.1)',
7878
}}
@@ -101,7 +101,7 @@ export const ExifPanel: FC<{
101101

102102
<ScrollArea
103103
rootClassName="flex-1 min-h-0 overflow-auto lg:overflow-hidden"
104-
viewportClassName="px-4 pb-4 [&_*]:select-text"
104+
viewportClassName="px-4 pb-4 **:select-text"
105105
>
106106
<div className={`space-y-${isMobile ? '3' : '4'}`}>
107107
{/* 基本信息和标签 - 合并到一个 section */}

apps/web/src/components/ui/photo-viewer/GalleryThumbnail.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const GalleryThumbnail: FC<{
9393

9494
return (
9595
<m.div
96-
className="pb-safe border-accent/20 z-10 shrink-0 border-t backdrop-blur-2xl"
96+
className="pb-safe border-accent/20 bg-material-medium z-10 shrink-0 border-t backdrop-blur-2xl"
9797
initial={{ y: 100, opacity: 0 }}
9898
animate={{
9999
y: visible ? 0 : 48,
@@ -103,8 +103,6 @@ export const GalleryThumbnail: FC<{
103103
transition={Spring.presets.smooth}
104104
style={{
105105
pointerEvents: visible ? 'auto' : 'none',
106-
backgroundImage:
107-
'linear-gradient(to bottom right, color-mix(in srgb, var(--color-background) 98%, transparent), color-mix(in srgb, var(--color-background) 95%, transparent))',
108106
boxShadow:
109107
'0 -8px 32px color-mix(in srgb, var(--color-accent) 8%, transparent), 0 -4px 16px color-mix(in srgb, var(--color-accent) 6%, transparent), 0 -2px 8px rgba(0, 0, 0, 0.1)',
110108
}}

be/apps/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@afilmory/utils": "workspace:*",
2727
"@aws-sdk/client-s3": "3.921.0",
2828
"@hono/node-server": "^1.19.6",
29+
"@resvg/resvg-js": "2.6.2",
2930
"better-auth": "1.3.34",
3031
"drizzle-orm": "^0.44.7",
3132
"hono": "4.10.4",
@@ -34,6 +35,7 @@
3435
"pg": "^8.16.3",
3536
"picocolors": "1.1.1",
3637
"reflect-metadata": "0.2.2",
38+
"satori": "0.18.3",
3739
"tsyringe": "4.10.0",
3840
"zod": "^4.1.11"
3941
},

be/apps/core/src/modules/index.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CacheModule } from './cache/cache.module'
1212
import { DashboardModule } from './dashboard/dashboard.module'
1313
import { DataSyncModule } from './data-sync/data-sync.module'
1414
import { FeedModule } from './feed/feed.module'
15+
import { OgModule } from './og/og.module'
1516
import { OnboardingModule } from './onboarding/onboarding.module'
1617
import { PhotoModule } from './photo/photo.module'
1718
import { ReactionModule } from './reaction/reaction.module'
@@ -51,6 +52,7 @@ function createEventModuleOptions(redis: RedisAccessor) {
5152
TenantModule,
5253
DataSyncModule,
5354
FeedModule,
55+
OgModule,
5456

5557
// This must be last
5658
StaticWebModule,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ContextParam, Controller, Get, Param } from '@afilmory/framework'
2+
import type { Context } from 'hono'
3+
4+
import { OgService } from './og.service'
5+
6+
@Controller({ prefix: '/og', bypassGlobalPrefix: true })
7+
export class OgController {
8+
constructor(private readonly ogService: OgService) {}
9+
10+
@Get('/:photoId')
11+
async getOgImage(@ContextParam() context: Context, @Param('photoId') photoId: string) {
12+
return await this.ogService.render(context, photoId)
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Module } from '@afilmory/framework'
2+
3+
import { ManifestModule } from '../manifest/manifest.module'
4+
import { SiteSettingModule } from '../site-setting/site-setting.module'
5+
import { OgController } from './og.controller'
6+
import { OgService } from './og.service'
7+
8+
@Module({
9+
imports: [ManifestModule, SiteSettingModule],
10+
controllers: [OgController],
11+
providers: [OgService],
12+
})
13+
export class OgModule {}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/** @jsxImportSource hono/jsx */
2+
import { Buffer } from 'node:buffer'
3+
4+
import { Resvg } from '@resvg/resvg-js'
5+
import type { SatoriOptions } from 'satori'
6+
import satori from 'satori'
7+
8+
import type { OgTemplateProps } from './og.template'
9+
import { OgTemplate } from './og.template'
10+
11+
interface RenderOgImageOptions {
12+
template: OgTemplateProps
13+
fonts: SatoriOptions['fonts']
14+
}
15+
16+
export async function renderOgImage({ template, fonts }: RenderOgImageOptions): Promise<Uint8Array> {
17+
const svg = await satori(<OgTemplate {...template} />, {
18+
width: 1200,
19+
height: 628,
20+
fonts,
21+
embedFont: true,
22+
})
23+
24+
const svgInput = typeof svg === 'string' ? svg : Buffer.from(svg)
25+
const renderer = new Resvg(svgInput, {
26+
fitTo: { mode: 'width', value: 1200 },
27+
background: 'rgba(0,0,0,0)',
28+
})
29+
30+
return renderer.render().asPng()
31+
}

0 commit comments

Comments
 (0)