├── .gitattributes ├── .gitignore ├── .prettierrc ├── example ├── feed.xml.js ├── index.html └── script.js ├── .editorconfig ├── src ├── Logger.ts ├── Writer.ts ├── Server.ts ├── cli.ts └── Crawler.ts ├── CHANGELOG.md ├── LICENSE ├── package.json ├── .github └── workflows │ └── nodejs.yml ├── tsconfig.json ├── README.md └── pnpm-lock.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .presite 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /example/feed.xml.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return `` 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SPA 5 | 6 | 7 |
8 | feed 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/script.js: -------------------------------------------------------------------------------- 1 | if (location.pathname === '/') { 2 | document.getElementById('app').innerHTML = ` 3 |

home

4 | 5 | other page 6 | ` 7 | } else { 8 | document.getElementById('app').innerHTML = 'other page' 9 | } 10 | -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | verbose?: boolean 3 | 4 | constructor({ verbose }: { verbose?: boolean } = {}) { 5 | this.verbose = verbose 6 | } 7 | 8 | log(...args: any[]) { 9 | if (!this.verbose) return 10 | 11 | console.log(...args) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0](https://github.com/egoist/presite/compare/v1.0.0...v2.0.0) (2020-07-31) 2 | 3 | 4 | ### Features 5 | 6 | * crawl pages ([f78cf17](https://github.com/egoist/presite/commit/f78cf17ec1e8f2d937f79da452105f4967c99859)) 7 | * expose onBrowserPage option ([5d099c4](https://github.com/egoist/presite/commit/5d099c4b8ab1ea345920a7ee7e22f2ba50c33c53)) 8 | * support xml and json pages ([b8a0467](https://github.com/egoist/presite/commit/b8a046747a042b8effcd137b77a1f17bb2dedfc2)) 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Writer.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | 4 | type WriterOptions = { 5 | outDir: string 6 | } 7 | 8 | export class Writer { 9 | opts: WriterOptions 10 | 11 | constructor(opts: WriterOptions) { 12 | this.opts = opts 13 | } 14 | 15 | write({ file, html }: { file: string; html: string }) { 16 | const { outDir } = this.opts 17 | const filepath = path.join(outDir, file) 18 | return fs 19 | .ensureDir(path.dirname(filepath)) 20 | .then(() => fs.writeFile(filepath, html, 'utf8')) 21 | } 22 | 23 | copyFrom(from: string) { 24 | from = path.resolve(from) 25 | const outDir = path.resolve(this.opts.outDir) 26 | if (from === outDir) return Promise.resolve() 27 | return fs.copy(from, outDir) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) egoist <0x142857@gmail.com> (https://egoist.moe) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "presite", 3 | "version": "2.0.0", 4 | "description": "CLI app for pre-rendering SPA websites.", 5 | "repository": { 6 | "url": "egoist/presite", 7 | "type": "git" 8 | }, 9 | "bin": "dist/cli.js", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "test": "npm run build", 15 | "build": "rm -rf dist && tsup src/cli.ts --dts", 16 | "prepublishOnly": "npm run build", 17 | "presite": "node -r esbuild-register src/cli.ts" 18 | }, 19 | "author": "egoist <0x142857@gmail.com>", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@egoist/promise-queue": "^1.1.0", 23 | "cac": "^6.7.2", 24 | "chalk": "^4.1.0", 25 | "fs-extra": "^9.1.0", 26 | "get-port": "^5.0.0", 27 | "joycon": "^3.0.1", 28 | "polka": "^0.5.2", 29 | "read-pkg-up": "^8.0.0", 30 | "sirv": "^1.0.11", 31 | "taki": "^2.3.3", 32 | "update-notifier": "^5.1.0" 33 | }, 34 | "devDependencies": { 35 | "@types/fs-extra": "^9.0.1", 36 | "@types/polka": "^0.5.2", 37 | "@types/puppeteer-core": "^5.4.0", 38 | "@types/update-notifier": "^5.0.0", 39 | "esbuild-register": "^2.3.0", 40 | "tsup": "^4.8.21", 41 | "typescript": "^4.2.3" 42 | }, 43 | "engines": { 44 | "node": ">=12" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | issue_comment: 9 | types: [created] 10 | 11 | jobs: 12 | test: 13 | # skip "ci skip" unless a issue comment explictly asks 14 | if: "!contains(github.event.head_commit.message, 'ci skip') || (github.event.issue && startsWith(github.event.issue.comment.body, '/please_release'))" 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 25 | - uses: actions/checkout@v2 26 | 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: 14.x 30 | 31 | - name: Cache ~/.pnpm-store 32 | uses: actions/cache@v2 33 | env: 34 | cache-name: cache-pnpm-store 35 | with: 36 | path: ~/.pnpm-store 37 | key: ${{ runner.os }}-${{ matrix.node-version }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-${{ matrix.node-version }}-build-${{ env.cache-name }}- 40 | ${{ runner.os }}-${{ matrix.node-version }}-build- 41 | ${{ runner.os }}- 42 | - name: Install pnpm 43 | run: npm i -g pnpm 44 | 45 | - name: Install deps 46 | run: pnpm i 47 | 48 | # Runs a set of commands using the runners shell 49 | - name: Build and Test 50 | run: npm run test 51 | 52 | - name: Release 53 | run: pnpx semantic-release --branches main 54 | if: "contains(github.event.head_commit.message, 'publish release') || github.event.issue" 55 | env: 56 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | -------------------------------------------------------------------------------- /src/Server.ts: -------------------------------------------------------------------------------- 1 | import { Server as HttpServer } from 'http' 2 | import { join } from 'path' 3 | import fs from 'fs-extra' 4 | import polka from 'polka' 5 | import getPort from 'get-port' 6 | import serveStatic from 'sirv' 7 | import { SPECIAL_EXTENSIONS_RE } from './Crawler' 8 | 9 | type ServerOptions = { 10 | baseDir: string 11 | outDir: string 12 | } 13 | 14 | export class Server { 15 | app: polka.Polka 16 | hostname: string 17 | opts: ServerOptions 18 | port?: number 19 | server?: HttpServer 20 | 21 | constructor(opts: ServerOptions) { 22 | this.app = polka() 23 | this.hostname = 'localhost' 24 | this.opts = opts 25 | 26 | this.app.use(serveStatic(this.opts.baseDir, { single: true })) 27 | this.app.use(async (req, res, next) => { 28 | if (!SPECIAL_EXTENSIONS_RE.test(req.path)) { 29 | return next() 30 | } 31 | const file = join(this.opts.baseDir, req.path + '.js') 32 | if (await fs.pathExists(file)) { 33 | // Remove copied original file in output directory 34 | // e.g. /feed.xml should remove original feed.xml.js in output directory 35 | await fs.remove(join(this.opts.outDir, req.path + '.js')) 36 | res.setHeader('content-type', 'text/html') 37 | res.end(` 38 | 39 | 40 | 41 | 51 | 52 | 53 | `) 54 | return 55 | } 56 | next() 57 | }) 58 | } 59 | 60 | async start(): Promise { 61 | const port = await getPort() 62 | this.port = port 63 | this.server = new HttpServer(this.app.handler as any) 64 | this.server.listen(this.port!, this.hostname) 65 | } 66 | 67 | stop() { 68 | return this.server && this.server.close() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import path from 'path' 3 | import { cac } from 'cac' 4 | import chalk from 'chalk' 5 | import update from 'update-notifier' 6 | import JoyCon from 'joycon' 7 | import { Page } from 'puppeteer-core' 8 | 9 | import type { CrawlerOptions } from './Crawler' 10 | 11 | const pkg: typeof import('../package.json') = require('../package') 12 | 13 | update({ pkg }).notify() 14 | 15 | async function main() { 16 | const cli = cac('presite') 17 | 18 | cli 19 | .command('[dir]', `Prerender your website`) 20 | .option( 21 | '--wait ', 22 | 'Wait for specific ms or dom element to appear' 23 | ) 24 | .option( 25 | '--manually [optional_variable_name]', 26 | 'Manually set ready state in your app' 27 | ) 28 | .option('-m, --minify', 'Minify HTML') 29 | .option('-r, --routes ', 'Addtional routes to crawl contents from') 30 | .option('-d, -o, --out-dir ', 'The directory to output files') 31 | .option('-q, --quiet', 'Output nothing in console') 32 | .action(async (dir: string | undefined, flags) => { 33 | const { Server } = await import('./Server') 34 | const { Crawler } = await import('./Crawler') 35 | const { Writer } = await import('./Writer') 36 | const { Logger } = await import('./Logger') 37 | 38 | type ConfigInput = CrawlerOptions['options'] & { 39 | baseDir?: string 40 | outDir?: string 41 | } 42 | 43 | let config: Required 44 | 45 | const joycon = new JoyCon({ 46 | packageKey: 'presite', 47 | files: ['package.json', 'presite.config.json', 'presite.config.js'], 48 | }) 49 | 50 | const { data: configData, path: configPath } = await joycon.load() 51 | 52 | if (configPath) { 53 | console.log( 54 | `Using config from ${chalk.green( 55 | path.relative(process.cwd(), configPath) 56 | )}` 57 | ) 58 | } 59 | config = Object.assign( 60 | { 61 | baseDir: '.', 62 | outDir: '.presite', 63 | routes: ['/'], 64 | }, 65 | configData, 66 | flags, 67 | dir && { baseDir: dir } 68 | ) 69 | 70 | const logger = new Logger({ verbose: !flags.quiet }) 71 | 72 | const server = new Server({ 73 | baseDir: config.baseDir, 74 | outDir: config.outDir, 75 | }) 76 | 77 | const writer = new Writer({ 78 | outDir: config.outDir, 79 | }) 80 | 81 | logger.log(`Copy static assets`) 82 | await Promise.all([server.start(), writer.copyFrom(config.baseDir)]) 83 | 84 | const crawler = new Crawler({ 85 | hostname: server.hostname, 86 | port: server.port!, 87 | options: { 88 | routes: config.routes, 89 | onBrowserPage: config.onBrowserPage, 90 | manually: config.manually, 91 | linkFilter: config.linkFilter, 92 | wait: config.wait, 93 | }, 94 | writer, 95 | logger, 96 | }) 97 | 98 | await crawler.crawl() 99 | 100 | server.stop() 101 | logger.log(`Done, check out ${chalk.green(config.outDir)} folder`) 102 | }) 103 | 104 | cli.version(pkg.version) 105 | cli.help() 106 | 107 | cli.parse(process.argv, { run: false }) 108 | await cli.runMatchedCommand() 109 | } 110 | 111 | main().catch((error) => { 112 | console.error(error) 113 | process.exit(1) 114 | }) 115 | -------------------------------------------------------------------------------- /src/Crawler.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseUrl } from 'url' 2 | import { request, cleanup } from 'taki' 3 | import chalk from 'chalk' 4 | import { PromiseQueue } from '@egoist/promise-queue' 5 | import { Writer } from './Writer' 6 | import { Logger } from './Logger' 7 | import { Page } from 'puppeteer-core' 8 | 9 | export const SPECIAL_EXTENSIONS_RE = /\.(xml|json)$/ 10 | 11 | const routeToFile = (route: string) => { 12 | if (/\.html$/.test(route) || SPECIAL_EXTENSIONS_RE.test(route)) { 13 | return route 14 | } 15 | return route.replace(/\/?$/, '/index.html') 16 | } 17 | 18 | export type CrawlerOptions = { 19 | hostname: string 20 | port: number 21 | options: { 22 | routes: string[] | (() => Promise) 23 | onBrowserPage?: (page: Page) => void | Promise 24 | manually?: string | boolean 25 | linkFilter?: (url: string) => boolean 26 | wait?: string | number 27 | } 28 | writer: Writer 29 | logger: Logger 30 | } 31 | 32 | export class Crawler { 33 | opts: CrawlerOptions 34 | 35 | constructor(opts: CrawlerOptions) { 36 | this.opts = opts 37 | } 38 | 39 | async crawl() { 40 | const { hostname, port, options, writer, logger } = this.opts 41 | 42 | const routes = 43 | typeof options.routes === 'function' 44 | ? await options.routes() 45 | : options.routes 46 | 47 | const crawlRoute = async (routes: string[]) => { 48 | const queue = new PromiseQueue( 49 | async (route: string) => { 50 | const file = routeToFile(route) 51 | let links: Set | undefined 52 | const html = await request({ 53 | url: `http://${hostname}:${port}${route}`, 54 | onBeforeRequest(url) { 55 | logger.log(`Crawling contents from ${chalk.cyan(url)}`) 56 | }, 57 | async onBeforeClosingPage(page) { 58 | links = new Set( 59 | await page.evaluate( 60 | ({ hostname, port }: { hostname: string; port: string }) => { 61 | return Array.from(document.querySelectorAll('a')) 62 | .filter((a) => { 63 | return a.hostname === hostname && a.port === port 64 | }) 65 | .map((a) => a.pathname) 66 | }, 67 | { hostname, port: String(port) } 68 | ) 69 | ) 70 | }, 71 | manually: SPECIAL_EXTENSIONS_RE.test(route) 72 | ? true 73 | : options.manually, 74 | async onCreatedPage(page) { 75 | if (options.onBrowserPage) { 76 | await options.onBrowserPage(page) 77 | } 78 | page.on('console', (e) => { 79 | const type = e.type() 80 | // @ts-ignore 81 | const log = console[type] || console.log 82 | const location = e.location() 83 | log( 84 | `Message from ${location.url}:${location.lineNumber}:${location.columnNumber}`, 85 | e.text() 86 | ) 87 | }) 88 | }, 89 | wait: options.wait, 90 | }) 91 | 92 | if (links && links.size > 0) { 93 | const filtered = options.linkFilter 94 | ? Array.from(links).filter(options.linkFilter) 95 | : links 96 | 97 | for (const link of filtered) { 98 | queue.add(link) 99 | } 100 | } 101 | 102 | logger.log(`Writing ${chalk.cyan(file)} for ${chalk.cyan(route)}`) 103 | await writer.write({ html, file }) 104 | }, 105 | { maxConcurrent: 50 } 106 | ) 107 | for (const route of routes) { 108 | queue.add(route) 109 | } 110 | await queue.run() 111 | } 112 | 113 | await crawlRoute(routes) 114 | await cleanup() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | "resolveJsonModule": true, 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | 64 | /* Advanced Options */ 65 | "skipLibCheck": true /* Skip type checking of declaration files. */, 66 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # presite 2 | 3 | [![NPM version](https://img.shields.io/npm/v/presite.svg?style=flat)](https://npmjs.com/package/presite) [![NPM downloads](https://img.shields.io/npm/dm/presite.svg?style=flat)](https://npmjs.com/package/presite) [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/sponsors/egoist) 4 | 5 | ## Why Presite? 6 | 7 | Presite is an alternative to static site generators like Gatsby, Next.js and Nuxt.js etc, the difference is that it uses [Puppeteer](https://pptr.dev) to prerender websites instead of relying on server-side rendering. 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm i -g presite 13 | ``` 14 | 15 | **Note that Presite relies on Chrome (or Chromium) browser on your machine, so you need to ensure it's installed before running Presite**. 16 | 17 | ## Usage 18 | 19 | ```bash 20 | presite ./path/to/your/site 21 | ``` 22 | 23 | Presite is supposed to work with existing single-page applications, first you use something like Create React App, Vue CLI, Parcel or Vite to create a production build of your app, then use Presite to pre-render the website to static HTML files. 24 | 25 | Pre-rendered website will be generated into `.presite` folder. 26 | 27 | ## Examples 28 | 29 |
with Create React App 30 | 31 | ```diff 32 | { 33 | "scripts": { 34 | - "build": "react-scripts build" 35 | + "build": "react-scripts build && presite ./build" 36 | } 37 | } 38 | ``` 39 | 40 |
41 | 42 |
with Vue CLI 43 | 44 | ```diff 45 | { 46 | "scripts": { 47 | - "build": "vue-cli-service build" 48 | + "build": "vue-cli-service build && presite ./dist" 49 | } 50 | } 51 | ``` 52 | 53 |
54 | 55 |
with Poi 56 | 57 | ```diff 58 | { 59 | "scripts": { 60 | - "build": "poi build" 61 | + "build": "poi build && presite ./dist" 62 | } 63 | } 64 | ``` 65 | 66 |
67 | 68 |
with Vite 69 | 70 | ```diff 71 | { 72 | "scripts": { 73 | - "build": "vite build" 74 | + "build": "vite build && presite ./dist" 75 | } 76 | } 77 | ``` 78 | 79 |
80 | 81 | **That's it, Presite prerender all pages of your website without any configuration!** 82 | 83 | Run `presite --help` for all CLI flags. 84 | 85 | ## Non-HTML pages 86 | 87 | Presite also supports rendering non-HTML pages like XML or JSON pages, simply create files ending with `.xml.js` or `.json.js`, let's say you have a `feed.json.js`: 88 | 89 | ```js 90 | import { createJSONFeed } from './somewhere/create-json-feed' 91 | 92 | export default async () => { 93 | const posts = await fetch('/api/my-posts').then((res) => res.json()) 94 | return createJSONFeed(posts) 95 | } 96 | ``` 97 | 98 | You can export a function that resolves to a string or JSON object, then Presite will output this page as `feed.json`. 99 | 100 | These pages are evaluated in browser in a `