(core) Populate doc title, description and thumbnail in app.html

Summary:
Fills in the title and description/thumbnail (for templates) in app.html if the
page being requested is for a document.

Test Plan: Tested manually.

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3544
This commit is contained in:
George Gevoian 2022-07-27 13:20:14 -07:00
parent 7078922a65
commit c54dde3dba
6 changed files with 106 additions and 6 deletions

View File

@ -128,6 +128,10 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App)
// Set document title to strings like "DocName - Grist" // Set document title to strings like "DocName - Grist"
owner.autoDispose(subscribe(pageModel.currentDocTitle, (use, docName) => { owner.autoDispose(subscribe(pageModel.currentDocTitle, (use, docName) => {
// If the document hasn't loaded yet, don't update the title; since the HTML document already has
// a title element with the document's name, there's no need for further action.
if (!pageModel.currentDoc.get()) { return; }
document.title = `${docName}${getPageTitleSuffix(getGristConfig())}`; document.title = `${docName}${getPageTitleSuffix(getGristConfig())}`;
})); }));

View File

@ -19,7 +19,7 @@ import {User} from './entity/User';
import {HomeDBManager} from './lib/HomeDBManager'; import {HomeDBManager} from './lib/HomeDBManager';
// Special public organization that contains examples and templates. // Special public organization that contains examples and templates.
const TEMPLATES_ORG_DOMAIN = process.env.GRIST_ID_PREFIX ? export const TEMPLATES_ORG_DOMAIN = process.env.GRIST_ID_PREFIX ?
`templates-${process.env.GRIST_ID_PREFIX}` : `templates-${process.env.GRIST_ID_PREFIX}` :
'templates'; 'templates';

View File

@ -1,11 +1,14 @@
import {getPageTitleSuffix, GristLoadConfig, HideableUiElements, IHideableUiElement} from 'app/common/gristUrls'; import {getPageTitleSuffix, GristLoadConfig, HideableUiElements, IHideableUiElement} from 'app/common/gristUrls';
import {getTagManagerSnippet} from 'app/common/tagManager'; import {getTagManagerSnippet} from 'app/common/tagManager';
import {Document} from 'app/common/UserAPI';
import {isAnonymousUser, RequestWithLogin} from 'app/server/lib/Authorizer'; import {isAnonymousUser, RequestWithLogin} from 'app/server/lib/Authorizer';
import {RequestWithOrg} from 'app/server/lib/extractOrg'; import {RequestWithOrg} from 'app/server/lib/extractOrg';
import {GristServer} from 'app/server/lib/GristServer'; import {GristServer} from 'app/server/lib/GristServer';
import {getSupportedEngineChoices} from 'app/server/lib/serverUtils'; import {getSupportedEngineChoices} from 'app/server/lib/serverUtils';
import * as express from 'express'; import * as express from 'express';
import * as fse from 'fs-extra'; import * as fse from 'fs-extra';
import jsesc from 'jsesc';
import * as handlebars from 'handlebars';
import * as path from 'path'; import * as path from 'path';
export interface ISendAppPageOptions { export interface ISendAppPageOptions {
@ -65,8 +68,10 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial<GristLoadCo
export function makeMessagePage(staticDir: string) { export function makeMessagePage(staticDir: string) {
return async (req: express.Request, resp: express.Response, message: any) => { return async (req: express.Request, resp: express.Response, message: any) => {
const fileContent = await fse.readFile(path.join(staticDir, "message.html"), 'utf8'); const fileContent = await fse.readFile(path.join(staticDir, "message.html"), 'utf8');
const content = fileContent const content = fileContent.replace(
.replace("<!-- INSERT MESSAGE -->", `<script>window.message = ${JSON.stringify(message)};</script>`); "<!-- INSERT MESSAGE -->",
`<script>window.message = ${jsesc(message, {isScriptContext: true, json: true})};</script>`
);
resp.status(200).type('html').send(content); resp.status(200).type('html').send(content);
}; };
} }
@ -98,10 +103,15 @@ export function makeSendAppPage(opts: {
const warning = testLogin ? "<div class=\"dev_warning\">Authentication is not enforced</div>" : ""; const warning = testLogin ? "<div class=\"dev_warning\">Authentication is not enforced</div>" : "";
const content = fileContent const content = fileContent
.replace("<!-- INSERT WARNING -->", warning) .replace("<!-- INSERT WARNING -->", warning)
.replace("<!-- INSERT TITLE -->", getPageTitle(config))
.replace("<!-- INSERT META -->", getPageMetadataHtmlSnippet(config))
.replace("<!-- INSERT TITLE SUFFIX -->", getPageTitleSuffix(server?.getGristConfig())) .replace("<!-- INSERT TITLE SUFFIX -->", getPageTitleSuffix(server?.getGristConfig()))
.replace("<!-- INSERT BASE -->", `<base href="${staticBaseUrl}">` + tagManagerSnippet) .replace("<!-- INSERT BASE -->", `<base href="${staticBaseUrl}">` + tagManagerSnippet)
.replace("<!-- INSERT CUSTOM -->", customHeadHtmlSnippet) .replace("<!-- INSERT CUSTOM -->", customHeadHtmlSnippet)
.replace("<!-- INSERT CONFIG -->", `<script>window.gristConfig = ${JSON.stringify(config)};</script>`); .replace(
"<!-- INSERT CONFIG -->",
`<script>window.gristConfig = ${jsesc(config, {isScriptContext: true, json: true})};</script>`
);
resp.status(options.status).type('html').send(content); resp.status(options.status).type('html').send(content);
}; };
} }
@ -123,3 +133,51 @@ function configuredPageTitleSuffix() {
const result = process.env.GRIST_PAGE_TITLE_SUFFIX; const result = process.env.GRIST_PAGE_TITLE_SUFFIX;
return result === "_blank" ? "" : result; return result === "_blank" ? "" : result;
} }
/**
* Returns a page title suitable for inserting into an HTML title element.
*
* Currently returns the document name if the page being requested is for a document, or
* a placeholder, "Loading...", that's updated in the client once the page has loaded.
*
* Note: The string returned is escaped and safe to insert into HTML.
*/
function getPageTitle(config: GristLoadConfig): string {
const maybeDoc = getDocFromConfig(config);
if (!maybeDoc) { return 'Loading...'; }
return handlebars.Utils.escapeExpression(maybeDoc.name);
}
/**
* Returns a string representation of 0 or more HTML metadata elements.
*
* Currently includes the document description and thumbnail if the requested page is
* for a document and the document has one set.
*
* Note: The string returned is escaped and safe to insert into HTML.
*/
function getPageMetadataHtmlSnippet(config: GristLoadConfig): string {
const metadataElements: string[] = [];
const maybeDoc = getDocFromConfig(config);
const description = maybeDoc?.options?.description;
if (description) {
const content = handlebars.Utils.escapeExpression(description);
metadataElements.push(`<meta name="description" content="${content}">`);
}
const thumbnail = maybeDoc?.options?.icon;
if (thumbnail) {
const content = handlebars.Utils.escapeExpression(thumbnail);
metadataElements.push(`<meta name="thumbnail" content="${content}">`);
}
return metadataElements.join('\n');
}
function getDocFromConfig(config: GristLoadConfig): Document | null {
if (!config.getDoc || !config.assignmentId) { return null; }
return config.getDoc[config.assignmentId] ?? null;
}

