A framework for building simple, web-editable websites using Firestore + svelte.
Lately I've been reaching for Firebase as the backend for small websites - it has great free/pay-as-you-go plans and provides a lot of features out of the gate such as hosting (obviously), a database, cloud functions, etc.
On the front end, Svelte has quickly become my go-to for any new projects: it's fast, it (mostly) stays out of my way, and is a treat to use.
The main thing that has been missing is basic CMS functionality: a way to make a quick page edit without my full dev environment, or the need to let non-developers be able to create and edit new pages. This project provides that missing piece.
A firesite-based website is an SPA that stores some or all of its pages in a Firestore collection (table) instead of individual files on disk. Each page is in Markdown (though "raw" HTML is also allowed) to make it easier on non-developers, and images are hosted using Firebase storage.
With the above features in place, it becomes pretty easy to whip together a basic website without too much trouble. For more complex pages with needs beyond what can be easily done via markdown, you (as a developer) can add custom pages as Svelte components, or you can write widgets in Svelte that you (or a non-developer) can than use in any page via an extension to the markdown syntax.
I'm putting this code on github because I'm using it on multiple sites, not because I think it's in some form that is feature-rich enough for everybody. It does what I need, and I'll continue to add to it, but... yeah.
Below are the general steps to follow when creating a new site from scratch. Integrating Firesite into an existing site is doable, but it's generally best to create a new working directory, getting Firesite set up, and then layering in existing files and assets.
mkdir mysite ; cd mysite
mkdir dbsnap src public ; cd src
git clone [email protected]:dfb/firesite.git
cp -r firesite/starter_files/* ..
cd ../functions
firesite.Init({
firebaseConfig:{
apiKey: "...",
authDomain: "...",
},
...
Service accounts
tab, click Generate new private key
and save the file as gcp-prod.json
in your project directory. Protect this file and never check it into source control!npm i --save-dev js-sha3 npm-run-all sass svelte svelte-preprocess svelte-scrollto vite autoprefixer @vitejs/plugin-legacy @sveltejs/vite-plugin-svelte firebase firebase-functions firebase-admin moo nearley remarkable include-media
firebase login:ci
and approve the permission requestsetupenv
and prefix it with export FIREBASE_TOKEN=
. The result will be like:
export FIREBASE_TOKEN=1//0AOpy4-...
gcp-prod.json
file downloaded from the Firebase console:
export GOOGLE_APPLICATION_CREDENTIALS="/c/users/dave/dev/mysite/gcp-prod.json"
# modify this path as neededsetupenv
to .gitignore because you should always keep this file secret and never put it in source control.source setupenv
and then you'll be able to run Firebase commands.firebase use --add
and select your Firebase projectfirebase deploy --only functions,firestore:rules,storage:rules
# deploy the initial cloud functions and access control rules for FirestoreFunctions
item in the left panel, and in the Functions dashboard you will see a main
function listed - this is the function that was just deployed.src/App.svelte
as the value for the prodFuncURL
in the firesite.Init
call.Authentication
item in the left panel, and on the Signin method
tab, make sure the Anonymous
sign-in provider is enabled (adding it if needed).APIs & Services
then the + Enable APIS and services
button.IAM Service Account Credentials API
and enable it if it's not enabled already.IAM & Admin
.Roles
on the left, then + Create role
. Fill in title=BlobSigner
, id=BlobSigner
, roleLaunchStage=General Availability
,+ Add permissions
and in the second search box ('Enter property name or value') type signblob
and choose iam.serviceAccounts.signBlob
, check the checkbox to select it, then click the Add
button.Create
button.Service accounts
and click on the account that does not have 'adminsdk' in its name.Grant access
New principals
box, choose the same service account name.Role
box enter BlobSigner
and click the Save
button.When developing your site, you'll have two shells/consoles open, one to run the Firebase emulators and the other to be a local web server. In the first shell:
source setupenv
firebase emulators:start --only functions,firestore --export-on-exit --import=dbsnap/
This will allow you to test cloud functions locally, and will set up a local Firestore emulator so you don't run against production data. If the command
fails, be sure to correct any issues before proceeding. If it fails due to ports being in use, make sure you don't have this command running in another
window. It's also common for a prior run to have no shut down properly, so kill any errant Java processes (on Windows, look for java.exe
in the Task Manaager).
The first time you run this command, copy the URL displayed on this line:
+ functions[main]: http function initialized (http://localhost:5001/...).
and in src/App.svelte
, paste the URL as the value for the devFuncURL
key in the firesite.Init call
.
In the second shell:
npm run dev
This will start up a local webserver at http://localhost:5000
with Svelte, SASS, and hot module reloading enabled.
When you are ready to push a new version of your site to production:
firebase deploy --only functions
# if you've modified anything in functions/
npm run bd
# runs the packager and then pushes the built files to Firebase hostingfirebase deploy --only firestore:rules
# if you've modified firestore.rules
Currently users have to be added to the database manually (sorry!) using the steps below. Note that since the local and production databases don't share any data, you'll need to add users in both places.
[email protected]
with a password of HappyDayz1234
:>>> import hashlib
>>> hashlib.sha3_256('firesite:[email protected]||HappyDayz1234'.encode('utf8')).hexdigest()
'87396cf24ecac3d95e0ee7dd68c5d4cc9a33ff0574cdd03cf331f6e757de7343'
firesite
in the input string; it is simply for a salt value. You can use a different value for the prefix when generating hashes, but be sure to edit src/App.svelte
's call to firesite.Init
so that it passes passwordSaltPrefix:<your custom value>
in the call.Local development
section above)User
collection and add a document with:email
(type=string), with a value of the user's email addresspasswordHash
(type=string), with a value of the calculated password hashroles
(tyep=map), with an entry of either member
or admin
with a value of 1 (type=number)The ROUTES
list in src/App.svelte
is where you define the organization of your website - what pages are available
and what URLs are used to reach them. For each page, you need to decide if you want it to be a web-editable SitePage
or if you want it to be implemented as a Svelte component.
SitePages can be created and edited through the web (and without access to the dev environment) and support Markdown or "raw" HTML, and can make use of reusable Svelte components you have created. SitePages are a good option for cases where you need to give edit access to a non-developer or to someone who doesn't want to mess with getting a dev environment set up, and/or when you want to be able to make quick edits to a site, including when you're away from your main computer. They are not a good option when your page needs extensive Javascript or you want to make use of the power of Svelte.
Pages implemented as Svelte components offer unlimited power/flexibility, but require development experience. Also, adding new pages or editing these pages requires you to do a new release of your site.
Each entry in the routes table is a Javascript object with:
path
- a string or regex for the page. If you use a regex with named parameters, your component should expose a params
variable to receive any named parameters.comp
- the component that will be rendered for that pageNote that the routes table is ordered and Firesite will use the first match it finds.
As an example, here is a routes table for a site that uses a Svelte component for the landing (index) page, exposes the builtin
admin page, and then implements everything else on the site as SitePages:
js import HomePage from './homepage.svelte'; // you'd implement this as a normal Svelte component const ROUTES = [ {path:'/', comp:HomePage}, {path:'/secret/admin', comp:admin}, {path:new RegExp('.*'), comp:SitePage}, ]
If you want the landing page ('/') to be a SitePage, give it the name index
; Firesite recognizes this as a special case.
As mentioned earlier, a Firesite-based website is actually an SPA, though from the user's perspective this isn't overly obvious - the URLs look like normal URLs (and are not hash-based) and the browser history works properly. The site is an SPA, however, so that navigation among pages doesn't reset our connection to the database or reset our in memory cache of records read from the database. At some point this may change (e.g. use local storage for the cache).
Currently, the builtin admin page is best described as "absurdly cheesy" and can be used to see what pages exist and add new pages, but that's about it.
{widgetname param="x" other="y"}
?prod=1
to page URL