feat: Return publicly shared document title in SSR HTML (#2191)
* feat: Return publicly shared document title in SSR HTML closes #2146 * tests
This commit is contained in:
parent
4cd61db1ea
commit
a99f6bed42
|
@ -0,0 +1,52 @@
|
||||||
|
// @flow
|
||||||
|
import TestServer from "fetch-test-server";
|
||||||
|
import app from "./app";
|
||||||
|
import { buildShare, buildDocument } from "./test/factories";
|
||||||
|
import { flushdb } from "./test/support";
|
||||||
|
|
||||||
|
const server = new TestServer(app.callback());
|
||||||
|
|
||||||
|
beforeEach(() => flushdb());
|
||||||
|
afterAll(() => server.close());
|
||||||
|
|
||||||
|
describe("/share/:id", () => {
|
||||||
|
it("should return standard title in html when loading share", async () => {
|
||||||
|
const share = await buildShare({ published: false });
|
||||||
|
|
||||||
|
const res = await server.get(`/share/${share.id}`);
|
||||||
|
const body = await res.text();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body).toContain("<title>Outline</title>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return standard title in html when share does not exist", async () => {
|
||||||
|
const res = await server.get(`/share/junk`);
|
||||||
|
const body = await res.text();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body).toContain("<title>Outline</title>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return document title in html when loading published share", async () => {
|
||||||
|
const document = await buildDocument();
|
||||||
|
const share = await buildShare({ documentId: document.id });
|
||||||
|
|
||||||
|
const res = await server.get(`/share/${share.id}`);
|
||||||
|
const body = await res.text();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body).toContain(`<title>${document.title}</title>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return document title in html when loading published share with nested doc route", async () => {
|
||||||
|
const document = await buildDocument();
|
||||||
|
const share = await buildShare({ documentId: document.id });
|
||||||
|
|
||||||
|
const res = await server.get(`/share/${share.id}/doc/test-Cl6g1AgPYn`);
|
||||||
|
const body = await res.text();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
expect(body).toContain(`<title>${document.title}</title>`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,14 +6,17 @@ import Koa from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import sendfile from "koa-sendfile";
|
import sendfile from "koa-sendfile";
|
||||||
import serve from "koa-static";
|
import serve from "koa-static";
|
||||||
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { languages } from "../shared/i18n";
|
import { languages } from "../shared/i18n";
|
||||||
import env from "./env";
|
import env from "./env";
|
||||||
import apexRedirect from "./middlewares/apexRedirect";
|
import apexRedirect from "./middlewares/apexRedirect";
|
||||||
|
import Share from "./models/Share";
|
||||||
import { opensearchResponse } from "./utils/opensearch";
|
import { opensearchResponse } from "./utils/opensearch";
|
||||||
import prefetchTags from "./utils/prefetchTags";
|
import prefetchTags from "./utils/prefetchTags";
|
||||||
import { robotsResponse } from "./utils/robots";
|
import { robotsResponse } from "./utils/robots";
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
const isTest = process.env.NODE_ENV === "test";
|
||||||
const koa = new Koa();
|
const koa = new Koa();
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
@ -22,6 +25,9 @@ const readIndexFile = async (ctx) => {
|
||||||
if (isProduction) {
|
if (isProduction) {
|
||||||
return readFile(path.join(__dirname, "../app/index.html"));
|
return readFile(path.join(__dirname, "../app/index.html"));
|
||||||
}
|
}
|
||||||
|
if (isTest) {
|
||||||
|
return readFile(path.join(__dirname, "/static/index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
const middleware = ctx.devMiddleware;
|
const middleware = ctx.devMiddleware;
|
||||||
await new Promise((resolve) => middleware.waitUntilValid(resolve));
|
await new Promise((resolve) => middleware.waitUntilValid(resolve));
|
||||||
|
@ -39,7 +45,7 @@ const readIndexFile = async (ctx) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderApp = async (ctx, next) => {
|
const renderApp = async (ctx, next, title = "Outline") => {
|
||||||
if (ctx.request.path === "/realtime/") {
|
if (ctx.request.path === "/realtime/") {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
@ -51,10 +57,34 @@ const renderApp = async (ctx, next) => {
|
||||||
ctx.body = page
|
ctx.body = page
|
||||||
.toString()
|
.toString()
|
||||||
.replace(/\/\/inject-env\/\//g, environment)
|
.replace(/\/\/inject-env\/\//g, environment)
|
||||||
|
.replace(/\/\/inject-title\/\//g, title)
|
||||||
.replace(/\/\/inject-prefetch\/\//g, prefetchTags)
|
.replace(/\/\/inject-prefetch\/\//g, prefetchTags)
|
||||||
.replace(/\/\/inject-slack-app-id\/\//g, process.env.SLACK_APP_ID || "");
|
.replace(/\/\/inject-slack-app-id\/\//g, process.env.SLACK_APP_ID || "");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderShare = async (ctx, next) => {
|
||||||
|
const { shareId } = ctx.params;
|
||||||
|
|
||||||
|
// Find the share record if publicly published so that the document title
|
||||||
|
// can be be returned in the server-rendered HTML. This allows it to appear in
|
||||||
|
// unfurls with more reliablity
|
||||||
|
let share;
|
||||||
|
|
||||||
|
if (isUUID(shareId)) {
|
||||||
|
share = await Share.findOne({
|
||||||
|
where: {
|
||||||
|
id: shareId,
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow shares to be embedded in iframes on other websites
|
||||||
|
ctx.remove("X-Frame-Options");
|
||||||
|
|
||||||
|
return renderApp(ctx, next, share ? share.document.title : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
// serve static assets
|
// serve static assets
|
||||||
koa.use(
|
koa.use(
|
||||||
serve(path.resolve(__dirname, "../../public"), {
|
serve(path.resolve(__dirname, "../../public"), {
|
||||||
|
@ -105,10 +135,8 @@ router.get("/opensearch.xml", (ctx) => {
|
||||||
ctx.body = opensearchResponse();
|
ctx.body = opensearchResponse();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/share/*", (ctx, next) => {
|
router.get("/share/:shareId", renderShare);
|
||||||
ctx.remove("X-Frame-Options");
|
router.get("/share/:shareId/*", renderShare);
|
||||||
return renderApp(ctx, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
// catch all for application
|
// catch all for application
|
||||||
router.get("*", renderApp);
|
router.get("*", renderApp);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Outline</title>
|
<title>//inject-title//</title>
|
||||||
<meta name="theme-color" content="#FFF" />
|
<meta name="theme-color" content="#FFF" />
|
||||||
<meta name="slack-app-id" content="//inject-slack-app-id//" />
|
<meta name="slack-app-id" content="//inject-slack-app-id//" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
|
@ -26,6 +26,13 @@ export async function buildShare(overrides: Object = {}) {
|
||||||
const user = await buildUser({ teamId: overrides.teamId });
|
const user = await buildUser({ teamId: overrides.teamId });
|
||||||
overrides.userId = user.id;
|
overrides.userId = user.id;
|
||||||
}
|
}
|
||||||
|
if (!overrides.documentId) {
|
||||||
|
const document = await buildDocument({
|
||||||
|
createdById: overrides.userId,
|
||||||
|
teamId: overrides.teamId,
|
||||||
|
});
|
||||||
|
overrides.documentId = document.id;
|
||||||
|
}
|
||||||
|
|
||||||
return Share.create({
|
return Share.create({
|
||||||
published: true,
|
published: true,
|
||||||
|
|
Reference in New Issue