diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts
index 38f3dcb5e9c..7774b017969 100644
--- a/app/server/lib/sendAppPage.ts
+++ b/app/server/lib/sendAppPage.ts
@@ -157,8 +157,8 @@ export function makeSendAppPage({ server, staticDir, tag, testLogin, baseDomain
const staticTag = options.tag || tag;
// If boot tag is used, serve assets locally, otherwise respect
// APP_STATIC_URL.
- const staticOrigin = staticTag === 'boot' ? '' : (process.env.APP_STATIC_URL || '');
- const staticBaseUrl = `${staticOrigin}/v/${staticTag}/`;
+ const staticOrigin = staticTag === 'boot' ? '' : (process.env.APP_STATIC_URL || config.homeUrl || '');
+ const staticBaseUrl = `${staticOrigin.replace(/\/*$/, '')}/v/${staticTag}/`;
const customHeadHtmlSnippet = server.create.getExtraHeadHtml?.() ?? "";
const warning = testLogin ? "
Authentication is not enforced
" : "";
// Preload all languages that will be used or are requested by client.
@@ -172,8 +172,8 @@ export function makeSendAppPage({ server, staticDir, tag, testLogin, baseDomain
).join('\n');
const content = fileContent
.replace("", warning)
- .replace("", getPageTitle(req, config))
- .replace("", getPageMetadataHtmlSnippet(config))
+ .replace("", getPageTitle(config) ?? (translate(req, 'Loading') + "..."))
+ .replace("", getPageMetadataHtmlSnippet(req, config, staticBaseUrl))
.replace("", getPageTitleSuffix(server.getGristConfig()))
.replace("", `` + tagManagerSnippet)
.replace("", preloads)
@@ -269,11 +269,10 @@ function configuredPageTitleSuffix() {
*
* Note: The string returned is escaped and safe to insert into HTML.
*/
-function getPageTitle(req: express.Request, config: GristLoadConfig): string {
+function getPageTitle(config: GristLoadConfig): string|null {
const maybeDoc = getDocFromConfig(config);
- if (!maybeDoc) { return translate(req, 'Loading') + "..."; }
- return handlebars.Utils.escapeExpression(maybeDoc.name);
+ return maybeDoc && handlebars.Utils.escapeExpression(maybeDoc.name);
}
/**
@@ -284,25 +283,26 @@ function getPageTitle(req: express.Request, config: GristLoadConfig): string {
*
* Note: The string returned is escaped and safe to insert into HTML.
*/
-function getPageMetadataHtmlSnippet(config: GristLoadConfig): string {
+function getPageMetadataHtmlSnippet(req: express.Request, config: GristLoadConfig, staticBaseUrl: string): string {
const metadataElements: string[] = [];
const maybeDoc = getDocFromConfig(config);
- const description = maybeDoc?.options?.description;
- if (description) {
- const content = handlebars.Utils.escapeExpression(description);
- metadataElements.push(``);
- metadataElements.push(``);
- metadataElements.push(``);
- }
+ const description = maybeDoc?.options?.description ?? String(translate(req, 'gristMetaDescription'));
+ const escapedDescription = handlebars.Utils.escapeExpression(description);
+ metadataElements.push(``);
+ metadataElements.push(``);
+ metadataElements.push(``);
- const icon = maybeDoc?.options?.icon;
- if (icon) {
- const content = handlebars.Utils.escapeExpression(icon);
- metadataElements.push(``);
- metadataElements.push(``);
- metadataElements.push(``);
- }
+ const icon = maybeDoc?.options?.icon ?? new URL('img/icon-grist.png', staticBaseUrl).href;
+ const escapedIcon = handlebars.Utils.escapeExpression(icon);
+ metadataElements.push(``);
+ metadataElements.push(``);
+ metadataElements.push(``);
+
+ const title = (getPageTitle(config) ?? translate(req, 'Welcome')) + getPageTitleSuffix(config);
+ // NB: We don't generate the content of the tag here.
+ metadataElements.push(``);
+ metadataElements.push(``);
return metadataElements.join('\n');
}
diff --git a/static/img/icon-grist.png b/static/img/icon-grist.png
new file mode 100644
index 00000000000..f4e9f56f93c
Binary files /dev/null and b/static/img/icon-grist.png differ
diff --git a/static/locales/en.server.json b/static/locales/en.server.json
index 3023c47ad28..7be757b677d 100644
--- a/static/locales/en.server.json
+++ b/static/locales/en.server.json
@@ -1,6 +1,8 @@
{
"sendAppPage": {
- "Loading": "Loading"
+ "Loading": "Loading",
+ "gristMetaDescription": "Grist is the evolution of spreadsheets.",
+ "Welcome": "Welcome"
},
"oidc": {
"emailNotVerifiedError": "Please verify your email with the identity provider, and log in again."
diff --git a/test/nbrowser/HomeIntro.ts b/test/nbrowser/HomeIntro.ts
index cf14676f5b6..62f78bab6c9 100644
--- a/test/nbrowser/HomeIntro.ts
+++ b/test/nbrowser/HomeIntro.ts
@@ -36,6 +36,7 @@ describe('HomeIntro', function() {
});
it('should should intro screen for anon', () => testIntroScreen({team: false}));
+ it('should should set meta tags for URL previews', testMetaTags);
it('should not show Other Sites section', testOtherSitesSection);
it('should allow create/import from intro screen', testCreateImport.bind(null, false));
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
@@ -124,6 +125,22 @@ describe('HomeIntro', function() {
}
}
+ async function testMetaTags() {
+ const expectedTitle = 'Welcome - Grist';
+ assert.equal(await driver.find('meta[name="twitter:title"]').getAttribute('content'), expectedTitle);
+ assert.equal(await driver.find('meta[property="og:title"]').getAttribute('content'), expectedTitle);
+
+ const expectedDescription = 'Grist is the evolution of spreadsheets.';
+ assert.equal(await driver.find('meta[name="description"]').getAttribute('content'), expectedDescription);
+ assert.equal(await driver.find('meta[name="twitter:description"]').getAttribute('content'), expectedDescription);
+ assert.equal(await driver.find('meta[property="og:description"]').getAttribute('content'), expectedDescription);
+
+ const gristIconFileName = 'icon-grist.png';
+ assert.include(await driver.find('meta[name="thumbnail"]').getAttribute('content'), gristIconFileName);
+ assert.include(await driver.find('meta[name="twitter:image"]').getAttribute('content'), gristIconFileName);
+ assert.include(await driver.find('meta[property="og:image"]').getAttribute('content'), gristIconFileName);
+ }
+
async function testCreateImport(isLoggedIn: boolean) {
await checkIntroButtons(isLoggedIn);
diff --git a/test/nbrowser/NewDocument.ntest.js b/test/nbrowser/NewDocument.ntest.js
index c81c3f7262e..74dc016c7e8 100644
--- a/test/nbrowser/NewDocument.ntest.js
+++ b/test/nbrowser/NewDocument.ntest.js
@@ -18,8 +18,23 @@ describe('NewDocument.ntest', function() {
this.timeout(10000);
await gu.actions.createNewDoc('Untitled');
assert.equal(await gu.actions.getDocTitle(), 'Untitled');
- assert.equal(await driver.getTitle(), 'Untitled - Grist');
- assert.equal(await $('.active_section .test-viewsection-title').wait().text(), 'TABLE1');
+
+ const expectedTitle = 'Untitled - Grist';
+ assert.equal(await driver.getTitle(), expectedTitle);
+ assert.equal(await driver.find('meta[name="twitter:title"]').getAttribute('content'), expectedTitle);
+ assert.equal(await driver.find('meta[property="og:title"]').getAttribute('content'), expectedTitle);
+
+ const expectedDescription = 'Grist is the evolution of spreadsheets.';
+ assert.equal(await driver.find('meta[name="description"]').getAttribute('content'), expectedDescription);
+ assert.equal(await driver.find('meta[name="twitter:description"]').getAttribute('content'), expectedDescription);
+ assert.equal(await driver.find('meta[property="og:description"]').getAttribute('content'), expectedDescription);
+
+ const gristIconFileName = 'icon-grist.png';
+ assert.include(await driver.find('meta[name="thumbnail"]').getAttribute('content'), gristIconFileName);
+ assert.include(await driver.find('meta[name="twitter:image"]').getAttribute('content'), gristIconFileName);
+ assert.include(await driver.find('meta[property="og:image"]').getAttribute('content'), gristIconFileName);
+
+ assert.equal(await $('.active_section .test-viewsection-title').wait().text(), 'TABLE');
await gu.waitForServer();
});