I've been spending about two days scouring the net on trying to find the optimal way to integrate Sapper with Firebase. It's not as easy as it sounds.
What is Sapper?
Sapper is a framework for building extremely high-performance web apps. It is a compiler actually. All your code gets built and optimized ready for production.
It's built on Svelte.js. Sapper has many cool features and I recommend you check the docs out Sapper. The Framework allows for SSR capabilities, but can also generate a static site with npm run export
.
What is Firebase
Firebase is Google's platform for "Building apps fast, without managing infrastructure". Basically, in a few commands you can have your static site hosted, hooked up to a database and have authentication readily available.
Other than static sites, Firebase also explores the realm of "serverless" by way of "Functions". These are basically small pieces of logic that you can call either from your app, or when some sort of update in terms of authentication or database occurs.
Functions are useful for:
- Getting sensitive logic out of the client
- Performing some action on a piece of data (eg. sanitizing) before it gets inserted into the database.
- In our case, helping our code serve up a Server Side Rendered application
Benefits of server-side
- SEO: Crawlers can better index your pre-generated markup.
- PERFORMANCE: You don't need to serve up bloated assets and you can do some server side caching.
Why the combination of Firebase x Sapper?
Firebase development is really fast and easy, so is Sapper's. I thought: "Why not best of both worlds?". I can have all the niceties of Firebase authentication, database and CDN hosting - along with the speed of development, size of app and general awesomeness of Sapper.
Let's get started
First, we'll need to install the necessary dependencies. (I'm assuming that you already have Node.js version >= 10.15.3 and the accommodating NPM version on your system).
Installing Sapper
Sapper uses a tool called degit, which we can use via npx
:
$ npx degit sveltejs/sapper-template#rollup sapper_firebase
$ cd sapper_firebase && npm install # install dependencies
Adding firebase to the project
- Note:
You will need the Firebase CLI tool for this:
npm i -g firebase-tools@latest
Before the next step:
You need to go to Firebase and log in to a console. Here you have to create a new Firebase project. When the project is created, on the home page click the </> button to add a new web-app. Name it whatever you like.
Now we can proceed.
# in /sapper_firebase
$ firebase login # make sure to login to your console account
$ firebase init functions hosting
This will return a screen where you can select new project:
Select a default Firebase project for this directory:
❯ sapper-firebase (sapper-firebase) # select your project
Follow the steps:
? What do you want to use as your public directory? static
? Configure as a single-page app (rewrite all urls to /index.html)? N
? Choose your language: Javascript.
? Do you want to use ESLint to catch probable bugs and enforce style? N
? Do you want to install dependencies with npm now? Y
Then let your functions dependencies install.
Changing our firebase.json
The goal we want to achieve by changing the firebase.json is to:
- Serve our project's static files through Firebase hosting.
- Redirect each request to the Function that we're going to create (I'll explain this in detail).
Here's the updated firebase.json:
{
"hosting": {
"public": "static",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [{
"source": "**",
"function": "ssr"
}]
}
}
The rewrite is checking for requests under any route, and basically letting the function named 'ssr' handle it.
Edit: I'm afraid there's more.. Recently an error pops up if the firebase-admin
dependency in functions/package.json is greater than 7, so just set it to:
"firebase-admin": "^7.0.0"
Remember to:
cd functions && npm install
So let's create the 'ssr' function:
/sapper_firebase/functions/index.js
const functions = require('firebase-functions');
exports.ssr = functions.https.onRequest((_req, res) => res.send('hello from function'));
This is only temporary so we can test our function. We can do so by running:
$ firebase serve
if you get this message:
⚠ Your requested "node" version "8" doesn't match your global version "10"
You can go to functions/package.json
and set:
"engines": {
"node": "10"
}
Other than that, after running firebase serve
and going to http://localhost:5000, you should see this:
Testing the function
We're seeing that because Firebase created an index.html
file in our static
floder. We can safely delete it and we can go to http://localhost:5000/ and should now see:
Hoorah!
Now that we've proven our function to work and redirect requests, we need to mess with Sapper a bit, to get our function to run our server. Let's start off by editing the src/server.js
file, our goals are:
- We want to export the sapper middleware that is generated on build.
- We want to only run the local server on
npm run dev
.
src/server.js
import sirv from 'sirv';
import polka from 'polka';
import compression from 'compression';
import * as sapper from '@sapper/server';
const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';
if (dev) {
polka() // You can also use Express
.use(
compression({ threshold: 0 }),
sirv('static', { dev }),
sapper.middleware()
)
.listen(PORT, err => {
if (err) console.log('error', err);
});
}
export { sapper };
We can test if npm run dev
works by letting it build and visit http://localhost:3000. It should show a picture of Borat (Standard Sapper humor).
But this is still only our Sapper server, we want to run it from our Firebase function. First we need to install express:
$ cd functions
$ npm install express sirv compression polka
Note: 'sirv' 'compression' and 'polka' are required, because of the built directory that depends on it. We'll only use express in our function though. But if we exclude the others, our function will fail on deploy.
After installing express and the other dependencies, we first want to tweak our workflow to build a copy of the project into functions. We can do this by editing the npm scripts:
Windows users: please use the guide at the end of the post for your scripts
...
"scripts": {
"dev": "sapper dev",
"build": "sapper build --legacy && cp -R ./__sapper__/build ./functions/__sapper__",
"prebuild": "rm -rf functions/__sapper__/build && mkdir -p functions/__sapper__/build",
"export": "sapper export --legacy",
"start": "npm run build && firebase serve",
"predeploy": "npm run build",
"deploy": "firebase deploy",
"cy:run": "cypress run",
"cy:open": "cypress open",
"test": "run-p --race dev cy:run"
},
...
This will copy all the necesarry files to the functions folder before hosting or serving them locally.
Try it with:
$ npm start # now using firebase
You should see the message from earlier!
Serving our Sapper app via Functions and express
We can plug an express app into our function, use our imported Sapper middleware on express and serve our SSR app seemlessly. The static folder is also being served via the very fast Firebase CDN.
functions/index.js
const functions = require('firebase-functions');
const express = require('express');
// We have to import the built version of the server middleware.
const { sapper } = require('./__sapper__/build/server/server');
const app = express().use(sapper.middleware());
exports.ssr = functions.https.onRequest(app);
Hosting your Sapper Firebase SSR app locally
All you have to do now is:
$ npm start
AAANND...
HOORAAAH
If you see this image on http://localhost:5000 your app is being served by a local firebase functions emulator!
To confirm that it is SSR, just reload a page and check the page source, all the markup should be prerenderd on initial load. Also check out your terminal, you should see all kinds of requests as you navigate your app!
Hosting your app
Because of our neat NPM scripts hosting/deploying is as easy as:
$ npm run deploy
This will take a while, to get your files and functions up. But here's my version online SSRing like a boss.
Windows users
You will need to do some extra stuff to get this working...
- Add some npm packages to help with removing and copying stuff:
npm install -D rimraf mkdirp ncp
The -D flag adds it to dev dependencies, since that's where we need it. Change your scripts to this:
...
"build": "sapper build --legacy && ncp ./__sapper__/build ./functions/__sapper__/build",
"prebuild": "rimraf - functions/__sapper__/build && mkdirp functions/__sapper__/build",
...
These solutions are hacky and this post might bite me in the future due to rapid changes in sapper and firebase stuff. Thank you to Diogio Marques for requesting this in comments.
Thank you
This is one of my first posts that I actually feel could help someone out, since it took me hours to figure out (Maybe I'm just slow). But I typed it out since I can't make videos on my Youtube Channel, because of time issues.
But if you did enjoy this, or have questions, chat to me on twitter @eckhardtdreyer. Until next time.
Top comments (34)
Amazing work. For some reason, I'm not getting any images or styling when I run
npm start
. Everything renders correctly withnpm run dev
, but no luck on the firebase side.Any thoughts?
Not a solution but a head relief:
Images and styling will work after running
npm deploy
and visit the hosted urlDon't know the exact reason, but probably due to firebase hosting not mounting locally at /
In my Firebase Console, if I click on the link under "Hosting" the site seems to be loading just fine. For some reason, I'm not getting anything but html on my localhost:5000 when I run
npm start
.Mm, difficult to say, do you have a repo maybe? Is your firebase.json set like in the article? And are your external css and image files in your static folder?
repo: github.com/mortscode/sapper-firebase
The issue in my browser console is that all of the asset urls are like so:
http://localhost:5000/[firebase-project-name]/us-central1/ssr/global.css
. They're all returning 404's.Is there a location in the app where I should be defining that path?
One more thing, the command
firebase init functions hosting
didn't give me the hosting options in the console. I had to runfirebase init hosting
separately.works for me
const functions = require("firebase-functions");
// We have to import the built version of the server middleware.
const { sapper } = require("./sapper/build/server/server");
const middleware = sapper.middleware();
exports.ssr = functions.https.onRequest((req, res) => {
req.baseUrl = '';
middleware(req, res);
});
Hi Mort,
So sorry for the late reply. Have been taking a screen break.
Have you figured it out? I will look at your repo in my breaks.
Regards
I can't seem to replicate your problem, my urls are correct out of the box. I'll have to spend time finding the issue. I did however notice something else regarding versions of dependencies in the ./functions folder. It seems that I have to use the exact versions in package.json in functions here github.com/Eckhardt-D/sapper-fireb.... Strange. I'll explore. Sorry I don't have an off-the-cuff response.
I'm having the exact same issue as Mort. I'm using newest versions in the functions package.json, as I got an error trying to use firebase admin 7.0.0. Once built and uploaded it works fine, but on localhost:5000 the firebase project name and useast1 time zone stuff shows up in the urls and causes 404s.
focus-at's solution worked for me, I'm using latest firebase-admin. I suspect this is a problem with the baseUrl var
Hi,
Same issue here with latest versions. Any fix?
focus-at workaround worked for me as well.
Thanks very much for breaking this down.
I also tried following this video,
youtu.be/fxfFMn4VMpQ
and found the workflow a bit more manageable when creating a Firebase project first, and then adding in a new Sapper project. I got through it without any issues, on Windows. Would have much preferred it to be a write-up like yours though, so here's a summary:
Create a new empty folder, then navigate to it in the VS Code terminal.
firebase init
functions/static
(Sapper project will go into functions folder)Move or rename
package.json
, and delete.gitignore
(sapper will create these for us later)cd functions
npx degit sveltejs/sapper-template#rollup --force
Copy contents of
scripts
block (Firebase commands) from oldpackage.json
intoscripts
block of newpackage.json
that was generated by Sapper.Rename Firebase's
start
command tofb_start
.Copy entire
engines
block from old to newpackage.json
, and change node version to 10.Copy over contents of
dependencies
anddevDependencies
blocks.Delete old
package.json
, once all Firebase stuff is moved over, and save the new Sapper one.Remove polka from dependencies in
package.json
.npm install --save express
npm install
server.js
:const expressServer = express()
....listen
toif (dev) { expressServer.listen
...}
export { expressServer }
index.js
:const {expressServer} = require('./__sapper__/build/server/server')
exports.ssr = functions.https.onRequest(expressServer);
npm run build
npm run dev
localhost:3000 will show the Firebase default
index.html
fromstatic
folder, which can be deleted.Page reload will bring up Sapper project.
firebase.json
:"rewrites": [ { "source": "**", "function": "ssr" }]
npm run build
firebase deploy
Visit app and click around, refresh to verify functionality.
Try Postman, send a GET to your project URL.
In output, look for confirmation that content is SSR.
package.json
:"deploy": "npm run build && firebase deploy"
Nav.svelte
:li
to the navbar, for a new pageroutes
:.svelte
page, and add some quick HTML contentnpm run deploy
Verify new content shows.
Run audit from Chrome dev tools.
trying to follow this tutorial. But when I try to run the final setup of firebase functions with sapper, I get this error. Any idea why?
Error: ENOENT: no such file or directory, open '__sapper__/build/build.json'
I have not had this error. Keep in mind this is an old post and perhaps things are outdated. I don't keep this post updated, but I'll consider doing an edit to address all the questions here. Have you checked out the repo at github.com/Eckhardt-D/sapper-fireb... ? Perhaps you can find something there.
I tried running the repo but I'm still getting the same error. Maybe I did something wrong with my setup. I'll find a more recent tutorial for now.
Thanks for your time!
I'll have a thorough look at the repo this weekend and update the post accordingly. Sorry I can't give you a fast answer. Hope you get it sorted, if not maybe check in here the weekend or watch the repo for changes :).
You have to get build before
npm start
command. You shouldnpm run build
in the "functions" folder.Trying to get this to work but I'm getting stuck in the editing the npm scripts.
First, i'm assuming we need to edit the npm scripts from sapper (so not the package.json from inside functions), secondly, when I edit the scripts, the prebuid doesn't run, there are two errors; the first says that rm is not a valid command (fair enough, changed to del) then it didn't like the mkdir -p so I removed the -p. At the end it still doesn't run and says
Invalid switch - "__sapper__".
, Any idea why? Thank you for the article anyway!Hi!
It would really be easier for me if I can get a link to a github repo and explore myself so I can give a complete answer.
I’d like to update this post if there were any changes to the workflow because of updates etc.
Also what OS are you running? I haven’t tested this on all platforms. You would be a huge help here!
Thanks.
Hello!
Sorry for the delay. So here you go: gitlab.com/JoFont/sapper-firebase-...
The repo has a Readme with the same info I wrote in the first comment along with some images.
I'm running Windows 10. Thank you very much for you help, and have a great day!
Hi! No problem.
I just cloned your repo and holy ... is Windows copy and folder deletion a pain... Here's my solution (Also adding to post):
npm install -D rimraf mkdirp ncp
The -D flag adds it to dev dependencies, since that's where we need it. Change your scripts to this:
I'm afraid there's more.. An error pops up if the
firebase-admin
dependency in functions/package.json is greater than 7, so just set it to:Remember to:
cd functions && npm install
These solutions are hacky and this post might bite me in the future due to rapid changes in sapper and firebase stuff. But this got it working for me on Windows 10 using your repository. Regards!
Ok! So we do have success with your changes!
Some notes:
Couldn't replicate error with firebase-admin version. Working fine with version 8.2.0, actually if I were to set it to 7.0.0, firebase throws an error.
npm start
does work but as someone mentioned earlier, I also do not get styles and assets. No issues I think, because you don't really neednpm start
, just usenpm run dev
for development andnpm run deploy
when deploying to Firebase.Finally, this does indeed look like a very half-arsed way to make Firebase deliver Sapper SSR, because I feel like it's one update from Firebase or one dependency away from breaking (at least on windows). So perhaps either thinking on a more stable way of implementing this or waiting for firebase for a proper Node.js server would be the way. Either way at least for now it works, though I'll probably stick with AWS for production runs.
Thank you very much @eckhardtd for the tutorial! Have a great one!
Brilliant article!!!
I'm really thinking a lot about SSR vs static-site [sapper export].
With Firebase we can host the static files of the site & still use user authentication for login etc.
Which makes me wonder why shouldn't I just serve a static site [which is MUCH faster than SSR]?!
Hey, thank you! And yes. There is a big movement towards static or even ‘JAMstack’, personally I’d use static as much as possible, since it almost always fulfills the needs of my clients. But sometimes I need more complicated server architecture and prefer not having to manage client and server explicitly. With Sapper or Nuxt I can manage both in one project. All up to preference though.
Oh. Warning:
"Firebase projects on the Spark plan can make only outbound requests to Google APIs. Requests to third-party APIs fail with an error. For more information about upgrading your project, see Pricing."
I followed all the guide just to find that I can't use it. Nice writing anyway.
Thanks a lot for the step-by-step guide.
I created a small rss reader with svelte/sapper, in which I have implemented a backend-api to get and delete a list representing the rss feed. How do I get the backend to work as well? Currently I get 404s.
For the purposes of handling all requests to the /api, i created an
api.js
:In my server.js I import the apiRouter form api.js and simply added it as middleware:
You can find the repo (branch) here: github.com/basti-n/rss-reader-sapp...
An alternative approach is to use Firebase Hosting for static assets and Cloud Run for the server-side part, instead of Cloud Functions. I recently wrote about that in detail, including a template repo.
I'd love to hear feedback if anyone gets a chance to review the article.
mikenikles.com/blog/firebase-hosti...
Has someone already setup an API proxy with this Firebase/Sapper configuration ?
Would be useful to call the local instance of functions when in dev mode.
brilliant! Thanks for writing it up