├── .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 | [](https://npmjs.com/package/presite) [](https://npmjs.com/package/presite) [](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 `