AppFlowy/frontend/appflowy_web_app/deploy/server.ts
Kilu.He 23c67bcdba
feat: support publish document (#5576)
* feat: support a event for getting encoded collab of document

* feat: support publish view and unpublish views

* feat: publish page to the web

* chore: refacotor share bloc

* feat: call the publish event

* feat: support publish view and unpublish views

* feat: integrate publish api

* feat: integrate unpublish api

* feat: fetch the publish info to show the publish status

* feat: support publish interfaces

* fix: lint error

* fix: modified web server

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* chore: refactor publish bloc

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: the name is too long to publish

* chore: change color

* fix: some style

* fix: some style

* feat: refacotor share menu UI

* fix: some style

* fix: lint

* fix: some style

* feat: refacotor export-as

* fix: some style

* chore: refactor share menu colors

* fix: rust ci

* fix: some style

* fix: some style

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: rerelease

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: og image

* fix: support copy button

* fix: support copy button

* fix: support copy button

* chore: add a params

* feat: use default publish name

* chore: update copy

* feat: show a confirm deletion dialog if the deleted page contains published page

* feat: add copy toast in publish tab

* fix: to 404

fix: to 404

fix: to 404

fix: the error to 404

* feat: unpublish the page auto when moving it to another space

* feat: improve confirm deletion dialog

* feat: show unpublish error

* chore: use beta.appflowy.com

* feat: disable publish in non-apppflowy-cloud user mode

* fix: modified bullted icon style

* fix: the dark mode color

* fix: save the dark mode in local storage

* fix: text color

* chore: make bash script more portable (#5679)

* fix: title longer

* chore: move the files and modified the en

* chore: update deploy.sh

* chore: modified Dockerfile

* chore: modified server.cjs to server.js

* chore: modifed server.js to server.ts

* chore: replace publish url

* chore: remove todo list hover

* chore: show confirm dialog before deleting page

* fix: unpublish the pages before deleting

* fix: table cell bg color

* fix: callout icon

* fix: list number

* fix: emoji

* fix: number icon

* fix: callout icon position

* fix: add margin bottom

* fix: code block

* fix: support scroll for breadcrumbs

* fix: the breadcrumb doesn't update after moving page

* fix: 0705 issues

* fix: update publish status afer deleting page

* chore: add hover effect for visit site button

* fix: remove puiblish url text field enable border color

* chore: update delete page copy

* chore: enable debug category

* fix: only render sidebar if the spaces are ready

* fix: the breadcrumb doesn't update after moving page

* fix: auto code

* fix: add emoji

* fix: add emoji

* fix: favicon

* fix: cypress test

* fix: remove deploy ci

* fix: default url

* chore: revert launch.json

* fix: docker ci

* fix: change favicon

* fix: flutter integration test

* feat: add hover effect to share menu

* chore: add a checkmark if the page has been published

* chore: revert space deletion

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Zack <speed2exe@live.com.sg>
2024-07-08 13:45:57 +08:00

194 lines
5.7 KiB
TypeScript

import path from 'path';
import * as fs from 'fs';
import pino from 'pino';
import { type CheerioAPI, load } from 'cheerio';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { fetch } from 'bun';
const distDir = path.join(__dirname, 'dist');
const indexPath = path.join(distDir, 'index.html');
const baseURL = process.env.AF_BASE_URL as string;
const defaultSite = 'https://appflowy.io';
const setOrUpdateMetaTag = ($: CheerioAPI, selector: string, attribute: string, content: string) => {
if ($(selector).length === 0) {
$('head').append(`<meta ${attribute}="${selector.match(/\[(.*?)\]/)?.[1]}" content="${content}">`);
} else {
$(selector).attr('content', content);
}
};
const logger = pino({
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
destination: `${__dirname}/pino-logger.log`,
},
},
level: 'info',
});
const logRequestTimer = (req: Request) => {
const start = Date.now();
const pathname = new URL(req.url).pathname;
logger.info(`Incoming request: ${pathname}`);
return () => {
const duration = Date.now() - start;
logger.info(`Request for ${pathname} took ${duration}ms`);
};
};
const fetchMetaData = async (url: string) => {
logger.info(`Fetching meta data from ${url}`);
try {
const response = await fetch(url, {
verbose: true,
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
} catch (error) {
logger.error(`Error fetching meta data ${error}`);
return null;
}
};
const createServer = async (req: Request) => {
const timer = logRequestTimer(req);
const reqUrl = new URL(req.url);
const hostname = req.headers.get('host');
logger.info(`Request URL: ${hostname}${reqUrl.pathname}`);
const [namespace, publishName] = reqUrl.pathname.slice(1).split('/');
logger.info(`Namespace: ${namespace}, Publish Name: ${publishName}`);
if (req.method === 'GET') {
if (namespace === '' || !publishName) {
timer();
return new Response(null, {
status: 302,
headers: {
Location: defaultSite,
},
});
}
let metaData;
try {
metaData = await fetchMetaData(`${baseURL}/api/workspace/published/${namespace}/${publishName}`);
} catch (error) {
logger.error(`Error fetching meta data: ${error}`);
}
const htmlData = fs.readFileSync(indexPath, 'utf8');
const $ = load(htmlData);
const description = 'Write, share, and publish docs quickly on AppFlowy.\nGet started for free.';
let title = 'AppFlowy';
const url = `https://${hostname}${reqUrl.pathname}`;
let image = '/og-image.png';
let favicon = '/appflowy.svg';
try {
if (metaData && metaData.view) {
const view = metaData.view;
const emoji = view.icon.value;
const titleList = [];
if (emoji) {
const emojiCode = emoji.codePointAt(0).toString(16); // Convert emoji to hex code
const baseUrl = 'https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u';
favicon = `${baseUrl}${emojiCode}.svg`;
}
if (view.name) {
titleList.push(view.name);
titleList.push('|');
}
titleList.push('AppFlowy');
title = titleList.join(' ');
try {
const cover = view.extra ? JSON.parse(view.extra)?.cover : null;
if (cover) {
if (['unsplash', 'custom'].includes(cover.type)) {
image = cover.value;
} else if (cover.type === 'built_in') {
image = `/covers/m_cover_image_${cover.value}.png`;
}
}
} catch (_) {
// Do nothing
}
}
} catch (error) {
logger.error(`Error injecting meta data: ${error}`);
}
$('title').text(title);
$('link[rel="icon"]').attr('href', favicon);
setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description);
setOrUpdateMetaTag($, 'meta[property="og:title"]', 'property', title);
setOrUpdateMetaTag($, 'meta[property="og:description"]', 'property', description);
setOrUpdateMetaTag($, 'meta[property="og:image"]', 'property', image);
setOrUpdateMetaTag($, 'meta[property="og:url"]', 'property', url);
setOrUpdateMetaTag($, 'meta[property="og:site_name"]', 'property', 'AppFlowy');
setOrUpdateMetaTag($, 'meta[property="og:type"]', 'property', 'website');
setOrUpdateMetaTag($, 'meta[name="twitter:card"]', 'name', 'summary_large_image');
setOrUpdateMetaTag($, 'meta[name="twitter:title"]', 'name', title);
setOrUpdateMetaTag($, 'meta[name="twitter:description"]', 'name', description);
setOrUpdateMetaTag($, 'meta[name="twitter:image"]', 'name', image);
setOrUpdateMetaTag($, 'meta[name="twitter:site"]', 'name', '@appflowy');
timer();
return new Response($.html(), {
headers: { 'Content-Type': 'text/html' },
});
} else {
timer();
logger.error({ message: 'Method not allowed', method: req.method });
return new Response('Method not allowed', { status: 405 });
}
};
declare const Bun: {
serve: (options: { port: number; fetch: typeof createServer; error: (err: Error) => Response }) => void;
};
const start = () => {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
Bun.serve({
port: 3000,
fetch: createServer,
error: (err) => {
logger.error(`Internal Server Error: ${err}`);
return new Response('Internal Server Error', { status: 500 });
},
});
logger.info('Server is running on port 3000');
logger.info(`Base URL: ${baseURL}`);
} catch (err) {
logger.error(err);
process.exit(1);
}
};
start();
export {};