├── .gitignore ├── license.md ├── package.json ├── readme.md ├── server.js ├── src ├── assets │ ├── favicon.ico │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── mstile-150x150.png ├── components │ ├── app.js │ └── header │ │ ├── index.js │ │ └── style.css ├── index.js ├── manifest.json ├── routes │ ├── home │ │ ├── index.js │ │ └── style.css │ └── profile │ │ ├── index.js │ │ └── style.css └── style │ └── index.css └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | /build 4 | /*.log -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 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 | "private": true, 3 | "name": "preact-cli-ssr-demo", 4 | "version": "0.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "preact watch", 8 | "build": "preact build", 9 | "start": "node server" 10 | }, 11 | "devDependencies": { 12 | "preact-cli": "^2.0.0" 13 | }, 14 | "dependencies": { 15 | "compression": "^1.7.1", 16 | "polka": "^0.4.0", 17 | "preact": "^8.2.1", 18 | "preact-compat": "^3.17.0", 19 | "preact-render-to-string": "^3.7.0", 20 | "sirv": "^0.1.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Preact CLI SSR Demo 2 | 3 | > A quick demo that illustrates how to add SSR to a Preact CLI app 4 | 5 | This demo was built with [`preact-cli`](https://github.com/developit/preact-cli), using the [`default`](https://github.com/preactjs-templates/default) template. 6 | 7 | It's powered by an Express server with `gzip` compression... nothing special there. 8 | 9 | Because of how `preact-cli` produces the `build` directory, the server must respect static/file requests first. This also means that your `build/index.html` will _always_ be served on the `/` request. This isn't a bad thing, it's just something to be aware of! 10 | 11 | **Important:** This server behaves exactly like Preact CLI's [prerendering](https://github.com/developit/preact-cli#pre-rendering). This means that if you (or your libraries) have references to `window` or `document`, you must wrap them in conditional statements or include a shim. 12 | 13 | 14 | ## Install 15 | 16 | ```sh 17 | $ git clone https://github.com/lukeed/preact-cli-ssr 18 | $ npm install 19 | $ npm run build 20 | $ npm start 21 | ``` 22 | 23 | 24 | ## License 25 | 26 | MIT © [Luke Edwards](https://lukeed.com) -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const sirv = require('sirv'); 2 | const polka = require('polka'); 3 | const { h } = require('preact'); 4 | const { basename } = require('path'); 5 | const { readFileSync } = require('fs'); 6 | const compression = require('compression')(); 7 | const render = require('preact-render-to-string'); 8 | const bundle = require('./build/ssr-build/ssr-bundle'); 9 | 10 | const App = bundle.default; 11 | const { PORT=3000 } = process.env; 12 | 13 | // TODO: improve this? 14 | const RGX = /
]*>.*?(?= { 26 | let body = render(h(App, { url:req.url })); 27 | res.setHeader('Content-Type', 'text/html'); 28 | res.end(template.replace(RGX, body)); 29 | }) 30 | .listen(PORT, err => { 31 | if (err) throw err; 32 | console.log(`> Running on localhost:${PORT}`); 33 | }); 34 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukeed/preact-cli-ssr/9459dd5fd3f7d8c59c47900f59c9d802ebcb3a56/src/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { Router } from 'preact-router'; 3 | 4 | import Header from './header'; 5 | import Home from '../routes/home'; 6 | import Profile from '../routes/profile'; 7 | // import Home from 'async!../routes/home'; 8 | // import Profile from 'async!../routes/profile'; 9 | 10 | export default class App extends Component { 11 | /** Gets fired when the route changes. 12 | * @param {Object} event "change" event from [preact-router](http://git.io/preact-router) 13 | * @param {string} event.url The newly routed URL 14 | */ 15 | handleRoute = e => { 16 | this.currentUrl = e.url; 17 | }; 18 | 19 | render(props) { 20 | return ( 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | import style from './style'; 4 | 5 | export default class Header extends Component { 6 | render() { 7 | return ( 8 |
9 |

Preact App

10 | 15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/header/style.css: -------------------------------------------------------------------------------- 1 | .header { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | width: 100%; 6 | height: 56px; 7 | padding: 0; 8 | background: #673AB7; 9 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 10 | z-index: 50; 11 | } 12 | 13 | .header h1 { 14 | float: left; 15 | margin: 0; 16 | padding: 0 15px; 17 | font-size: 24px; 18 | line-height: 56px; 19 | font-weight: 400; 20 | color: #FFF; 21 | } 22 | 23 | .header nav { 24 | float: right; 25 | font-size: 100%; 26 | } 27 | 28 | .header nav a { 29 | display: inline-block; 30 | height: 56px; 31 | line-height: 56px; 32 | padding: 0 15px; 33 | min-width: 50px; 34 | text-align: center; 35 | background: rgba(255,255,255,0); 36 | text-decoration: none; 37 | color: #FFF; 38 | will-change: background-color; 39 | } 40 | 41 | .header nav a:hover, 42 | .header nav a:active { 43 | background: rgba(0,0,0,0.2); 44 | } 45 | 46 | .header nav a.active { 47 | background: rgba(0,0,0,0.4); 48 | } 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import App from './components/app'; 3 | 4 | export default App; 5 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Preact CLI SSR", 3 | "short_name": "Preact CLI SSR", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#673ab8", 9 | "icons": [{ 10 | "src": "/assets/icons/android-chrome-192x192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "/assets/icons/android-chrome-512x512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/home/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import style from './style'; 3 | 4 | export default class Home extends Component { 5 | render() { 6 | return ( 7 |
8 |

Home

9 |

This is the Home component.

10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/routes/home/style.css: -------------------------------------------------------------------------------- 1 | .home { 2 | padding: 56px 20px; 3 | min-height: 100%; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/profile/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import style from './style'; 3 | 4 | export default class Profile extends Component { 5 | state = { 6 | time: Date.now(), 7 | count: 10 8 | }; 9 | 10 | // gets called when this route is navigated to 11 | componentDidMount() { 12 | // start a timer for the clock: 13 | this.timer = setInterval(this.updateTime, 1000); 14 | } 15 | 16 | // gets called just before navigating away from the route 17 | componentWillUnmount() { 18 | clearInterval(this.timer); 19 | } 20 | 21 | // update the current time 22 | updateTime = () => { 23 | this.setState({ time: Date.now() }); 24 | }; 25 | 26 | increment = () => { 27 | this.setState({ count: this.state.count+1 }); 28 | }; 29 | 30 | // Note: `user` comes from the URL, courtesy of our router 31 | render({ user }, { time, count }) { 32 | return ( 33 |
34 |

Profile: {user}

35 |

This is the user profile for a user named { user }.

36 | 37 |
Current time: {new Date(time).toLocaleString()}
38 | 39 |

40 | 41 | {' '} 42 | Clicked {count} times. 43 |

44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/routes/profile/style.css: -------------------------------------------------------------------------------- 1 | .profile { 2 | padding: 56px 20px; 3 | min-height: 100%; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | padding: 0; 5 | margin: 0; 6 | background: #FAFAFA; 7 | font-family: 'Helvetica Neue', arial, sans-serif; 8 | font-weight: 400; 9 | color: #444; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | #app { 19 | height: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font: 14px/1.21 'Helvetica Neue', arial, sans-serif; 3 | font-weight: 400; 4 | } 5 | 6 | h1 { 7 | text-align: center; 8 | } 9 | --------------------------------------------------------------------------------