├── .github └── FUNDING.yml ├── .gitignore ├── public ├── error-400.png ├── error-500.png ├── screenshot-pdf.png ├── screenshot-desktop.png └── screenshot-mobile.png ├── .prettierrc ├── Dockerfile ├── vercel.json ├── config └── index.js ├── validator.js ├── package.json ├── index.js ├── LICENSE ├── browser.js ├── README.md └── screenshot.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: fransallen -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | node_modules -------------------------------------------------------------------------------- /public/error-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticallyio/screenshot/HEAD/public/error-400.png -------------------------------------------------------------------------------- /public/error-500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticallyio/screenshot/HEAD/public/error-500.png -------------------------------------------------------------------------------- /public/screenshot-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticallyio/screenshot/HEAD/public/screenshot-pdf.png -------------------------------------------------------------------------------- /public/screenshot-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticallyio/screenshot/HEAD/public/screenshot-desktop.png -------------------------------------------------------------------------------- /public/screenshot-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticallyio/screenshot/HEAD/public/screenshot-mobile.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | 6 | RUN npm install --only=production 7 | EXPOSE 5000 8 | CMD [ "npm", "start" ] 9 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "regions": ["sin1"], 3 | "builds": [ 4 | { 5 | "src": "index.js", 6 | "use": "@now/node" 7 | } 8 | ], 9 | "routes": [{ "src": "/(.*)", "dest": "/index.js" }] 10 | } 11 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | // Public directory containing static files 7 | publicDir: path.join(__dirname, '..', 'public'), 8 | 9 | // Remote browser endpoint 10 | browserWSEndpoint: process.env.REMOTE_BROWSER || 'ws://localhost:3000', 11 | }; 12 | -------------------------------------------------------------------------------- /validator.js: -------------------------------------------------------------------------------- 1 | const { URL } = require('url'); 2 | 3 | function getInt(str) { 4 | return /[0-9]+/.test(str) ? parseInt(str) : undefined; 5 | } 6 | 7 | function isValidUrl(str) { 8 | try { 9 | const url = new URL(str); 10 | return url.hostname.includes('.'); 11 | } catch (e) { 12 | console.error(e.message); 13 | return false; 14 | } 15 | } 16 | 17 | module.exports = { getInt, isValidUrl }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screenshot", 3 | "description": "A simple screenshot API.", 4 | "private": true, 5 | "dependencies": { 6 | "etag": "1.8.1", 7 | "express": "4.17.1", 8 | "puppeteer-core": "10.1.0" 9 | }, 10 | "devDependencies": { 11 | "prettier": "1.18.2" 12 | }, 13 | "scripts": { 14 | "dev": "nodemon", 15 | "format": "prettier --write '**/*.{js,json,css}'", 16 | "start": "node index" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const express = require('express'); 6 | 7 | const { getMobile, getDesktop, getPdf } = require('./screenshot'); 8 | 9 | const app = express(); 10 | 11 | // Disable `X-Powered-By` header 12 | app.disable('x-powered-by'); 13 | 14 | app.get('/screenshot/pdf/*', getPdf); 15 | app.get('/screenshot/mobile/*', getMobile); 16 | app.get('/*', getDesktop); 17 | 18 | const port = process.env.PORT_SCREENSHOT || 5000; 19 | app.listen(port, () => 20 | console.log(`Screenshot listen on http://localhost:${port}`), 21 | ); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Statically 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer-core'); 2 | const config = require('./config'); 3 | 4 | const disableTransitionDelayCSS = `*,::after,::before{transition-delay:0s!important;transition-duration:0s!important;animation-delay:-.1ms!important;animation-duration:0s!important;animation-play-state:paused!important;caret-color:transparent!important;color-adjust:exact!important}`; 5 | 6 | /* Set browser for desktop */ 7 | async function getScreenshot(url, type, quality, fullPage) { 8 | const browser = await puppeteer.connect({ 9 | browserWSEndpoint: config.browserWSEndpoint, 10 | }); 11 | const page = await browser.newPage(); 12 | 13 | await page.emulate({ 14 | userAgent: 15 | 'Mozilla/5.0 (Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36 Statically-Screenshot/1.0 (+https://statically.io/screenshot/)', 16 | viewport: { 17 | width: 1280, 18 | height: 960, 19 | deviceScaleFactor: 1, 20 | isMobile: false, 21 | hasTouch: false, 22 | isLandscape: true, 23 | }, 24 | }); 25 | 26 | await page.goto(url /*{ waitUntil: 'networkidle0' }*/); 27 | await page.addStyleTag({ content: disableTransitionDelayCSS }); 28 | 29 | const file = await page.screenshot({ type, quality, fullPage }); 30 | await browser.close(); 31 | console.log('HTTP ' + url); 32 | return file; 33 | } 34 | 35 | /* Set browser for mobile */ 36 | async function getScreenshotMobile(url, type, quality, fullPage) { 37 | const browser = await puppeteer.connect({ 38 | browserWSEndpoint: config.browserWSEndpoint, 39 | }); 40 | const page = await browser.newPage(); 41 | 42 | await page.emulate({ 43 | userAgent: 44 | 'Mozilla/5.0 (Linux Android 5.0 SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Mobile Safari/537.36 Statically-Screenshot-Mobile/1.0 (+https://statically.io/screenshot/)', 45 | viewport: { 46 | width: 360, 47 | height: 640, 48 | deviceScaleFactor: 1, 49 | isMobile: true, 50 | hasTouch: true, 51 | isLandscape: false, 52 | }, 53 | }); 54 | 55 | await page.goto(url /*{ waitUntil: 'networkidle0' }*/); 56 | await page.addStyleTag({ content: disableTransitionDelayCSS }); 57 | 58 | console.log('HTTP ' + url); 59 | const file = await page.screenshot({ type, quality, fullPage }); 60 | await browser.close(); 61 | return file; 62 | } 63 | 64 | /* Set browser for PDF */ 65 | async function generatePdf(url) { 66 | const browser = await puppeteer.connect({ 67 | browserWSEndpoint: config.browserWSEndpoint, 68 | }); 69 | const page = await browser.newPage(); 70 | 71 | await page.emulate({ 72 | userAgent: 73 | 'Mozilla/5.0 (Linux Android 5.0 SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Mobile Safari/537.36 Statically-Screenshot-PDF/1.0 (+https://statically.io/screenshot/)', 74 | viewport: { 75 | width: 360, 76 | height: 640, 77 | deviceScaleFactor: 1, 78 | isMobile: true, 79 | hasTouch: true, 80 | isLandscape: false, 81 | }, 82 | }); 83 | 84 | await page.goto(url /*{ waitUntil: 'networkidle0' }*/); 85 | const file = await page.pdf({ format: 'A4' }); 86 | await browser.close(); 87 | console.log('HTTP ' + url); 88 | return file; 89 | } 90 | 91 | module.exports = { getScreenshot, getScreenshotMobile, generatePdf }; 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
Automagically converts URLs into images and PDFs.
10 | 11 |
12 | statically.io |
13 | Twitter |
14 | Community |
15 | Become A Backer
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |