├── client ├── blog.js └── client.js ├── public ├── logo.png └── style.css ├── .gitignore ├── server └── getPosts.js ├── package.json ├── webpack.config.js ├── README.md ├── server.js └── LICENSE /client/blog.js: -------------------------------------------------------------------------------- 1 | const blog = () => 'here will be blog'; 2 | 3 | export default blog; 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Everettss/isomorphic-pwa/HEAD/public/logo.png -------------------------------------------------------------------------------- /client/client.js: -------------------------------------------------------------------------------- 1 | import blog from './blog'; 2 | 3 | const renderBlog = () => { 4 | document.getElementById('app').innerHTML = blog(); 5 | }; 6 | 7 | setTimeout(() => { 8 | renderBlog(); 9 | }, 1000); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/script.js 3 | server.build.js 4 | 5 | # misc 6 | .DS_Store 7 | .env.local 8 | .env.development.local 9 | .env.test.local 10 | .env.production.local 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | .idea 17 | 18 | 19 | -------------------------------------------------------------------------------- /server/getPosts.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | 3 | const makePost = () => ({ 4 | avatar: faker.image.avatar(), 5 | title: faker.lorem.sentence(), 6 | date: `${faker.date.recent()}`, 7 | teaser: faker.lorem.paragraph(), 8 | }); 9 | 10 | const getPosts = cb => { 11 | setTimeout(() => { 12 | const posts = Array.from(new Array(5)).map(makePost); 13 | cb(posts); 14 | }, 1000); 15 | }; 16 | 17 | module.exports = getPosts; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-pwa", 3 | "version": "1.0.0", 4 | "description": "Example how to combine PWA with isomorphic rendering", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "./node_modules/webpack/bin/webpack.js -w", 8 | "build": "./node_modules/webpack/bin/webpack.js -p" 9 | }, 10 | "author": "Michal Janaszek", 11 | "license": "MIT", 12 | "dependencies": { 13 | "babel-core": "^6.26.0", 14 | "babel-loader": "^7.1.2", 15 | "babel-preset-env": "^1.6.1", 16 | "express": "^4.16.2", 17 | "faker": "^4.1.0", 18 | "webpack": "^3.8.1", 19 | "webpack-node-externals": "^1.6.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, 'client', 'client.js'), 5 | output: { 6 | path: path.resolve(__dirname, 'public'), 7 | filename: 'script.js', 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | use: { 15 | loader: 'babel-loader', 16 | options: { 17 | presets: ['babel-preset-env'], 18 | }, 19 | }, 20 | }, 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isomorphic-pwa 2 | Example how to combine PWA with isomorphic rendering (SSR) used in article https://michaljanaszek.com/blog/combine-pwa-and-isomorphic-rendering 3 | 4 | ## Build project 5 | `npm install` - surprised? 6 | 7 | `npm run build` - build project 8 | 9 | or `npm run start` - develop project (the same as `build` but with watch) 10 | 11 | 12 | ## Run project 13 | after `start` or `build` run server: 14 | 15 | `node server` for stage 1 - 3 16 | 17 | `node server.build` for stage 4 - 5 18 | 19 | ## Chapters 20 | Project contains branches stage1 - stage5, each branch represents chapter in [article](https://michaljanaszek.com/blog/combine-pwa-and-isomorphic-rendering). 21 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const getPosts = require('./server/getPosts'); 4 | 5 | app.use(express.static('public')); 6 | 7 | const html = () => ` 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
loading
16 |
17 | 18 | 19 | 20 | `; 21 | 22 | app.get('/', (req, res) => { 23 | setTimeout(() => { 24 | res.send(html()); 25 | }, 1000); 26 | }); 27 | 28 | app.get('/posts', (req, res) => { 29 | getPosts(posts => { 30 | res.send(posts); 31 | }); 32 | }); 33 | 34 | app.listen(3000, () => console.log('App is running http://localhost:3000')); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michał Janaszek 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. 22 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | color: #333; 8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 9 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 10 | 'Segoe UI Symbol'; 11 | line-height: 1.3em; 12 | } 13 | 14 | header { 15 | background-color: #563d7c; 16 | padding-top: 20px; 17 | padding-bottom: 20px; 18 | color: white; 19 | } 20 | 21 | nav { 22 | float: right; 23 | display: inline-block; 24 | } 25 | 26 | nav a { 27 | padding-left: 15px; 28 | color: #c9b4db; 29 | text-decoration: none; 30 | } 31 | 32 | main { 33 | padding-top: 30px; 34 | display: flex; 35 | } 36 | 37 | section { 38 | flex-grow: 1; 39 | margin-right: 45px; 40 | } 41 | 42 | aside { 43 | max-width: 250px; 44 | min-width: 250px; 45 | text-align: center; 46 | } 47 | 48 | h1, 49 | h2, 50 | h3 { 51 | color: #563d7c; 52 | } 53 | 54 | .container { 55 | width: 100%; 56 | max-width: 800px; 57 | padding-left: 15px; 58 | padding-right: 15px; 59 | margin: 0 auto; 60 | } 61 | 62 | article { 63 | margin-bottom: 45px; 64 | } 65 | 66 | .loading { 67 | text-align: center; 68 | font-weight: 700; 69 | color: #563d7c; 70 | margin-top: 30px; 71 | font-size: 30px; 72 | text-transform: uppercase; 73 | } 74 | 75 | .article__header { 76 | display: flex; 77 | margin-bottom: 15px; 78 | } 79 | 80 | .article__top { 81 | flex-grow: 1; 82 | } 83 | 84 | .article__avatar { 85 | display: inline-block; 86 | width: 70px; 87 | height: 70px; 88 | min-width: 70px; 89 | max-width: 70px; 90 | border-radius: 50%; 91 | margin-right: 15px; 92 | background-color: #d5d5d5; 93 | } 94 | 95 | .article__title { 96 | margin-top: 10px; 97 | margin-bottom: 10px; 98 | } 99 | 100 | .article__avatar--skeleton, 101 | .article__title--skeleton, 102 | .article__date--skeleton, 103 | .article__teaser--skeleton span { 104 | background-color: #d5d5d5; 105 | } 106 | 107 | .article__title--skeleton, 108 | .article__date--skeleton, 109 | .article__teaser--skeleton span { 110 | height: 18px; 111 | width: 100%; 112 | } 113 | 114 | .article__title--skeleton { 115 | max-width: 250px; 116 | } 117 | 118 | .article__date--skeleton { 119 | max-width: 100px; 120 | } 121 | 122 | .article__teaser--skeleton span { 123 | display: inline-block; 124 | height: 16px; 125 | margin-bottom: 2px; 126 | } 127 | 128 | .article__teaser--skeleton span:nth-child(3) { 129 | max-width: 230px; 130 | } 131 | --------------------------------------------------------------------------------