├── .firebaserc.sample ├── .gitattributes ├── .gitignore ├── README.md ├── firebase.json ├── package.json ├── prod ├── client │ ├── assets │ │ ├── index.spa.html │ │ ├── index.ssr.html │ │ ├── server-bundle.json │ │ └── vue-ssr-client-manifest.json │ ├── favicon.ico │ ├── icon.png │ └── site.jpg └── server │ ├── index.js │ ├── package.json │ └── yarn.lock ├── src ├── .editorconfig ├── .eslintrc.js ├── .nuxt │ ├── App.js │ ├── client.js │ ├── components │ │ ├── no-ssr.js │ │ ├── nuxt-child.js │ │ ├── nuxt-error.vue │ │ ├── nuxt-link.js │ │ ├── nuxt-loading.vue │ │ └── nuxt.js │ ├── dist │ │ ├── 2aa56ac0bcbbc141d9ca.js │ │ ├── 6f77ffcd15ddb20c4434.js │ │ ├── LICENSES │ │ ├── a95365ffe4047e6094dd.js │ │ ├── c79fef71b97fb2a1e26e.js │ │ ├── d97ee76b6641d6111716.js │ │ ├── icons │ │ │ ├── icon_120.9qid3ZBUcQn.png │ │ │ ├── icon_144.9qid3ZBUcQn.png │ │ │ ├── icon_152.9qid3ZBUcQn.png │ │ │ ├── icon_192.9qid3ZBUcQn.png │ │ │ ├── icon_384.9qid3ZBUcQn.png │ │ │ ├── icon_512.9qid3ZBUcQn.png │ │ │ └── icon_64.9qid3ZBUcQn.png │ │ ├── index.spa.html │ │ ├── index.ssr.html │ │ ├── manifest.18c4f7f1.json │ │ ├── server-bundle.json │ │ ├── vue-ssr-client-manifest.json │ │ └── workbox.3de3418b.js │ ├── empty.js │ ├── index.js │ ├── loading.html │ ├── middleware.js │ ├── router.js │ ├── server.js │ ├── store.js │ ├── utils.js │ └── views │ │ ├── app.template.html │ │ └── error.html ├── README.md ├── assets │ ├── README.md │ └── styles │ │ └── main.css ├── components │ ├── AppFooter.vue │ ├── AppHeader.vue │ └── Logo.vue ├── layouts │ └── default.vue ├── middleware │ └── .gitkeep ├── nuxt.config.js ├── package.json ├── pages │ ├── index.vue │ ├── page2.vue │ └── page3.vue ├── plugins │ └── .gitkeep ├── static │ ├── favicon.ico │ ├── icon.png │ └── site.jpg ├── store │ └── index.js └── yarn.lock └── yarn.lock /.firebaserc.sample: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "your-project-id" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These settings are for any web project 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | 57 | # code analysis config 58 | .jshintrc text 59 | .jscsrc text 60 | .jshintignore text 61 | .csslintrc text 62 | 63 | # misc config 64 | *.yaml text 65 | *.yml text 66 | .editorconfig text 67 | 68 | # build config 69 | *.npmignore text 70 | *.bowerrc text 71 | 72 | # Heroku 73 | Procfile text 74 | .slugignore text 75 | 76 | # Documentation 77 | *.md text 78 | LICENSE text 79 | AUTHORS text 80 | 81 | 82 | # 83 | ## These files are binary and should be left untouched 84 | # 85 | 86 | # (binary is a macro for -text -diff) 87 | *.png binary 88 | *.jpg binary 89 | *.jpeg binary 90 | *.gif binary 91 | *.ico binary 92 | *.mov binary 93 | *.mp4 binary 94 | *.mp3 binary 95 | *.flv binary 96 | *.fla binary 97 | *.swf binary 98 | *.gz binary 99 | *.zip binary 100 | *.7z binary 101 | *.ttf binary 102 | *.eot binary 103 | *.woff binary 104 | *.pyc binary 105 | *.pdf binary 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/8edb8a95c4c4b3dce71a378aaaf89275510b9cef/Node.gitignore 2 | **/*/sw.* 3 | sw.* 4 | 5 | backups 6 | prod/client/assets/ 7 | prod/server/nuxt/ 8 | .firebaserc 9 | 10 | .DS_Store 11 | **/**/.DS_Store 12 | **/**/node_modules 13 | ProjectNotes.md 14 | # Logs 15 | logs 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (http://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Typescript v1 declaration files 53 | typings/ 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt.js Universal App with SSR via Firebase Functions and Firebase Hosting - **_Nuxt 2 Version_** 2 | 3 | Host a Nuxt Universal app or site by combining Nuxt.js with Firebase Cloud Functions and Hosting. 4 | 5 | [Live Preview](https://nuxt2ssrfire.firebaseapp.com) 6 | 7 | --- 8 | 9 | ## Pre-Setup: Before Installing Any Dependencies 10 | 11 | 1. Obtain a Firebase Project ID to use for this project. [See Overiew Here](#firebase-project-setup) 12 | 13 | 2. Inside this directory, locate the file `.firebaserc.sample`, and do the following: 14 | 15 | - Rename this file to `.firebaserc` 16 | - Inside this file, replace `your-project-id` with your Firebase Project ID. 17 | 18 | --- 19 | 20 | ## Setup 21 | 22 | We will now get everything setup and deployed in 3 commands: 23 | 24 | **Note:** _All of these commands are ran from the root directory_ 25 | 26 | 1. Install Dependencies in all necessary directories in 1 command 27 | 28 | ```bash 29 | yarn setup 30 | # OR 31 | npm run setup 32 | ``` 33 | 34 | 2. Build The Project 35 | 36 | ```bash 37 | yarn build 38 | # OR 39 | npm run build 40 | ``` 41 | 42 | 3. Deploy To Firebase 43 | 44 | ```bash 45 | yarn deploy 46 | # OR 47 | npm run deploy 48 | ``` 49 | 50 | **_Your site should now be live!_** 51 | 52 | --- 53 | 54 | ## Development 55 | 56 | There are 2 development options. 57 | 58 | ### 1. _Without_ Firebase Functions 59 | 60 | This will be like a normal Nuxt development experienced. 61 | 62 | ```bash 63 | yarn dev 64 | ``` 65 | 66 | ### 2. _With_ Firebase Functions 67 | 68 | This uses Firebase's local development tools to test our project 69 | 70 | ```bash 71 | yarn serve 72 | ``` 73 | 74 | --- 75 | 76 | ### Firebase Project Setup 77 | 78 | 1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com). 79 | 80 | 2. Obtain the Firebase Project ID 81 | 82 | ### Features 83 | 84 | - Server-side rendering with Firebase Hosting combined with Firebase Functions 85 | - Firebase Hosting as our CDN for our publicPath (See nuxt.config.js) 86 | 87 | ### Things to know... 88 | 89 | - You must have the Firebase CLI installed. If you don't have it install it with `npm install -g firebase-tools` and then configure it with `firebase login`. 90 | 91 | - If you have errors, make sure `firebase-tools` is up to date. I've experienced many problems that were resolved once I updated. 92 | 93 | * The root directory has a package.json file with several scripts that will be used to optimize and ease getting started and the workflow 94 | 95 | * ALL commands are ran from the root directory 96 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "prod/client", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "function": "nuxtssr" 13 | } 14 | ] 15 | }, 16 | "functions": { 17 | "source": "prod/server" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nuxt-SSR-Firebase", 3 | "version": "0.2.0", 4 | "description": "Nuxt.js 2 app with SSR using Firebase Cloud Functions and Firebase Hosting.", 5 | "license": "MIT", 6 | "author": "David Royer", 7 | "repository": "https://github.com/davidroyer/nuxt-ssr-firebase", 8 | "scripts": { 9 | "dev": "cd \"src\" && yarn dev", 10 | "build": "yarn build:nuxt && yarn clean && yarn copyassets", 11 | "serve": "NODE_ENV=development firebase serve", 12 | "deploy": "firebase deploy", 13 | "predeploy": "yarn build", 14 | "setup": "yarn install && yarn setup:client && yarn setup:server", 15 | "setup:client": "cd \"src\" && yarn install", 16 | "setup:server": "cd \"prod/server\" && yarn install", 17 | "copyassets": "yarn copydist && yarn copystatic", 18 | "copydist": "cp -R prod/server/nuxt/dist/ prod/client/assets", 19 | "copystatic": "cp -R src/static/ prod/client", 20 | "clean": "rimraf prod/client/assets/* && rimraf prod/client/*.*", 21 | "build:nuxt": "cd \"src\" && yarn build" 22 | }, 23 | "devDependencies": { 24 | "cross-env": "^5.0.5", 25 | "rimraf": "^2.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /prod/client/assets/index.spa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ HEAD }} 5 | 6 | 7 | {{ APP }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /prod/client/assets/index.ssr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ HEAD }} 5 | 6 | 7 | {{ APP }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /prod/client/assets/vue-ssr-client-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "publicPath": "/assets/", 3 | "all": [ 4 | "icons/icon_144.9qid3ZBUcQn.png", 5 | "2aa56ac0bcbbc141d9ca.js", 6 | "a95365ffe4047e6094dd.js", 7 | "f4ea9fa3a65fe239f29e.js", 8 | "9e5e48e3aa724b419f04.js", 9 | "LICENSES", 10 | "icons/icon_64.9qid3ZBUcQn.png", 11 | "icons/icon_120.9qid3ZBUcQn.png", 12 | "6f77ffcd15ddb20c4434.js", 13 | "icons/icon_152.9qid3ZBUcQn.png", 14 | "icons/icon_192.9qid3ZBUcQn.png", 15 | "icons/icon_384.9qid3ZBUcQn.png", 16 | "icons/icon_512.9qid3ZBUcQn.png", 17 | "manifest.18c4f7f1.json", 18 | "index.ssr.html", 19 | "index.spa.html" 20 | ], 21 | "initial": [ 22 | "f4ea9fa3a65fe239f29e.js", 23 | "9e5e48e3aa724b419f04.js" 24 | ], 25 | "async": [ 26 | "2aa56ac0bcbbc141d9ca.js", 27 | "a95365ffe4047e6094dd.js", 28 | "6f77ffcd15ddb20c4434.js" 29 | ], 30 | "modules": { 31 | "14502198": [ 32 | 3 33 | ], 34 | "54501242": [ 35 | 3 36 | ], 37 | "7e4ce0de": [ 38 | 3 39 | ], 40 | "5001d4bc": [ 41 | 3 42 | ], 43 | "06501d7c": [ 44 | 3 45 | ], 46 | "155f3b8a": [ 47 | 3 48 | ], 49 | "5b2c5af7": [ 50 | 3 51 | ], 52 | "63a080d5": [ 53 | 3 54 | ], 55 | "3642b0c9": [ 56 | 3 57 | ], 58 | "2eea921d": [ 59 | 3 60 | ], 61 | "5b351356": [ 62 | 3 63 | ], 64 | "51865a20": [ 65 | 3 66 | ], 67 | "70d7c10c": [ 68 | 3 69 | ], 70 | "5d3a0546": [ 71 | 3 72 | ], 73 | "2aaa1a58": [ 74 | 3 75 | ], 76 | "497ae47a": [ 77 | 3 78 | ], 79 | "0f2bc1dc": [ 80 | 3 81 | ], 82 | "1a65a490": [ 83 | 3 84 | ], 85 | "2bea819a": [ 86 | 3 87 | ], 88 | "29040fda": [ 89 | 3 90 | ], 91 | "05090404": [ 92 | 4 93 | ], 94 | "53aff1e4": [ 95 | 4 96 | ], 97 | "60f776b8": [ 98 | 3 99 | ], 100 | "0e249e42": [ 101 | 3 102 | ], 103 | "eef0f7a0": [ 104 | 3 105 | ], 106 | "55b953d8": [ 107 | 3 108 | ], 109 | "41b2099a": [ 110 | 3 111 | ], 112 | "d297055c": [ 113 | 3 114 | ], 115 | "752d63af": [ 116 | 3 117 | ], 118 | "1b6b1c39": [ 119 | 3 120 | ], 121 | "3f1f713c": [ 122 | 3 123 | ], 124 | "439147a2": [ 125 | 3 126 | ], 127 | "6013cf04": [ 128 | 3 129 | ], 130 | "fd81a62a": [ 131 | 3 132 | ], 133 | "b31fdc94": [ 134 | 3 135 | ], 136 | "4200adcd": [ 137 | 3 138 | ], 139 | "6da85d62": [ 140 | 4 141 | ], 142 | "6d7cd40e": [ 143 | 4 144 | ], 145 | "7279b843": [ 146 | 4 147 | ], 148 | "6d78a4c2": [ 149 | 4 150 | ], 151 | "669b2596": [ 152 | 4 153 | ], 154 | "e1f4c75c": [ 155 | 3 156 | ], 157 | "c0fe4470": [ 158 | 3 159 | ], 160 | "ec7efc7a": [ 161 | 3 162 | ], 163 | "58a59a0a": [ 164 | 3 165 | ], 166 | "03caef9e": [ 167 | 3 168 | ], 169 | "6e278281": [ 170 | 3 171 | ], 172 | "22bbc43d": [ 173 | 3 174 | ], 175 | "0d0d21ba": [ 176 | 3 177 | ], 178 | "359f450a": [ 179 | 3 180 | ], 181 | "34703a16": [ 182 | 3 183 | ], 184 | "167720ba": [ 185 | 3 186 | ], 187 | "72db6305": [ 188 | 3 189 | ], 190 | "5bbdd6b3": [ 191 | 3 192 | ], 193 | "00e8b82b": [ 194 | 3 195 | ], 196 | "f7ca66e4": [ 197 | 3 198 | ], 199 | "17ae2e58": [ 200 | 3 201 | ], 202 | "0be49c28": [ 203 | 4 204 | ], 205 | "3adfbcba": [ 206 | 3 207 | ], 208 | "a31de1b2": [ 209 | 3 210 | ], 211 | "49583c5a": [ 212 | 3 213 | ], 214 | "92b4e05a": [ 215 | 3 216 | ], 217 | "60c74a95": [ 218 | 3 219 | ], 220 | "cfbdd46a": [ 221 | 3 222 | ], 223 | "0703591b": [ 224 | 3 225 | ], 226 | "2d2ccf25": [ 227 | 3 228 | ], 229 | "2b986b4a": [ 230 | 3 231 | ], 232 | "58aa25c0": [ 233 | 3 234 | ], 235 | "06147eaf": [ 236 | 3 237 | ], 238 | "4c3c621c": [ 239 | 3 240 | ], 241 | "16f544ec": [ 242 | 3 243 | ], 244 | "2fb3fa5c": [ 245 | 3 246 | ], 247 | "22f9958a": [ 248 | 3 249 | ], 250 | "4e2dd768": [ 251 | 3 252 | ], 253 | "5be0fea3": [ 254 | 3 255 | ], 256 | "650cf044": [ 257 | 3 258 | ], 259 | "84031bfa": [ 260 | 3 261 | ], 262 | "7958b580": [ 263 | 3 264 | ], 265 | "2461170c": [ 266 | 4 267 | ], 268 | "2a1a843a": [ 269 | 4 270 | ], 271 | "ebbf3db4": [ 272 | 4 273 | ], 274 | "43b0463a": [ 275 | 4 276 | ], 277 | "5ec83b28": [ 278 | 3 279 | ], 280 | "6a73df2a": [ 281 | 3 282 | ], 283 | "5baec0b5": [ 284 | 4 285 | ], 286 | "5b976c21": [ 287 | 4 288 | ], 289 | "4885e307": [ 290 | 4 291 | ], 292 | "7e9b7b1b": [ 293 | 4 294 | ], 295 | "0121c386": [ 296 | 4 297 | ], 298 | "ec9540cc": [ 299 | 4 300 | ], 301 | "c29616c6": [ 302 | 4 303 | ], 304 | "6139684e": [ 305 | 4 306 | ], 307 | "4521485a": [ 308 | 4 309 | ], 310 | "4b678f24": [ 311 | 4 312 | ], 313 | "63e0c584": [ 314 | 4 315 | ], 316 | "3327ba76": [ 317 | 4 318 | ], 319 | "4741edf6": [ 320 | 3 321 | ], 322 | "06d92d05": [ 323 | 3 324 | ], 325 | "645a1497": [ 326 | 3 327 | ], 328 | "e0479b9a": [ 329 | 3 330 | ], 331 | "4d3cf23d": [ 332 | 3 333 | ], 334 | "c6091e14": [ 335 | 3 336 | ], 337 | "53dcccb7": [ 338 | 3 339 | ], 340 | "6503bd21": [ 341 | 3 342 | ], 343 | "a56ebff6": [ 344 | 3 345 | ], 346 | "43d23f26": [ 347 | 3 348 | ], 349 | "3ed0803c": [ 350 | 3 351 | ], 352 | "2ff8cc20": [ 353 | 3 354 | ], 355 | "869faff4": [ 356 | 3 357 | ], 358 | "66137f40": [ 359 | 3 360 | ], 361 | "9aaf2016": [ 362 | 3 363 | ], 364 | "a5803654": [ 365 | 3 366 | ], 367 | "12791b0e": [ 368 | 3 369 | ], 370 | "423c9a5c": [ 371 | 3 372 | ], 373 | "a026283a": [ 374 | 3 375 | ], 376 | "15fb62f8": [ 377 | 3 378 | ], 379 | "74670ad2": [ 380 | 3 381 | ], 382 | "47f70eaa": [ 383 | 3 384 | ], 385 | "470823fc": [ 386 | 3 387 | ], 388 | "41a4e7b6": [ 389 | 3 390 | ], 391 | "802a7a7e": [ 392 | 3 393 | ], 394 | "438fda3f": [ 395 | 3 396 | ], 397 | "bf241c02": [ 398 | 3 399 | ], 400 | "622a3f64": [ 401 | 3 402 | ], 403 | "2608ae2e": [ 404 | 3 405 | ], 406 | "24f31105": [ 407 | 3 408 | ], 409 | "7d13e3a9": [ 410 | 3 411 | ], 412 | "58b33d2c": [ 413 | 3 414 | ], 415 | "81865d68": [ 416 | 3 417 | ], 418 | "0822b904": [ 419 | 3 420 | ], 421 | "31c88054": [ 422 | 3 423 | ], 424 | "154ecbd6": [ 425 | 3 426 | ], 427 | "d2841012": [ 428 | 3 429 | ], 430 | "7637e8af": [ 431 | 3 432 | ], 433 | "2e59b793": [ 434 | 3 435 | ], 436 | "cca0889e": [ 437 | 3 438 | ], 439 | "15e47d68": [ 440 | 3 441 | ], 442 | "302b03e2": [ 443 | 3 444 | ], 445 | "691f6574": [ 446 | 3 447 | ], 448 | "2323c05b": [ 449 | 3 450 | ], 451 | "2b0e4b81": [ 452 | 3 453 | ], 454 | "5601553a": [ 455 | 3 456 | ], 457 | "18a001c5": [ 458 | 3 459 | ], 460 | "7fa5a5bf": [ 461 | 3 462 | ], 463 | "2a91d752": [ 464 | 3 465 | ], 466 | "aae6143c": [ 467 | 1 468 | ], 469 | "3cfa209e": [ 470 | 1 471 | ], 472 | "3ce2cc0a": [ 473 | 1 474 | ], 475 | "10a2f4fa": [ 476 | 8 477 | ], 478 | "1086c5f8": [ 479 | 2 480 | ], 481 | "d533015c": [ 482 | 1 483 | ] 484 | } 485 | } -------------------------------------------------------------------------------- /prod/client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidroyer/nuxt2-ssr-firebase/c7fce7b4b83d5b05671beab59cd943539c378b46/prod/client/favicon.ico -------------------------------------------------------------------------------- /prod/client/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidroyer/nuxt2-ssr-firebase/c7fce7b4b83d5b05671beab59cd943539c378b46/prod/client/icon.png -------------------------------------------------------------------------------- /prod/client/site.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidroyer/nuxt2-ssr-firebase/c7fce7b4b83d5b05671beab59cd943539c378b46/prod/client/site.jpg -------------------------------------------------------------------------------- /prod/server/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | const { Nuxt, Builder } = require("nuxt-edge"); 3 | const express = require("express"); 4 | const app = express(); 5 | const config = { 6 | dev: false, 7 | buildDir: "nuxt", 8 | build: { 9 | publicPath: "/assets/" 10 | } 11 | }; 12 | const nuxt = new Nuxt(config); 13 | 14 | function handleRequest(req, res) { 15 | console.log("IN New Nuxt Trial: "); 16 | const isProduction = process.env.NODE_ENV === "development" ? false : true; 17 | if (isProduction) 18 | res.set("Cache-Control", "public, max-age=150, s-maxage=150"); 19 | 20 | try { 21 | nuxt.render(req, res); 22 | } catch (err) { 23 | console.error(err); 24 | } 25 | } 26 | 27 | app.use(handleRequest); 28 | exports.nuxtssr = functions.https.onRequest(app); 29 | -------------------------------------------------------------------------------- /prod/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Nuxt-SSR-Firebase: Nuxt.js with Firebase Functions Production Setup", 4 | "dependencies": { 5 | "express": "^4.15.3", 6 | "firebase-admin": "~5.13.1", 7 | "firebase-functions": "^2.0.0", 8 | "lodash": "^4.17.10", 9 | "nuxt-edge": "^2.0.0-25471736.3b2ed03" 10 | }, 11 | "engines": { 12 | "node": "8" 13 | }, 14 | "private": true 15 | } 16 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | env: { 5 | browser: true, 6 | node: true 7 | }, 8 | // required to lint *.vue files 9 | plugins: [ 10 | 'html' 11 | ], 12 | // add your custom rules here 13 | rules: {}, 14 | globals: {} 15 | } 16 | -------------------------------------------------------------------------------- /src/.nuxt/App.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import NuxtLoading from './components/nuxt-loading.vue' 3 | 4 | import '../assets/styles/main.css' 5 | 6 | 7 | import _6f6c098b from '../layouts/default.vue' 8 | 9 | const layouts = { "_default": _6f6c098b } 10 | 11 | 12 | 13 | export default { 14 | head: {"title":"Nuxtjs SSR on Firebase Functions","meta":[{"charset":"utf-8"},{"name":"viewport","content":"width=device-width, initial-scale=1"},{"hid":"description","name":"description","content":"Nuxt.js project"},{"hid":"mobile-web-app-capable","name":"mobile-web-app-capable","content":"yes"},{"hid":"apple-mobile-web-app-title","name":"apple-mobile-web-app-title","content":"nuxt-ssr-firebase-source"},{"hid":"theme-color","name":"theme-color","content":"#3B8070"},{"hid":"og:type","name":"og:type","property":"og:type","content":"website"},{"hid":"og:title","name":"og:title","property":"og:title","content":"nuxt-ssr-firebase-source"},{"hid":"og:description","name":"og:description","property":"og:description","content":"\u003E Nuxt.js project"}],"link":[{"rel":"icon","type":"image\u002Fx-icon","href":"\u002Ffavicon.ico"},{"rel":"stylesheet","href":"https:\u002F\u002Ffonts.googleapis.com\u002Fcss?family=Roboto"},{"rel":"stylesheet","href":"https:\u002F\u002Fcdn.muicss.com\u002Fmui-0.9.35\u002Fcss\u002Fmui.min.css"},{"rel":"manifest","href":"\u002Fassets\u002Fmanifest.18c4f7f1.json"},{"rel":"shortcut icon","href":"\u002Fassets\u002Ficons\u002Ficon_64.9qid3ZBUcQn.png"},{"rel":"apple-touch-icon","href":"\u002Fassets\u002Ficons\u002Ficon_512.9qid3ZBUcQn.png","sizes":"512x512"}],"style":[],"script":[],"htmlAttrs":{"lang":"en"}}, 15 | render(h, props) { 16 | const loadingEl = h('nuxt-loading', { ref: 'loading' }) 17 | const layoutEl = h(this.layout || 'nuxt') 18 | const templateEl = h('div', { 19 | domProps: { 20 | id: '__layout' 21 | }, 22 | key: this.layoutName 23 | }, [ layoutEl ]) 24 | 25 | const transitionEl = h('transition', { 26 | props: { 27 | name: 'layout', 28 | mode: 'out-in' 29 | } 30 | }, [ templateEl ]) 31 | 32 | return h('div',{ 33 | domProps: { 34 | id: '__nuxt' 35 | } 36 | }, [ 37 | loadingEl, 38 | transitionEl 39 | ]) 40 | }, 41 | data: () => ({ 42 | layout: null, 43 | layoutName: '' 44 | }), 45 | beforeCreate () { 46 | Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt) 47 | }, 48 | created () { 49 | // Add this.$nuxt in child instances 50 | Vue.prototype.$nuxt = this 51 | // add to window so we can listen when ready 52 | if (typeof window !== 'undefined') { 53 | window.$nuxt = this 54 | } 55 | // Add $nuxt.error() 56 | this.error = this.nuxt.error 57 | }, 58 | 59 | mounted () { 60 | this.$loading = this.$refs.loading 61 | }, 62 | watch: { 63 | 'nuxt.err': 'errorChanged' 64 | }, 65 | 66 | methods: { 67 | 68 | errorChanged () { 69 | if (this.nuxt.err && this.$loading) { 70 | if (this.$loading.fail) this.$loading.fail() 71 | if (this.$loading.finish) this.$loading.finish() 72 | } 73 | }, 74 | 75 | 76 | setLayout(layout) { 77 | if (!layout || !layouts['_' + layout]) layout = 'default' 78 | this.layoutName = layout 79 | this.layout = layouts['_' + layout] 80 | return this.layout 81 | }, 82 | loadLayout(layout) { 83 | if (!layout || !layouts['_' + layout]) layout = 'default' 84 | return Promise.resolve(layouts['_' + layout]) 85 | } 86 | 87 | }, 88 | components: { 89 | NuxtLoading 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/.nuxt/client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import middleware from './middleware' 3 | import { createApp, NuxtError } from './index' 4 | import { 5 | applyAsyncData, 6 | sanitizeComponent, 7 | resolveRouteComponents, 8 | getMatchedComponents, 9 | getMatchedComponentsInstances, 10 | flatMapComponents, 11 | setContext, 12 | middlewareSeries, 13 | promisify, 14 | getLocation, 15 | compile, 16 | getQueryDiff 17 | } from './utils' 18 | 19 | const noopData = () => { return {} } 20 | const noopFetch = () => {} 21 | 22 | // Global shared references 23 | let _lastPaths = [] 24 | let app 25 | let router 26 | let store 27 | 28 | // Try to rehydrate SSR data from window 29 | const NUXT = window.__NUXT__ || {} 30 | 31 | 32 | // Setup global Vue error handler 33 | const defaultErrorHandler = Vue.config.errorHandler 34 | Vue.config.errorHandler = function (err, vm, info) { 35 | const nuxtError = { 36 | statusCode: err.statusCode || err.name || 'Whoops!', 37 | message: err.message || err.toString() 38 | } 39 | 40 | // Call other handler if exist 41 | let handled = null 42 | if (typeof defaultErrorHandler === 'function') { 43 | handled = defaultErrorHandler(...arguments) 44 | } 45 | if(handled === true){ 46 | return handled 47 | } 48 | 49 | // Show Nuxt Error Page 50 | if(vm && vm.$root && vm.$root.$nuxt && vm.$root.$nuxt.error && info !== 'render function') { 51 | vm.$root.$nuxt.error(nuxtError) 52 | } 53 | if (typeof defaultErrorHandler === 'function') { 54 | return handled 55 | } 56 | 57 | // Log to console 58 | if (process.env.NODE_ENV !== 'production') { 59 | console.error(err) 60 | } else { 61 | console.error(err.message || nuxtError.message) 62 | } 63 | } 64 | 65 | 66 | // Create and mount App 67 | createApp() 68 | .then(mountApp) 69 | .catch(err => { 70 | if (err.message === 'ERR_REDIRECT') { 71 | return // Wait for browser to redirect... 72 | } 73 | console.error('[nuxt] Error while initializing app', err) 74 | }) 75 | 76 | function componentOption(component, key, ...args) { 77 | if (!component || !component.options || !component.options[key]) { 78 | return {} 79 | } 80 | const option = component.options[key] 81 | if (typeof option === 'function') { 82 | return option(...args) 83 | } 84 | return option 85 | } 86 | 87 | function mapTransitions(Components, to, from) { 88 | const componentTransitions = component => { 89 | const transition = componentOption(component, 'transition', to, from) || {} 90 | return (typeof transition === 'string' ? { name: transition } : transition) 91 | } 92 | 93 | return Components.map(Component => { 94 | // Clone original object to prevent overrides 95 | const transitions = Object.assign({}, componentTransitions(Component)) 96 | 97 | // Combine transitions & prefer `leave` transitions of 'from' route 98 | if (from && from.matched.length && from.matched[0].components.default) { 99 | const from_transitions = componentTransitions(from.matched[0].components.default) 100 | Object.keys(from_transitions) 101 | .filter(key => from_transitions[key] && key.toLowerCase().indexOf('leave') !== -1) 102 | .forEach(key => { transitions[key] = from_transitions[key] }) 103 | } 104 | 105 | return transitions 106 | }) 107 | } 108 | 109 | async function loadAsyncComponents (to, from, next) { 110 | // Check if route path changed (this._pathChanged), only if the page is not an error (for validate()) 111 | this._pathChanged = !!app.nuxt.err || from.path !== to.path 112 | this._queryChanged = JSON.stringify(to.query) !== JSON.stringify(from.query) 113 | this._diffQuery = (this._queryChanged ? getQueryDiff(to.query, from.query) : []) 114 | 115 | 116 | if (this._pathChanged && this.$loading.start) { 117 | this.$loading.start() 118 | } 119 | 120 | 121 | try { 122 | const Components = await resolveRouteComponents(to) 123 | 124 | if (!this._pathChanged && this._queryChanged) { 125 | // Add a marker on each component that it needs to refresh or not 126 | const startLoader = Components.some((Component) => { 127 | const watchQuery = Component.options.watchQuery 128 | if (watchQuery === true) return true 129 | if (Array.isArray(watchQuery)) { 130 | return watchQuery.some((key) => this._diffQuery[key]) 131 | } 132 | return false 133 | }) 134 | if (startLoader && this.$loading.start) { 135 | this.$loading.start() 136 | } 137 | } 138 | 139 | // Call next() 140 | next() 141 | } catch (err) { 142 | err = err || {} 143 | const statusCode = err.statusCode || err.status || (err.response && err.response.status) || 500 144 | this.error({ statusCode, message: err.message }) 145 | this.$nuxt.$emit('routeChanged', to, from, err) 146 | next(false) 147 | } 148 | } 149 | 150 | function applySSRData(Component, ssrData) { 151 | if (NUXT.serverRendered && ssrData) { 152 | applyAsyncData(Component, ssrData) 153 | } 154 | Component._Ctor = Component 155 | return Component 156 | } 157 | 158 | // Get matched components 159 | function resolveComponents(router) { 160 | const path = getLocation(router.options.base, router.options.mode) 161 | 162 | return flatMapComponents(router.match(path), async (Component, _, match, key, index) => { 163 | // If component is not resolved yet, resolve it 164 | if (typeof Component === 'function' && !Component.options) { 165 | Component = await Component() 166 | } 167 | // Sanitize it and save it 168 | const _Component = applySSRData(sanitizeComponent(Component), NUXT.data ? NUXT.data[index] : null) 169 | match.components[key] = _Component 170 | return _Component 171 | }) 172 | } 173 | 174 | function callMiddleware (Components, context, layout) { 175 | let midd = [] 176 | let unknownMiddleware = false 177 | 178 | // If layout is undefined, only call global middleware 179 | if (typeof layout !== 'undefined') { 180 | midd = [] // Exclude global middleware if layout defined (already called before) 181 | if (layout.middleware) { 182 | midd = midd.concat(layout.middleware) 183 | } 184 | Components.forEach(Component => { 185 | if (Component.options.middleware) { 186 | midd = midd.concat(Component.options.middleware) 187 | } 188 | }) 189 | } 190 | 191 | midd = midd.map(name => { 192 | if (typeof name === 'function') return name 193 | if (typeof middleware[name] !== 'function') { 194 | unknownMiddleware = true 195 | this.error({ statusCode: 500, message: 'Unknown middleware ' + name }) 196 | } 197 | return middleware[name] 198 | }) 199 | 200 | if (unknownMiddleware) return 201 | return middlewareSeries(midd, context) 202 | } 203 | 204 | async function render (to, from, next) { 205 | if (this._pathChanged === false && this._queryChanged === false) return next() 206 | // Handle first render on SPA mode 207 | if (to === from) _lastPaths = [] 208 | else { 209 | const fromMatches = [] 210 | _lastPaths = getMatchedComponents(from, fromMatches).map((Component, i) => compile(from.matched[fromMatches[i]].path)(from.params)) 211 | } 212 | 213 | // nextCalled is true when redirected 214 | let nextCalled = false 215 | const _next = path => { 216 | if (from.path === path.path && this.$loading.finish) this.$loading.finish() 217 | if (from.path !== path.path && this.$loading.pause) this.$loading.pause() 218 | if (nextCalled) return 219 | nextCalled = true 220 | next(path) 221 | } 222 | 223 | // Update context 224 | await setContext(app, { 225 | route: to, 226 | from, 227 | next: _next.bind(this) 228 | }) 229 | this._dateLastError = app.nuxt.dateErr 230 | this._hadError = !!app.nuxt.err 231 | 232 | // Get route's matched components 233 | const matches = [] 234 | const Components = getMatchedComponents(to, matches) 235 | 236 | // If no Components matched, generate 404 237 | if (!Components.length) { 238 | // Default layout 239 | await callMiddleware.call(this, Components, app.context) 240 | if (nextCalled) return 241 | // Load layout for error page 242 | const layout = await this.loadLayout(typeof NuxtError.layout === 'function' ? NuxtError.layout(app.context) : NuxtError.layout) 243 | await callMiddleware.call(this, Components, app.context, layout) 244 | if (nextCalled) return 245 | // Show error page 246 | app.context.error({ statusCode: 404, message: 'This page could not be found' }) 247 | return next() 248 | } 249 | 250 | // Update ._data and other properties if hot reloaded 251 | Components.forEach(Component => { 252 | if (Component._Ctor && Component._Ctor.options) { 253 | Component.options.asyncData = Component._Ctor.options.asyncData 254 | Component.options.fetch = Component._Ctor.options.fetch 255 | } 256 | }) 257 | 258 | // Apply transitions 259 | this.setTransitions(mapTransitions(Components, to, from)) 260 | 261 | try { 262 | // Call middleware 263 | await callMiddleware.call(this, Components, app.context) 264 | if (nextCalled) return 265 | if (app.context._errored) return next() 266 | 267 | // Set layout 268 | let layout = Components[0].options.layout 269 | if (typeof layout === 'function') { 270 | layout = layout(app.context) 271 | } 272 | layout = await this.loadLayout(layout) 273 | 274 | // Call middleware for layout 275 | await callMiddleware.call(this, Components, app.context, layout) 276 | if (nextCalled) return 277 | if (app.context._errored) return next() 278 | 279 | // Call .validate() 280 | let isValid = true 281 | Components.forEach(Component => { 282 | if (!isValid) return 283 | if (typeof Component.options.validate !== 'function') return 284 | isValid = Component.options.validate(app.context) 285 | }) 286 | // ...If .validate() returned false 287 | if (!isValid) { 288 | this.error({ statusCode: 404, message: 'This page could not be found' }) 289 | return next() 290 | } 291 | 292 | // Call asyncData & fetch hooks on components matched by the route. 293 | await Promise.all(Components.map((Component, i) => { 294 | // Check if only children route changed 295 | Component._path = compile(to.matched[matches[i]].path)(to.params) 296 | Component._dataRefresh = false 297 | // Check if Component need to be refreshed (call asyncData & fetch) 298 | // Only if its slug has changed or is watch query changes 299 | if (this._pathChanged && Component._path !== _lastPaths[i]) { 300 | Component._dataRefresh = true 301 | } else if (!this._pathChanged && this._queryChanged) { 302 | const watchQuery = Component.options.watchQuery 303 | if (watchQuery === true) { 304 | Component._dataRefresh = true 305 | } else if (Array.isArray(watchQuery)) { 306 | Component._dataRefresh = watchQuery.some((key) => this._diffQuery[key]) 307 | } 308 | } 309 | if (!this._hadError && this._isMounted && !Component._dataRefresh) { 310 | return Promise.resolve() 311 | } 312 | 313 | let promises = [] 314 | 315 | const hasAsyncData = Component.options.asyncData && typeof Component.options.asyncData === 'function' 316 | const hasFetch = !!Component.options.fetch 317 | const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45 318 | 319 | // Call asyncData(context) 320 | if (hasAsyncData) { 321 | const promise = promisify(Component.options.asyncData, app.context) 322 | .then(asyncDataResult => { 323 | applyAsyncData(Component, asyncDataResult) 324 | if(this.$loading.increase) this.$loading.increase(loadingIncrease) 325 | }) 326 | promises.push(promise) 327 | } 328 | 329 | // Call fetch(context) 330 | if (hasFetch) { 331 | let p = Component.options.fetch(app.context) 332 | if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { 333 | p = Promise.resolve(p) 334 | } 335 | p.then(fetchResult => { 336 | if(this.$loading.increase) this.$loading.increase(loadingIncrease) 337 | }) 338 | promises.push(p) 339 | } 340 | 341 | return Promise.all(promises) 342 | })) 343 | 344 | // If not redirected 345 | if (!nextCalled) { 346 | if(this.$loading.finish) this.$loading.finish() 347 | next() 348 | } 349 | 350 | } catch (error) { 351 | if (!error) error = {} 352 | _lastPaths = [] 353 | error.statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500 354 | 355 | // Load error layout 356 | let layout = NuxtError.layout 357 | if (typeof layout === 'function') { 358 | layout = layout(app.context) 359 | } 360 | await this.loadLayout(layout) 361 | 362 | this.error(error) 363 | this.$nuxt.$emit('routeChanged', to, from, error) 364 | next(false) 365 | } 366 | } 367 | 368 | // Fix components format in matched, it's due to code-splitting of vue-router 369 | function normalizeComponents (to, ___) { 370 | flatMapComponents(to, (Component, _, match, key) => { 371 | if (typeof Component === 'object' && !Component.options) { 372 | // Updated via vue-router resolveAsyncComponents() 373 | Component = Vue.extend(Component) 374 | Component._Ctor = Component 375 | match.components[key] = Component 376 | } 377 | return Component 378 | }) 379 | } 380 | 381 | function showNextPage(to) { 382 | // Hide error component if no error 383 | if (this._hadError && this._dateLastError === this.$options.nuxt.dateErr) { 384 | this.error() 385 | } 386 | 387 | // Set layout 388 | let layout = this.$options.nuxt.err ? NuxtError.layout : to.matched[0].components.default.options.layout 389 | if (typeof layout === 'function') { 390 | layout = layout(app.context) 391 | } 392 | this.setLayout(layout) 393 | } 394 | 395 | // When navigating on a different route but the same component is used, Vue.js 396 | // Will not update the instance data, so we have to update $data ourselves 397 | function fixPrepatch(to, ___) { 398 | if (this._pathChanged === false && this._queryChanged === false) return 399 | 400 | Vue.nextTick(() => { 401 | const matches = [] 402 | const instances = getMatchedComponentsInstances(to, matches) 403 | const Components = getMatchedComponents(to, matches) 404 | 405 | instances.forEach((instance, i) => { 406 | if (!instance) return 407 | // if (!this._queryChanged && to.matched[matches[i]].path.indexOf(':') === -1 && to.matched[matches[i]].path.indexOf('*') === -1) return // If not a dynamic route, skip 408 | if (instance.constructor._dataRefresh && Components[i] === instance.constructor && typeof instance.constructor.options.data === 'function') { 409 | const newData = instance.constructor.options.data.call(instance) 410 | for (let key in newData) { 411 | Vue.set(instance.$data, key, newData[key]) 412 | } 413 | } 414 | }) 415 | showNextPage.call(this, to) 416 | 417 | }) 418 | } 419 | 420 | function nuxtReady (_app) { 421 | window._nuxtReadyCbs.forEach((cb) => { 422 | if (typeof cb === 'function') { 423 | cb(_app) 424 | } 425 | }) 426 | // Special JSDOM 427 | if (typeof window._onNuxtLoaded === 'function') { 428 | window._onNuxtLoaded(_app) 429 | } 430 | // Add router hooks 431 | router.afterEach(function (to, from) { 432 | // Wait for fixPrepatch + $data updates 433 | Vue.nextTick(() => _app.$nuxt.$emit('routeChanged', to, from)) 434 | }) 435 | } 436 | 437 | 438 | 439 | async function mountApp(__app) { 440 | // Set global variables 441 | app = __app.app 442 | router = __app.router 443 | store = __app.store 444 | 445 | // Resolve route components 446 | const Components = await Promise.all(resolveComponents(router)) 447 | 448 | // Create Vue instance 449 | const _app = new Vue(app) 450 | 451 | 452 | // Load layout 453 | const layout = NUXT.layout || 'default' 454 | await _app.loadLayout(layout) 455 | _app.setLayout(layout) 456 | 457 | 458 | // Mounts Vue app to DOM element 459 | const mount = () => { 460 | _app.$mount('#__nuxt') 461 | 462 | // Listen for first Vue update 463 | Vue.nextTick(() => { 464 | // Call window.onNuxtReady callbacks 465 | nuxtReady(_app) 466 | 467 | }) 468 | } 469 | 470 | // Enable transitions 471 | _app.setTransitions = _app.$options.nuxt.setTransitions.bind(_app) 472 | if (Components.length) { 473 | _app.setTransitions(mapTransitions(Components, router.currentRoute)) 474 | _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params)) 475 | } 476 | 477 | // Initialize error handler 478 | _app.$loading = {} // To avoid error while _app.$nuxt does not exist 479 | if (NUXT.error) _app.error(NUXT.error) 480 | 481 | // Add router hooks 482 | router.beforeEach(loadAsyncComponents.bind(_app)) 483 | router.beforeEach(render.bind(_app)) 484 | router.afterEach(normalizeComponents) 485 | router.afterEach(fixPrepatch.bind(_app)) 486 | 487 | // If page already is server rendered 488 | if (NUXT.serverRendered) { 489 | mount() 490 | return 491 | } 492 | 493 | // First render on client-side 494 | render.call(_app, router.currentRoute, router.currentRoute, (path) => { 495 | // If not redirected 496 | if (!path) { 497 | normalizeComponents(router.currentRoute, router.currentRoute) 498 | showNextPage.call(_app, router.currentRoute) 499 | // Dont call fixPrepatch.call(_app, router.currentRoute, router.currentRoute) since it's first render 500 | mount() 501 | return 502 | } 503 | 504 | // Push the path and then mount app 505 | router.push(path, () => mount(), (err) => { 506 | if (!err) return mount() 507 | console.error(err) 508 | }) 509 | }) 510 | } 511 | -------------------------------------------------------------------------------- /src/.nuxt/components/no-ssr.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** From https://github.com/egoist/vue-no-ssr 3 | ** With the authorization of @egoist 4 | */ 5 | export default { 6 | name: 'no-ssr', 7 | props: ['placeholder'], 8 | data () { 9 | return { 10 | canRender: false 11 | } 12 | }, 13 | mounted () { 14 | this.canRender = true 15 | }, 16 | render (h) { 17 | if (this.canRender) { 18 | if ( 19 | process.env.NODE_ENV === 'development' && 20 | this.$slots.default && 21 | this.$slots.default.length > 1 22 | ) { 23 | throw new Error('
8 |
An error occurred while rendering the page. Check developer tools console for details.
12 | 13 | 14 |