mirror of
https://github.com/docmost/docmost.git
synced 2025-11-17 23:31:09 +10:00
Server render shared page meta tags
This commit is contained in:
@ -6,6 +6,7 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Docmost</title>
|
||||
<!--meta-tags-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -37,18 +37,18 @@
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.10",
|
||||
"@nestjs/config": "^4.0.0",
|
||||
"@nestjs/core": "^11.0.10",
|
||||
"@nestjs/common": "^11.0.20",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.20",
|
||||
"@nestjs/event-emitter": "^3.0.0",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.0.10",
|
||||
"@nestjs/platform-socket.io": "^11.0.10",
|
||||
"@nestjs/platform-fastify": "^11.0.20",
|
||||
"@nestjs/platform-socket.io": "^11.0.20",
|
||||
"@nestjs/schedule": "^5.0.1",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.0.10",
|
||||
"@nestjs/websockets": "^11.0.20",
|
||||
"@node-saml/passport-saml": "^5.0.1",
|
||||
"@react-email/components": "0.0.28",
|
||||
"@react-email/render": "1.0.2",
|
||||
|
||||
@ -44,7 +44,7 @@ export class SpaceIdDto {
|
||||
export class ShareInfoDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
shareId: string;
|
||||
shareId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
105
apps/server/src/core/share/share-seo.controller.ts
Normal file
105
apps/server/src/core/share/share-seo.controller.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Controller, Get, Param, Req, Res } from '@nestjs/common';
|
||||
import { ShareService } from './share.service';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { join } from 'path';
|
||||
import * as fs from 'node:fs';
|
||||
import { validate as isValidUUID } from 'uuid';
|
||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||
import { Workspace } from '@docmost/db/types/entity.types';
|
||||
|
||||
@Controller('share')
|
||||
export class ShareSeoController {
|
||||
constructor(
|
||||
private readonly shareService: ShareService,
|
||||
private workspaceRepo: WorkspaceRepo,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
/*
|
||||
* add meta tags to publicly shared pages
|
||||
*/
|
||||
@Get([':shareId/:pageSlug', 'p/:pageSlug'])
|
||||
async getShare(
|
||||
@Res({ passthrough: false }) res: FastifyReply,
|
||||
@Req() req: FastifyRequest,
|
||||
@Param('shareId') shareId: string,
|
||||
@Param('pageSlug') pageSlug: string,
|
||||
) {
|
||||
// Nestjs does not to apply middlewares to paths excluded from the global /api prefix
|
||||
// https://github.com/nestjs/nest/issues/9124
|
||||
// https://github.com/nestjs/nest/issues/11572
|
||||
// https://github.com/nestjs/nest/issues/13401
|
||||
// we have to duplicate the DomainMiddleware code here as a workaround
|
||||
|
||||
let workspace: Workspace = null;
|
||||
if (this.environmentService.isSelfHosted()) {
|
||||
workspace = await this.workspaceRepo.findFirst();
|
||||
} else {
|
||||
const header = req.raw.headers.host;
|
||||
const subdomain = header.split('.')[0];
|
||||
workspace = await this.workspaceRepo.findByHostname(subdomain);
|
||||
}
|
||||
|
||||
const clientDistPath = join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'client/dist',
|
||||
);
|
||||
|
||||
if (fs.existsSync(clientDistPath)) {
|
||||
const indexFilePath = join(clientDistPath, 'index.html');
|
||||
|
||||
if (!workspace) {
|
||||
return this.sendIndex(indexFilePath, res);
|
||||
}
|
||||
|
||||
const pageId = this.extractPageSlugId(pageSlug);
|
||||
|
||||
const share = await this.shareService.getShareForPage(
|
||||
pageId,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!share) {
|
||||
return this.sendIndex(indexFilePath, res);
|
||||
}
|
||||
|
||||
const rawTitle = share.sharedPage.title ?? 'untitled';
|
||||
const metaTitle =
|
||||
rawTitle.length > 80 ? `${rawTitle.slice(0, 77)}…` : rawTitle;
|
||||
|
||||
const metaTagVar = '<!--meta-tags-->';
|
||||
let metaTag = '';
|
||||
if (!share.searchIndexing) {
|
||||
metaTag = '<meta name="robots" content="noindex" />';
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(indexFilePath, 'utf8');
|
||||
const transformedHtml = html
|
||||
.replace(/<title>[\s\S]*?<\/title>/i, `<title>${metaTitle}</title>`)
|
||||
.replace(metaTagVar, metaTag);
|
||||
|
||||
res.type('text/html').send(transformedHtml);
|
||||
}
|
||||
}
|
||||
|
||||
sendIndex(indexFilePath: string, res: FastifyReply) {
|
||||
const stream = fs.createReadStream(indexFilePath);
|
||||
res.type('text/html').send(stream);
|
||||
}
|
||||
|
||||
extractPageSlugId(slug: string): string {
|
||||
if (!slug) {
|
||||
return undefined;
|
||||
}
|
||||
if (isValidUUID(slug)) {
|
||||
return slug;
|
||||
}
|
||||
const parts = slug.split('-');
|
||||
return parts.length > 1 ? parts[parts.length - 1] : slug;
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { ShareController } from './share.controller';
|
||||
import { ShareService } from './share.service';
|
||||
import { TokenModule } from '../auth/token.module';
|
||||
import { ShareSeoController } from './share-seo.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TokenModule],
|
||||
controllers: [ShareController],
|
||||
controllers: [ShareController, ShareSeoController],
|
||||
providers: [ShareService],
|
||||
exports: [ShareService],
|
||||
})
|
||||
|
||||
@ -4,7 +4,12 @@ import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { Logger, NotFoundException, ValidationPipe } from '@nestjs/common';
|
||||
import {
|
||||
Logger,
|
||||
NotFoundException,
|
||||
RequestMethod,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { TransformHttpResponseInterceptor } from './common/interceptors/http-response.interceptor';
|
||||
import { WsRedisIoAdapter } from './ws/adapter/ws-redis.adapter';
|
||||
import { InternalLogFilter } from './common/logger/internal-log-filter';
|
||||
@ -26,7 +31,9 @@ async function bootstrap() {
|
||||
},
|
||||
);
|
||||
|
||||
app.setGlobalPrefix('api', { exclude: ['robots.txt'] });
|
||||
app.setGlobalPrefix('api', {
|
||||
exclude: ['robots.txt', 'share/:shareId/:pageSlug'],
|
||||
});
|
||||
|
||||
const reflector = app.get(Reflector);
|
||||
const redisIoAdapter = new WsRedisIoAdapter(app);
|
||||
|
||||
Reference in New Issue
Block a user