feat(server): enable share og information for docs (#7794)
This commit is contained in:
95
packages/backend/server/src/core/doc-renderer/controller.ts
Normal file
95
packages/backend/server/src/core/doc-renderer/controller.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Controller, Get, Param, Res } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import xss from 'xss';
|
||||
|
||||
import { DocNotFound } from '../../fundamentals';
|
||||
import { PermissionService } from '../permission';
|
||||
import { PageDocContent } from '../utils/blocksuite';
|
||||
import { DocContentService } from './service';
|
||||
|
||||
interface RenderOptions {
|
||||
og: boolean;
|
||||
content: boolean;
|
||||
}
|
||||
|
||||
@Controller('/workspace/:workspaceId/:docId')
|
||||
export class DocRendererController {
|
||||
constructor(
|
||||
private readonly doc: DocContentService,
|
||||
private readonly permission: PermissionService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async render(
|
||||
@Res() res: Response,
|
||||
@Param('workspaceId') workspaceId: string,
|
||||
@Param('docId') docId: string
|
||||
) {
|
||||
if (workspaceId === docId) {
|
||||
throw new DocNotFound({ spaceId: workspaceId, docId });
|
||||
}
|
||||
|
||||
// if page is public, show all
|
||||
// if page is private, but workspace public og is on, show og but not content
|
||||
const opts: RenderOptions = {
|
||||
og: false,
|
||||
content: false,
|
||||
};
|
||||
const isPagePublic = await this.permission.isPublicPage(workspaceId, docId);
|
||||
|
||||
if (isPagePublic) {
|
||||
opts.og = true;
|
||||
opts.content = true;
|
||||
} else {
|
||||
const allowPreview = await this.permission.allowUrlPreview(workspaceId);
|
||||
|
||||
if (allowPreview) {
|
||||
opts.og = true;
|
||||
}
|
||||
}
|
||||
|
||||
let docContent = opts.og
|
||||
? await this.doc.getPageContent(workspaceId, docId)
|
||||
: null;
|
||||
if (!docContent) {
|
||||
docContent = { title: 'untitled', summary: '' };
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
if (!opts.og) {
|
||||
res.setHeader('X-Robots-Tag', 'noindex');
|
||||
}
|
||||
res.send(this._render(docContent, opts));
|
||||
}
|
||||
|
||||
_render(doc: PageDocContent, { og }: RenderOptions): string {
|
||||
const title = xss(doc.title);
|
||||
const summary = xss(doc.summary);
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${title} | AFFiNE</title>
|
||||
<meta name="theme-color" content="#fafafa" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" sizes="192x192" href="/favicon-192.png" />
|
||||
${!og ? '<meta name="robots" content="noindex, nofollow" />' : ''}
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta name="twitter:description" content="${title}" />
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
||||
<meta property="og:title" content="${title}" />
|
||||
<meta property="og:description" content="${summary}" />
|
||||
<meta property="og:image" content="https://affine.pro/og.jpeg" />
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
16
packages/backend/server/src/core/doc-renderer/index.ts
Normal file
16
packages/backend/server/src/core/doc-renderer/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DocStorageModule } from '../doc';
|
||||
import { PermissionModule } from '../permission';
|
||||
import { DocRendererController } from './controller';
|
||||
import { DocContentService } from './service';
|
||||
|
||||
@Module({
|
||||
imports: [DocStorageModule, PermissionModule],
|
||||
providers: [DocContentService],
|
||||
controllers: [DocRendererController],
|
||||
exports: [DocContentService],
|
||||
})
|
||||
export class DocRendererModule {}
|
||||
|
||||
export { DocContentService };
|
||||
88
packages/backend/server/src/core/doc-renderer/service.ts
Normal file
88
packages/backend/server/src/core/doc-renderer/service.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { applyUpdate, Doc } from 'yjs';
|
||||
|
||||
import { Cache } from '../../fundamentals';
|
||||
import { PgWorkspaceDocStorageAdapter } from '../doc';
|
||||
import {
|
||||
type PageDocContent,
|
||||
parsePageDoc,
|
||||
parseWorkspaceDoc,
|
||||
type WorkspaceDocContent,
|
||||
} from '../utils/blocksuite';
|
||||
|
||||
@Injectable()
|
||||
export class DocContentService {
|
||||
constructor(
|
||||
private readonly cache: Cache,
|
||||
private readonly workspace: PgWorkspaceDocStorageAdapter
|
||||
) {}
|
||||
|
||||
async getPageContent(
|
||||
workspaceId: string,
|
||||
guid: string
|
||||
): Promise<PageDocContent | null> {
|
||||
const cacheKey = `workspace:${workspaceId}:doc:${guid}:content`;
|
||||
const cachedResult = await this.cache.get<PageDocContent>(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const docRecord = await this.workspace.getDoc(workspaceId, guid);
|
||||
if (!docRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = new Doc();
|
||||
applyUpdate(doc, docRecord.bin);
|
||||
|
||||
const content = parsePageDoc(doc);
|
||||
|
||||
if (content) {
|
||||
await this.cache.set(cacheKey, content, {
|
||||
ttl:
|
||||
7 *
|
||||
24 *
|
||||
60 *
|
||||
60 *
|
||||
1000 /* TODO(@forehalo): we need time constants helper */,
|
||||
});
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
async getWorkspaceContent(
|
||||
workspaceId: string
|
||||
): Promise<WorkspaceDocContent | null> {
|
||||
const cacheKey = `workspace:${workspaceId}:content`;
|
||||
const cachedResult = await this.cache.get<WorkspaceDocContent>(cacheKey);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const docRecord = await this.workspace.getDoc(workspaceId, workspaceId);
|
||||
if (!docRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const doc = new Doc();
|
||||
applyUpdate(doc, docRecord.bin);
|
||||
|
||||
const content = parseWorkspaceDoc(doc);
|
||||
|
||||
if (content) {
|
||||
await this.cache.set(cacheKey, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
async markDocContentCacheStale(workspaceId: string, guid: string) {
|
||||
const key =
|
||||
workspaceId === guid
|
||||
? `workspace:${workspaceId}:content`
|
||||
: `workspace:${workspaceId}:doc:${guid}:content`;
|
||||
await this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user