Skip to content

Commit bf87645

Browse files
committed
feat: generate open graph images
1 parent 1d75d0b commit bf87645

File tree

27 files changed

+470
-40
lines changed

27 files changed

+470
-40
lines changed

config/rocket.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ export default {
5454
// serviceWorkerName: 'sw.js',
5555
// pathPrefix: '/_site/',
5656

57-
// emptyOutputDir: false,
57+
// clearOutputDir: false,
5858
};

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@
124124
"pre-commit": "lint-staged"
125125
}
126126
},
127+
"imports": {
128+
"#pageTree": "./site/pages/__shared/pageTree.js",
129+
"#assets/*": "./site/src/assets/*"
130+
},
127131
"lint-staged": {
128132
"*.js": [
129133
"eslint --fix",
@@ -140,8 +144,5 @@
140144
"packages/*",
141145
"examples/*",
142146
"presets/*"
143-
],
144-
"imports": {
145-
"#pageTree": "./site/pages/__shared/pageTree.js"
146-
}
147+
]
147148
}

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"rocket:build": "node src/cli.js build -c demo",
3131
"rocket:start": "node src/cli.js start -c demo",
3232
"start": "npm run rocket:start",
33-
"test": "mocha --require ../../scripts/testMochaGlobalHooks.js test-node/**/*.test.{js,cjs} test-node/*.test.{js,cjs}",
33+
"test": "mocha --require ../../scripts/testMochaGlobalHooks.js test-node/**/*.test.{js,cjs} test-node/*.test.{js,cjs} --timeout 5000",
3434
"test:watch": "onchange 'src/**/*.{js,cjs}' 'test-node/**/*.{js,cjs}' -- npm test",
3535
"types:copy": "copyfiles \"./types/**/*.d.ts\" dist-types/",
3636
"xtest:watch": "mocha test/**/*.test.js --parallel --watch"
@@ -60,6 +60,7 @@
6060
"commander": "^9.0.0",
6161
"fs-extra": "^9.0.1",
6262
"gray-matter": "^4.0.3",
63+
"playwright": "^1.15.0",
6364
"plugins-manager": "^0.3.0"
6465
},
6566
"devDependencies": {

packages/cli/src/RocketBuild.js

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// @ts-nocheck
33

44
import { Engine } from '@rocket/engine/server';
5+
import { gatherFiles } from '@rocket/engine';
6+
7+
import { fromRollup } from '@web/dev-server-rollup';
58

69
import { rollup } from 'rollup';
710
import path from 'path';
@@ -10,7 +13,9 @@ import { rollupPluginHTML } from '@web/rollup-plugin-html';
1013
import { createMpaConfig, createServiceWorkerConfig } from '@rocket/building-rollup';
1114
import { adjustPluginOptions } from 'plugins-manager';
1215
import { existsSync } from 'fs';
13-
import { readFile, writeFile } from 'fs/promises';
16+
import { readFile, unlink, writeFile } from 'fs/promises';
17+
18+
import { chromium } from 'playwright';
1419

1520
/**
1621
* @param {object} config
@@ -88,9 +93,14 @@ export class RocketBuild {
8893
outputDir: this.cli.options.outputDevDir,
8994
setupPlugins: this.cli.options.setupEnginePlugins,
9095
renderMode: 'production',
96+
clearOutputDir: this.cli.options.clearOutputDir,
9197
});
9298
await this.engine.build({ autoStop: this.cli.options.buildAutoStop });
9399

100+
if (this.cli.options.buildOpenGraphImages) {
101+
await this.buildOpenGraphImages();
102+
}
103+
94104
if (this.cli.options.buildOptimize) {
95105
await productionBuild(this.cli.options);
96106
await this.engine.copyPublicFilesTo(this.cli.options.outputDir);
@@ -109,4 +119,85 @@ export class RocketBuild {
109119
await writeFile(notFoundHtmlFilePath, notFoundHtml);
110120
}
111121
}
122+
123+
async buildOpenGraphImages() {
124+
const openGraphFiles = await gatherFiles(this.cli.options.outputDevDir, {
125+
fileEndings: ['.opengraph.html'],
126+
});
127+
if (openGraphFiles.length === 0) {
128+
return;
129+
}
130+
131+
// TODO: enable URL support in the Engine and remove this "workaround"
132+
if (
133+
typeof this.cli.options.inputDir !== 'string' ||
134+
typeof this.cli.options.outputDevDir !== 'string'
135+
) {
136+
return;
137+
}
138+
139+
const withWrap = this.cli.options.setupDevServerAndBuildPlugins
140+
? this.cli.options.setupDevServerAndBuildPlugins.map(modFunction => {
141+
modFunction.wrapPlugin = fromRollup;
142+
return modFunction;
143+
})
144+
: [];
145+
146+
this.engine = new Engine();
147+
this.engine.setOptions({
148+
docsDir: this.cli.options.inputDir,
149+
outputDir: this.cli.options.outputDevDir,
150+
setupPlugins: this.cli.options.setupEnginePlugins,
151+
open: false,
152+
clearOutputDir: false,
153+
adjustDevServerOptions: this.cli.options.adjustDevServerOptions,
154+
setupDevServerMiddleware: this.cli.options.setupDevServerMiddleware,
155+
setupDevServerPlugins: [...this.cli.options.setupDevServerPlugins, ...withWrap],
156+
});
157+
try {
158+
await this.engine.start();
159+
160+
const browser = await chromium.launch();
161+
// In 2022 Twitter & Facebook recommend a size of 1200x628 - we capture with 2 dpr for retina displays
162+
const context = await browser.newContext({
163+
viewport: { width: 1200, height: 628 },
164+
deviceScaleFactor: 2,
165+
});
166+
const page = await context.newPage();
167+
168+
for (const openGraphFile of openGraphFiles) {
169+
const relUrl = path.relative(this.cli.options.outputDevDir, openGraphFile);
170+
const imagePath = openGraphFile.replace('.opengraph.html', '.opengraph.png');
171+
const htmlPath = openGraphFile.replace('.opengraph.html', '.html');
172+
const relImageUrl = path.basename(imagePath);
173+
174+
let htmlString = await readFile(htmlPath, 'utf8');
175+
if (!htmlString.includes('<meta property="og:image"')) {
176+
if (htmlString.includes('</head>')) {
177+
htmlString = htmlString.replace(
178+
'</head>',
179+
[
180+
' <meta property="og:image:width" content="2400">',
181+
' <meta property="og:image:height" content="1256">',
182+
` <meta property="og:image" content="./${relImageUrl}">`,
183+
' </head>',
184+
].join('\n'),
185+
);
186+
}
187+
}
188+
const url = `http://localhost:${this.engine.devServer.config.port}/${relUrl}`;
189+
await page.goto(url);
190+
await page.screenshot({ path: imagePath });
191+
192+
await unlink(openGraphFile);
193+
await writeFile(htmlPath, htmlString);
194+
}
195+
await browser.close();
196+
197+
await this.engine.stop();
198+
} catch (e) {
199+
console.log('Could not start dev server to generate open graph images');
200+
console.error(e);
201+
}
202+
}
112203
}

packages/cli/src/RocketCli.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ export class RocketCli {
4040
serviceWorkerName: 'service-worker.js',
4141
buildOptimize: true,
4242
buildAutoStop: true,
43+
buildOpenGraphImages: true,
4344

4445
adjustBuildOptions: options => options,
4546
adjustDevServerOptions: options => options,
4647

4748
configFile: '',
4849
absoluteBaseUrl: '',
49-
emptyOutputDir: true,
50+
clearOutputDir: true,
5051

5152
// /** @type {{[key: string]: ImagePreset}} */
5253
// imagePresets: {

packages/cli/src/RocketStart.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export class RocketStart {
5252
outputDir: this.cli.options.outputDir,
5353
setupPlugins: this.cli.options.setupEnginePlugins,
5454
open: this.cli.options.open,
55+
clearOutputDir: this.cli.options.clearOutputDir,
5556
adjustDevServerOptions: this.cli.options.adjustDevServerOptions,
5657
setupDevServerMiddleware: this.cli.options.setupDevServerMiddleware,
5758
setupDevServerPlugins: [...this.cli.options.setupDevServerPlugins, ...withWrap],
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import chai from 'chai';
2+
import { setupTestCli } from './test-helpers.js';
3+
4+
const { expect } = chai;
5+
6+
describe('Open Graph', () => {
7+
it('generates the image and adds the meta tags', async () => {
8+
const { build, readOutput, outputExists } = await setupTestCli(
9+
'fixtures/04-open-graph/01-generate-image-and-inject-meta',
10+
undefined,
11+
{
12+
buildOptimize: true,
13+
},
14+
);
15+
await build();
16+
17+
expect(readOutput('index.html', { replaceImageHashes: true })).to.equal(
18+
[
19+
'<!DOCTYPE html>',
20+
'<html lang="en">',
21+
' <head>',
22+
' <meta charset="utf-8" />',
23+
' <meta property="og:image:width" content="2400" />',
24+
' <meta property="og:image:height" content="1256" />',
25+
' <meta property="og:image" content="http://my-site.com/__HASH__.png" />',
26+
' </head>',
27+
' <body>',
28+
' <h1>Hello World!</h1>',
29+
' </body>',
30+
'</html>',
31+
].join('\n'),
32+
);
33+
34+
expect(outputExists('./index.opengraph.html')).to.be.false;
35+
});
36+
37+
it('handles multiple pages', async () => {
38+
const { build, readOutput } = await setupTestCli(
39+
'fixtures/04-open-graph/02-multiple-pages',
40+
undefined,
41+
{
42+
buildOptimize: true,
43+
},
44+
);
45+
await build();
46+
47+
expect(readOutput('index.html', { replaceImageHashes: true })).to.equal(
48+
[
49+
'<!DOCTYPE html>',
50+
'<html lang="en">',
51+
' <head>',
52+
' <meta charset="utf-8" />',
53+
' <meta property="og:image:width" content="2400" />',
54+
' <meta property="og:image:height" content="1256" />',
55+
' <meta property="og:image" content="http://my-site.com/__HASH__.png" />',
56+
' </head>',
57+
' <body>',
58+
' <h1>Hello World!</h1>',
59+
' </body>',
60+
'</html>',
61+
].join('\n'),
62+
);
63+
64+
expect(readOutput('components/index.html', { replaceImageHashes: true })).to.equal(
65+
[
66+
'<!DOCTYPE html>',
67+
'<html lang="en">',
68+
' <head>',
69+
' <meta charset="utf-8" />',
70+
' <meta property="og:image:width" content="2400" />',
71+
' <meta property="og:image:height" content="1256" />',
72+
' <meta property="og:image" content="http://my-site.com/__HASH__.png" />',
73+
' </head>',
74+
' <body>',
75+
' <h1>Components</h1>',
76+
' </body>',
77+
'</html>',
78+
].join('\n'),
79+
);
80+
81+
expect(readOutput('components/accordion/index.html', { replaceImageHashes: true })).to.equal(
82+
[
83+
'<!DOCTYPE html>',
84+
'<html lang="en">',
85+
' <head>',
86+
' <meta charset="utf-8" />',
87+
' <meta property="og:image:width" content="2400" />',
88+
' <meta property="og:image:height" content="1256" />',
89+
' <meta property="og:image" content="http://my-site.com/__HASH__.png" />',
90+
' </head>',
91+
' <body>',
92+
' <h1>Accordion</h1>',
93+
' </body>',
94+
'</html>',
95+
].join('\n'),
96+
);
97+
98+
// This image is "wrong" as it does not output the page title as the page is not added to the page tree
99+
expect(readOutput('components/special.html', { replaceImageHashes: true })).to.equal(
100+
[
101+
'<!DOCTYPE html>',
102+
'<html lang="en">',
103+
' <head>',
104+
' <meta charset="utf-8" />',
105+
' <meta property="og:image:width" content="2400" />',
106+
' <meta property="og:image:height" content="1256" />',
107+
' <meta property="og:image" content="http://my-site.com/__HASH__.png" />',
108+
' </head>',
109+
' <body>',
110+
' <h1>Special</h1>',
111+
' </body>',
112+
'</html>',
113+
].join('\n'),
114+
);
115+
});
116+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default /** @type {import('../../../../../types/main').RocketCliOptions} */ ({
2+
absoluteBaseUrl: 'http://my-site.com',
3+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* START - Rocket auto generated - do not touch */
2+
export const sourceRelativeFilePath = 'index.rocket.js';
3+
import { layout, openGraphLayout, html } from './local.data.js';
4+
export { layout, openGraphLayout, html };
5+
/* END - Rocket auto generated - do not touch */
6+
7+
export default () => html`<h1>Hello World!</h1>`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { PageTree } from '@rocket/engine';
2+
import { html } from 'lit';
3+
4+
export const layout = data => html`
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
</head>
10+
<body>
11+
${data.content()}
12+
</body>
13+
</html>
14+
`;
15+
16+
const pageTree = new PageTree();
17+
await pageTree.restore(new URL('./pageTreeData.rocketGenerated.json', import.meta.url));
18+
19+
export const openGraphLayout = data => html`
20+
<!DOCTYPE html>
21+
<html lang="en">
22+
<head>
23+
<meta charset="utf-8" />
24+
</head>
25+
<body>
26+
<h1>${pageTree.getPage(data.sourceRelativeFilePath)?.model?.name}</h1>
27+
<footer>Generated by <a href="https://next.rocket.modern-web.dev/">Rocket</a></footer>
28+
</body>
29+
</html>
30+
`;
31+
32+
export { html };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"h1": "Hello World!",
3+
"name": "Hello World!",
4+
"menuLinkText": "Hello World!",
5+
"url": "/",
6+
"outputRelativeFilePath": "index.html",
7+
"sourceRelativeFilePath": "index.rocket.js",
8+
"level": 0
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default /** @type {import('../../../../../types/main').RocketCliOptions} */ ({
2+
absoluteBaseUrl: 'http://my-site.com',
3+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* START - Rocket auto generated - do not touch */
2+
export const sourceRelativeFilePath = 'components/accordion.rocket.js';
3+
import { layout, openGraphLayout, html } from '../recursive.data.js';
4+
export { layout, openGraphLayout, html };
5+
/* END - Rocket auto generated - do not touch */
6+
7+
export default () => html`<h1>Accordion</h1>`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* START - Rocket auto generated - do not touch */
2+
export const sourceRelativeFilePath = 'components/index.rocket.js';
3+
import { layout, openGraphLayout, html } from '../recursive.data.js';
4+
export { layout, openGraphLayout, html };
5+
/* END - Rocket auto generated - do not touch */
6+
7+
export default () => html`<h1>Components</h1>`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* START - Rocket auto generated - do not touch */
2+
export const sourceRelativeFilePath = 'components/special.html.rocket.js';
3+
import { layout, openGraphLayout, html } from '../recursive.data.js';
4+
export { layout, openGraphLayout, html };
5+
/* END - Rocket auto generated - do not touch */
6+
7+
export default () => html`<h1>Special</h1>`;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* START - Rocket auto generated - do not touch */
2+
export const sourceRelativeFilePath = 'index.rocket.js';
3+
import { layout, openGraphLayout, html } from './recursive.data.js';
4+
export { layout, openGraphLayout, html };
5+
/* END - Rocket auto generated - do not touch */
6+
7+
export default () => html`<h1>Hello World!</h1>`;

0 commit comments

Comments
 (0)