fix: enable PDF generation by adding playwright dependency
Browse files- Add playwright as a production dependency in backend
- Set PLAYWRIGHT_BROWSERS_PATH in Dockerfile for reliable browser discovery
- Remove silent fallback on playwright install to surface errors
- Remove non-functional PDF button from editor hero (only relevant in published output)
- Respect showPdf frontmatter flag in publisher pipeline
Made-with: Cursor
- Dockerfile +3 -2
- backend/package-lock.json +45 -0
- backend/package.json +1 -0
- backend/src/publisher/index.ts +3 -2
- frontend/src/editor/frontmatter/FrontmatterHero.tsx +1 -12
Dockerfile
CHANGED
|
@@ -50,8 +50,9 @@ COPY backend/package.json backend/package-lock.json* ./
|
|
| 50 |
RUN npm install --omit=dev
|
| 51 |
COPY --from=backend-build /app/dist ./dist
|
| 52 |
|
| 53 |
-
# Install Playwright Chromium
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
# Copy frontend build
|
| 57 |
COPY --from=frontend-build /app/frontend/dist ./frontend-dist
|
|
|
|
| 50 |
RUN npm install --omit=dev
|
| 51 |
COPY --from=backend-build /app/dist ./dist
|
| 52 |
|
| 53 |
+
# Install Playwright Chromium browser binaries
|
| 54 |
+
ENV PLAYWRIGHT_BROWSERS_PATH=/app/browsers
|
| 55 |
+
RUN npx playwright install chromium
|
| 56 |
|
| 57 |
# Copy frontend build
|
| 58 |
COPY --from=frontend-build /app/frontend/dist ./frontend-dist
|
backend/package-lock.json
CHANGED
|
@@ -36,6 +36,7 @@
|
|
| 36 |
"express": "^4.21.0",
|
| 37 |
"lowlight": "^3.3.0",
|
| 38 |
"multer": "^2.1.1",
|
|
|
|
| 39 |
"ws": "^8.20.0",
|
| 40 |
"yjs": "^13.6.0",
|
| 41 |
"zod": "^4.3.6"
|
|
@@ -3405,6 +3406,50 @@
|
|
| 3405 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 3406 |
}
|
| 3407 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3408 |
"node_modules/postcss": {
|
| 3409 |
"version": "8.5.9",
|
| 3410 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
|
|
|
|
| 36 |
"express": "^4.21.0",
|
| 37 |
"lowlight": "^3.3.0",
|
| 38 |
"multer": "^2.1.1",
|
| 39 |
+
"playwright": "^1.59.1",
|
| 40 |
"ws": "^8.20.0",
|
| 41 |
"yjs": "^13.6.0",
|
| 42 |
"zod": "^4.3.6"
|
|
|
|
| 3406 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 3407 |
}
|
| 3408 |
},
|
| 3409 |
+
"node_modules/playwright": {
|
| 3410 |
+
"version": "1.59.1",
|
| 3411 |
+
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
| 3412 |
+
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
| 3413 |
+
"license": "Apache-2.0",
|
| 3414 |
+
"dependencies": {
|
| 3415 |
+
"playwright-core": "1.59.1"
|
| 3416 |
+
},
|
| 3417 |
+
"bin": {
|
| 3418 |
+
"playwright": "cli.js"
|
| 3419 |
+
},
|
| 3420 |
+
"engines": {
|
| 3421 |
+
"node": ">=18"
|
| 3422 |
+
},
|
| 3423 |
+
"optionalDependencies": {
|
| 3424 |
+
"fsevents": "2.3.2"
|
| 3425 |
+
}
|
| 3426 |
+
},
|
| 3427 |
+
"node_modules/playwright-core": {
|
| 3428 |
+
"version": "1.59.1",
|
| 3429 |
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
| 3430 |
+
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
| 3431 |
+
"license": "Apache-2.0",
|
| 3432 |
+
"bin": {
|
| 3433 |
+
"playwright-core": "cli.js"
|
| 3434 |
+
},
|
| 3435 |
+
"engines": {
|
| 3436 |
+
"node": ">=18"
|
| 3437 |
+
}
|
| 3438 |
+
},
|
| 3439 |
+
"node_modules/playwright/node_modules/fsevents": {
|
| 3440 |
+
"version": "2.3.2",
|
| 3441 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
| 3442 |
+
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
| 3443 |
+
"hasInstallScript": true,
|
| 3444 |
+
"license": "MIT",
|
| 3445 |
+
"optional": true,
|
| 3446 |
+
"os": [
|
| 3447 |
+
"darwin"
|
| 3448 |
+
],
|
| 3449 |
+
"engines": {
|
| 3450 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 3451 |
+
}
|
| 3452 |
+
},
|
| 3453 |
"node_modules/postcss": {
|
| 3454 |
"version": "8.5.9",
|
| 3455 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
|
backend/package.json
CHANGED
|
@@ -39,6 +39,7 @@
|
|
| 39 |
"express": "^4.21.0",
|
| 40 |
"lowlight": "^3.3.0",
|
| 41 |
"multer": "^2.1.1",
|
|
|
|
| 42 |
"ws": "^8.20.0",
|
| 43 |
"yjs": "^13.6.0",
|
| 44 |
"zod": "^4.3.6"
|
|
|
|
| 39 |
"express": "^4.21.0",
|
| 40 |
"lowlight": "^3.3.0",
|
| 41 |
"multer": "^2.1.1",
|
| 42 |
+
"playwright": "^1.59.1",
|
| 43 |
"ws": "^8.20.0",
|
| 44 |
"yjs": "^13.6.0",
|
| 45 |
"zod": "^4.3.6"
|
backend/src/publisher/index.ts
CHANGED
|
@@ -232,8 +232,9 @@ export async function publishDocument(docName: string, token?: string): Promise<
|
|
| 232 |
};
|
| 233 |
|
| 234 |
// Pre-compute public URLs so og:image and PDF link are embedded in HTML
|
|
|
|
| 235 |
const useHf = isHfStorageEnabled();
|
| 236 |
-
if (useHf && isPdfEnabled()) {
|
| 237 |
meta.ogImage = getPublishedAssetUrl(docName, "thumb.jpg");
|
| 238 |
meta.pdfUrl = getPublishedAssetUrl(docName, "article.pdf");
|
| 239 |
}
|
|
@@ -250,7 +251,7 @@ export async function publishDocument(docName: string, token?: string): Promise<
|
|
| 250 |
let pdf: Buffer | null = null;
|
| 251 |
let thumbnail: Buffer | null = null;
|
| 252 |
|
| 253 |
-
if (isPdfEnabled()) {
|
| 254 |
try {
|
| 255 |
const assets = await generatePdfAndThumbnail(html);
|
| 256 |
pdf = assets.pdf;
|
|
|
|
| 232 |
};
|
| 233 |
|
| 234 |
// Pre-compute public URLs so og:image and PDF link are embedded in HTML
|
| 235 |
+
const wantPdf = frontmatter.showPdf !== false;
|
| 236 |
const useHf = isHfStorageEnabled();
|
| 237 |
+
if (wantPdf && useHf && isPdfEnabled()) {
|
| 238 |
meta.ogImage = getPublishedAssetUrl(docName, "thumb.jpg");
|
| 239 |
meta.pdfUrl = getPublishedAssetUrl(docName, "article.pdf");
|
| 240 |
}
|
|
|
|
| 251 |
let pdf: Buffer | null = null;
|
| 252 |
let thumbnail: Buffer | null = null;
|
| 253 |
|
| 254 |
+
if (wantPdf && isPdfEnabled()) {
|
| 255 |
try {
|
| 256 |
const assets = await generatePdfAndThumbnail(html);
|
| 257 |
pdf = assets.pdf;
|
frontend/src/editor/frontmatter/FrontmatterHero.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 28 |
const hasAuthors = data.authors.length > 0;
|
| 29 |
const hasAffiliations = data.affiliations.length > 0;
|
| 30 |
const multipleAffiliations = data.affiliations.length > 1;
|
| 31 |
-
const hasMeta = hasAuthors || data.published || data.doi
|
| 32 |
|
| 33 |
return (
|
| 34 |
<div>
|
|
@@ -146,17 +146,6 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
|
|
| 146 |
/>
|
| 147 |
</div>
|
| 148 |
|
| 149 |
-
{/* PDF cell */}
|
| 150 |
-
{data.showPdf && (
|
| 151 |
-
<div className="meta-container-cell meta-container-cell--pdf">
|
| 152 |
-
<h3>PDF</h3>
|
| 153 |
-
<p style={{ margin: 0, lineHeight: 1 }}>
|
| 154 |
-
<a className="button" href="#" onClick={(e) => e.preventDefault()}>
|
| 155 |
-
Download PDF
|
| 156 |
-
</a>
|
| 157 |
-
</p>
|
| 158 |
-
</div>
|
| 159 |
-
)}
|
| 160 |
</div>
|
| 161 |
</header>
|
| 162 |
)}
|
|
|
|
| 28 |
const hasAuthors = data.authors.length > 0;
|
| 29 |
const hasAffiliations = data.affiliations.length > 0;
|
| 30 |
const multipleAffiliations = data.affiliations.length > 1;
|
| 31 |
+
const hasMeta = hasAuthors || data.published || data.doi;
|
| 32 |
|
| 33 |
return (
|
| 34 |
<div>
|
|
|
|
| 146 |
/>
|
| 147 |
</div>
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
</div>
|
| 150 |
</header>
|
| 151 |
)}
|