View File

@ -41,6 +41,7 @@
"@types/fs-extra": "5.0.4", "@types/fs-extra": "5.0.4",
"@types/image-size": "0.0.29", "@types/image-size": "0.0.29",
"@types/js-yaml": "3.11.2", "@types/js-yaml": "3.11.2",
"@types/jsesc": "3.0.1",
"@types/jsonwebtoken": "7.2.8", "@types/jsonwebtoken": "7.2.8",
"@types/lodash": "4.14.117", "@types/lodash": "4.14.117",
"@types/lru-cache": "5.1.1", "@types/lru-cache": "5.1.1",
@ -113,6 +114,7 @@
"fs-extra": "7.0.0", "fs-extra": "7.0.0",
"grain-rpc": "0.1.7", "grain-rpc": "0.1.7",
"grainjs": "1.0.2", "grainjs": "1.0.2",
"handlebars": "4.4.5",
"highlight.js": "9.13.1", "highlight.js": "9.13.1",
"http-proxy-agent": "5.0.0", "http-proxy-agent": "5.0.0",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
@ -120,6 +122,7 @@
"image-size": "0.6.3", "image-size": "0.6.3",
"jquery": "2.2.1", "jquery": "2.2.1",
"js-yaml": "3.12.0", "js-yaml": "3.12.0",
"jsesc": "3.0.2",
"jsonwebtoken": "8.3.0", "jsonwebtoken": "8.3.0",
"knockout": "3.5.0", "knockout": "3.5.0",
"locale-currency": "0.0.2", "locale-currency": "0.0.2",

View File

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<meta charset="utf8"> <meta charset="utf8">
<!-- INSERT META -->
<!-- INSERT BASE --> <!-- INSERT BASE -->
@ -15,7 +16,7 @@
<link rel="stylesheet" href="icons/icons.css"> <link rel="stylesheet" href="icons/icons.css">
<!-- INSERT CUSTOM --> <!-- INSERT CUSTOM -->
<title>Loading...<!-- INSERT TITLE SUFFIX --></title> <title><!-- INSERT TITLE --><!-- INSERT TITLE SUFFIX --></title>
</head> </head>
<body> <body>
<!-- INSERT WARNING --> <!-- INSERT WARNING -->

View File

@ -3230,6 +3230,17 @@ gtoken@^5.0.4:
google-p12-pem "^3.0.3" google-p12-pem "^3.0.3"
jws "^4.0.0" jws "^4.0.0"
handlebars@4.4.5:
version "4.4.5"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.5.tgz#1b1f94f9bfe7379adda86a8b73fb570265a0dddd"
integrity sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg==
dependencies:
neo-async "^2.6.0"
optimist "^0.6.1"
source-map "^0.6.1"
optionalDependencies:
uglify-js "^3.1.4"
har-schema@^2.0.0: har-schema@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@ -4352,6 +4363,11 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
integrity sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==
minimist@~1.1.1: minimist@~1.1.1:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
@ -4575,7 +4591,7 @@ negotiator@0.6.2:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
neo-async@^2.6.2: neo-async@^2.6.0, neo-async@^2.6.2:
version "2.6.2" version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
@ -4820,6 +4836,14 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies: dependencies:
wrappy "1" wrappy "1"
optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
integrity sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==
dependencies:
minimist "~0.0.1"
wordwrap "~0.0.2"
os-browserify@~0.3.0: os-browserify@~0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
@ -6510,6 +6534,11 @@ typescript@4.7.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
uglify-js@^3.1.4:
version "3.16.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d"
integrity sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw==
uid-safe@2.1.5, uid-safe@~2.1.5: uid-safe@2.1.5, uid-safe@~2.1.5:
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
@ -6864,6 +6893,11 @@ winston@2.4.5:
isstream "0.1.x" isstream "0.1.x"
stack-trace "0.0.x" stack-trace "0.0.x"
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==
wrap-ansi@^5.1.0: wrap-ansi@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"