├── .firebaserc ├── .gitignore ├── .npmrc.example ├── README.md ├── app ├── App.tsx ├── entry-browser.tsx ├── entry-server.tsx ├── global.css ├── routes │ ├── 404.tsx │ ├── 500.tsx │ ├── index.css │ └── index.tsx └── tsconfig.json ├── config └── shared-tsconfig.json ├── firebase.json ├── functions ├── index.ts └── tsconfig.json ├── loaders ├── global.ts ├── routes │ └── index.ts └── tsconfig.json ├── package-lock.json ├── package.json ├── public ├── corner.svg └── favicon.ico ├── remix.config.js └── tsconfig.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | 7 | .firebase 8 | *.log 9 | .npmrc 10 | -------------------------------------------------------------------------------- /.npmrc.example: -------------------------------------------------------------------------------- 1 | # Swap out "" below with the key you get from logging in 2 | # to https://remix.run. If this is a public repo, you'll want to move this 3 | # line into ~/.npmrc to keep it private. 4 | //npm.remix.run/:_authToken= 5 | 6 | # This line tells npm where to find @remix-run packages. 7 | @remix-run:registry=https://npm.remix.run 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remix Starter for Firebase 2 | 3 | This is a starter repo for using [Remix](https://remix.run) with 4 | [Firebase](http://firebase.google.com/). 5 | 6 | ## Development 7 | 8 | After cloning the repo, rename `.npmrc.example` to `.npmrc` and insert the 9 | license key you get from [logging in to your dashboard at 10 | remix.run](https://remix.run). 11 | 12 | > Note: if this is a public repo, you'll probably want to move the line with 13 | > your key into `~/.npmrc` to keep it private. 14 | 15 | Then, install all dependencies using `npm`: 16 | 17 | ```sh 18 | $ npm install 19 | ``` 20 | 21 | Your `@remix-run/*` dependencies will come from the Remix package registry. 22 | 23 | Once everything is installed, start the app in development mode with the 24 | following command: 25 | 26 | ```sh 27 | $ npm run dev 28 | ``` 29 | 30 | This will run a few processes concurrently that will dynamically rebuild as your 31 | source files change. To see your changes, refresh the browser. 32 | 33 | > Note: Hot module reloading is coming soon, which will allow you to see your 34 | > changes without refreshing. 35 | 36 | ## Production 37 | 38 | To run the app in production mode, you'll need to build it first. 39 | 40 | ```sh 41 | $ npm run build 42 | $ npm run deploy 43 | ``` 44 | 45 | This will start a single HTTP server process that will serve the app from the 46 | files generated in the build step. 47 | 48 | ## Documentation 49 | 50 | Detailed documentation for Remix [is available at 51 | remix.run](https://remix.run/dashboard/docs). 52 | 53 | ## Project Structure 54 | 55 | There are 2 main directories you will want to be familiar with: `app` and 56 | `loaders`. 57 | 58 | - The `app` directory contains the major pieces that make up the frontend of 59 | your application. These include the entry points, routes, and CSS files. 60 | Most of the code in this directory runs both on the server _and_ in the 61 | browser. 62 | - The `loaders` directory contains functions that supply data to the frontend. 63 | These functions run only in node.js. 64 | 65 | Remix is responsible for compiling everything in your `app` directory so that it 66 | can run both on the server (to render the HTML needed for the page, aka 67 | server-side rendering or "SSR") and in the browser. It's your responsibility to 68 | compile the code in `loaders`, if needed. 69 | 70 | This project uses TypeScript for type safety. There are two main TypeScript 71 | configs in `app/tsconfig.json` and `loaders/tsconfig.json`. The `tsconfig.json` 72 | in the project root is a "solution" file that just contains 73 | [references](https://www.typescriptlang.org/docs/handbook/project-references.html) 74 | to the other two configs. 75 | 76 | ## Don't want TypeScript? 77 | 78 | The [`no-typescript` 79 | branch](https://github.com/remix-run/starter-express/tree/no-typescript) is a 80 | version of this same starter template that uses plain JavaScript instead of 81 | TypeScript for all code in `app` and `loaders`. 82 | -------------------------------------------------------------------------------- /app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Meta, Scripts, Styles, Routes } from "@remix-run/react"; 3 | 4 | export default function App() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | Corner icon 18 |
19 | 20 | 21 |
22 |

Every site needs a footer

23 |

//2020

24 |
25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/entry-browser.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Remix from "@remix-run/react/browser"; 4 | 5 | import App from "./App"; 6 | 7 | ReactDOM.hydrate( 8 | // @types/react-dom says the 2nd argument to ReactDOM.hydrate() must be a 9 | // `Element | DocumentFragment | null` but React 16 allows you to pass the 10 | // `document` object as well. This is a bug in @types/react-dom that we can 11 | // safely ignore for now. 12 | // @ts-ignore 13 | 14 | 15 | , 16 | document 17 | ); 18 | -------------------------------------------------------------------------------- /app/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOMServer from "react-dom/server"; 3 | import type { EntryContext } from "@remix-run/core"; 4 | import Remix from "@remix-run/react/server"; 5 | 6 | import App from "./App"; 7 | 8 | export default function handleRequest( 9 | request: Request, 10 | responseStatusCode: number, 11 | responseHeaders: Headers, 12 | remixContext: EntryContext 13 | ) { 14 | let markup = ReactDOMServer.renderToString( 15 | 16 | 17 | 18 | ); 19 | 20 | return new Response("" + markup, { 21 | status: responseStatusCode, 22 | headers: { 23 | ...Object.fromEntries(responseHeaders), 24 | "Content-Type": "text/html" 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /app/global.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | /* Remove default padding */ 8 | ul[class], 9 | ol[class] { 10 | padding: 0; 11 | } 12 | 13 | /* Remove default margin */ 14 | body, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | p, 20 | ul[class], 21 | ol[class], 22 | li, 23 | blockquote, 24 | dl, 25 | dd { 26 | margin: 0; 27 | } 28 | 29 | /* Set core body defaults */ 30 | body { 31 | min-height: 100vh; 32 | scroll-behavior: smooth; 33 | text-rendering: optimizeSpeed; 34 | line-height: 1.5; 35 | } 36 | 37 | /* Remove list styles on ul, ol elements with a class attribute */ 38 | ul[class], 39 | ol[class] { 40 | list-style: none; 41 | } 42 | 43 | /* A elements that don't have a class get default styles */ 44 | a:not([class]) { 45 | text-decoration-skip-ink: auto; 46 | } 47 | 48 | /* Make images easier to work with */ 49 | img { 50 | max-width: 100%; 51 | display: block; 52 | } 53 | 54 | /* Natural flow and rhythm in articles by default */ 55 | article > * + * { 56 | margin-top: 1em; 57 | } 58 | 59 | /* Inherit fonts for inputs and buttons */ 60 | input, 61 | button, 62 | textarea, 63 | select { 64 | font: inherit; 65 | } 66 | 67 | body { 68 | position: relative; 69 | } 70 | 71 | .corner { 72 | position: absolute; 73 | right: 0; 74 | top: 0; 75 | } 76 | 77 | footer { 78 | padding: 2rem 10vw; 79 | background-color: #F5F5F5; 80 | display: flex; 81 | justify-content: space-between; 82 | } 83 | 84 | footer p { 85 | font-family: 'Roboto Mono', monospace; 86 | font-size: 16px; 87 | letter-spacing: -1px; 88 | } 89 | -------------------------------------------------------------------------------- /app/routes/404.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function meta() { 4 | return { title: "Ain't nothing here" }; 5 | } 6 | 7 | export default function FourOhFour() { 8 | return ( 9 |
10 |

404

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/routes/500.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function meta() { 4 | return { title: "Shoot..." }; 5 | } 6 | 7 | export default function FiveHundred() { 8 | console.error("Check your server terminal output"); 9 | 10 | return ( 11 |
12 |

500

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/routes/index.css: -------------------------------------------------------------------------------- 1 | h1,h3,a.button { 2 | font-family: 'Rubik', sans-serif; 3 | } 4 | 5 | h1,h3 { 6 | color: rgba(0,0,0, .87); 7 | } 8 | 9 | h4,p,code { 10 | color: rgba(0,0,0, .67); 11 | } 12 | 13 | h4, code { 14 | font-family: 'Roboto Mono', monospace; 15 | } 16 | 17 | code { 18 | background-color: #F5F5F5; 19 | padding: 8px; 20 | } 21 | 22 | h4 { 23 | font-size: 24px; 24 | } 25 | 26 | h1 { 27 | font-size: 56px; 28 | font-weight: 700; 29 | } 30 | 31 | h3 { 32 | font-size: 24px; 33 | font-weight: 500; 34 | margin-bottom: 8px; 35 | } 36 | 37 | h4 { 38 | font-size: 24px; 39 | font-weight: 400; 40 | letter-spacing: 1px; 41 | } 42 | 43 | p { 44 | font-family: 'Roboto', sans-serif; 45 | font-weight: 400; 46 | font-size: 20px; 47 | line-height: 46px; 48 | margin: 8px 0; 49 | } 50 | 51 | .code-block { 52 | background-color: #f5f5f5; 53 | padding: 1rem; 54 | border-radius: 8px; 55 | font-family: Roboto Mono; 56 | letter-spacing: 1px; 57 | line-height: 32px; 58 | font-weight: 400; 59 | color: rgba(0,0,0,.77); 60 | } 61 | 62 | .bold { 63 | font-weight: 700; 64 | } 65 | 66 | a.button { 67 | border: none; 68 | font-weight: 700; 69 | text-decoration: none; 70 | background-color: #004DB0; 71 | padding: .8rem 3rem; 72 | text-transform: uppercase; 73 | color: white; 74 | border-radius: 8px; 75 | letter-spacing: 2px; 76 | text-align: center; 77 | } 78 | 79 | a.button.button-light { 80 | background-color: white; 81 | border: 2px solid #004DB0; 82 | color: #004DB0; 83 | } 84 | 85 | .container { 86 | padding: 6vw 10vw; 87 | position: relative; 88 | max-width: 1160px; 89 | } 90 | 91 | .content-grid { 92 | margin-top: 64px; 93 | display: grid; 94 | gap: 64px 0; 95 | } 96 | 97 | .content-grid nav { 98 | display: flex; 99 | align-items: center; 100 | } 101 | 102 | .content-grid nav > .button { 103 | margin-right: 8px; 104 | } 105 | 106 | @media (max-width: 560px) { 107 | 108 | .content-grid nav { 109 | display: flex; 110 | flex-direction: column; 111 | justify-content: center; 112 | align-items: unset; 113 | } 114 | 115 | .content-grid nav > .button { 116 | margin-right: 0px; 117 | margin-top: 8px; 118 | width: 100%; 119 | } 120 | 121 | 122 | } -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function meta() { 4 | return { 5 | title: "Firebase & Remix", 6 | description: "How to get started" 7 | }; 8 | } 9 | 10 | export function headers() { 11 | return { 12 | "cache-control": "public, max-age=3600, s-max-age=86400", 13 | }; 14 | } 15 | 16 | export default function Index() { 17 | return ( 18 |
19 | 20 |
21 |

22 | Firebase & Remix 23 |

24 |

25 | Now with extra TypeScript! 26 |

27 |
28 | 29 |
30 | 31 | 39 | 40 |
41 |

What are the build & deploy commands?

42 |
43 |
44 | npm run dev # develop 45 |
46 |
47 | npm run build # build 48 |
49 |
50 | npm run deploy # deploy 51 |
52 |
53 |
54 | 55 |
56 |

How does it work?

57 |

58 | Firebase serves your Remix app with Firebase Hosting for static assets and Cloud Functions for the Remix server. Remix’s focus on caching works perfect with this system. The cache-control header controls how long Firebase Hosting will cache the content in the CDN before having to run the server function again. 59 |

60 | 61 | {/* This is so ugly but it's fast, so there's that */} 62 |
63 |
export function headers() {'{'}
64 |
  return {'{'}
65 |
    "cache-control": "public, max-age=3600, s-max-age=86400",
66 |
  {'}'};
67 |
{'}'}
68 |
69 |
70 | 71 |
72 |

What do I need to know about the config?

73 |

74 | Firebase Hosting deploys the static assets located in the public directory. The main entry in package.json is set to build/index.js which is Cloud Functions will run when deployed. The functions/index.ts will compile to the build/index.js file. 75 |

76 |
77 | 78 |
79 | 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "compilerOptions": { 4 | // The Remix compiler takes care of compiling everything in the `app` 5 | // directory, so we don't need to emit anything with `tsc`. 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /config/shared-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "moduleResolution": "node", 6 | "target": "es2019", 7 | "strict": true, 8 | "skipLibCheck": true 9 | }, 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": "npm --prefix \"$RESOURCE_DIR\" run build", 4 | "source": "." 5 | }, 6 | "hosting": { 7 | "public": "public", 8 | "ignore": [ 9 | "firebase.json", 10 | "**/.*" 11 | ], 12 | "rewrites": [ 13 | { 14 | "source": "**", 15 | "function": "remixServer" 16 | } 17 | ] 18 | }, 19 | "emulators": { 20 | "functions": { 21 | "port": 5001 22 | }, 23 | "hosting": { 24 | "port": 5000 25 | }, 26 | "ui": { 27 | "enabled": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | const { createRequestHandler: remix } = require("@remix-run/express"); 3 | 4 | export const remixServer = functions.https.onRequest(remix()); 5 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "include": ["**/*"], 4 | "compilerOptions": { 5 | "outDir": "../build", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /loaders/global.ts: -------------------------------------------------------------------------------- 1 | import type { DataLoader } from "@remix-run/core"; 2 | 3 | let loader: DataLoader = async () => { 4 | return { 5 | date: new Date() 6 | }; 7 | }; 8 | 9 | export = loader; 10 | -------------------------------------------------------------------------------- /loaders/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { DataLoader } from "@remix-run/core"; 2 | 3 | let loader: DataLoader = async () => { 4 | return { 5 | message: "this is awesome 😎" 6 | }; 7 | }; 8 | 9 | export = loader; 10 | -------------------------------------------------------------------------------- /loaders/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../config/shared-tsconfig.json", 3 | "include": ["**/*"], 4 | "compilerOptions": { 5 | "outDir": "../build/loaders", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-starter-firebase", 3 | "version": "1.0.0", 4 | "main": "./build/index.js", 5 | "scripts": { 6 | "build": "cross-env NODE_ENV=production remix build && tsc -b", 7 | "dev": "concurrently \"remix run\" \"tsc -b -w\" \"firebase emulators:start\"", 8 | "deploy": "firebase deploy" 9 | }, 10 | "dependencies": { 11 | "@mdx-js/react": "^1.6.21", 12 | "@remix-run/cli": "^0.6.2", 13 | "@remix-run/express": "^0.6.2", 14 | "@remix-run/loader": "^0.6.2", 15 | "@remix-run/react": "^0.6.2", 16 | "express": "^4.17.1", 17 | "firebase-admin": "^9.4.1", 18 | "firebase-functions": "^3.11.0", 19 | "morgan": "^1.10.0", 20 | "react": "^16.14.0", 21 | "react-dom": "^16.14.0", 22 | "react-router": "^6.0.0-beta.0", 23 | "react-router-dom": "^6.0.0-beta.0" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "^16.9.53", 27 | "@types/react-dom": "^16.9.8", 28 | "concurrently": "^5.3.0", 29 | "cross-env": "^7.0.2", 30 | "firebase-tools": "^8.16.2", 31 | "nodemon": "^2.0.5", 32 | "typescript": "^4.0.3" 33 | }, 34 | "engines": { 35 | "node": "12" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/corner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Path Copy 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davideast/remix-starter-firebase/549b27f9848240fd4feaa6001f3ee0894f5a0e73/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * The path to the `app` directory, relative to remix.config.js. Defaults to 4 | * "app". All code in this directory is part of your app and will be compiled 5 | * by Remix. 6 | * 7 | */ 8 | appDirectory: "app", 9 | 10 | /** 11 | * A hook for defining custom routes based on your own file conventions. This 12 | * is not required, but may be useful if you have custom/advanced routing 13 | * requirements. 14 | */ 15 | // routes(defineRoutes) { 16 | // return defineRoutes(route => { 17 | // route( 18 | // // The URL path for this route. 19 | // "/pages/one", 20 | // // The path to this route's component file, relative to `appDirectory`. 21 | // "pages/one", 22 | // // Options: 23 | // { 24 | // // The path to this route's data loader, relative to `loadersDirectory`. 25 | // loader: "...", 26 | // // The path to this route's styles file, relative to `appDirectory`. 27 | // styles: "..." 28 | // } 29 | // ); 30 | // }); 31 | // }, 32 | 33 | /** 34 | * The path to the `loaders` directory, relative to remix.config.js. Defaults 35 | * to "loaders". The loaders directory contains "data loaders" for your 36 | * routes. 37 | */ 38 | loadersDirectory: "build/loaders", 39 | 40 | /** 41 | * The path to the browser build, relative to remix.config.js. Defaults to 42 | * `public/build`. The browser build contains all public JavaScript and CSS 43 | * files that are created when building your routes. 44 | */ 45 | browserBuildDirectory: "public/build", 46 | 47 | /** 48 | * The URL prefix of the browser build with a trailing slash. Defaults to 49 | * `/build/`. 50 | */ 51 | publicPath: "/build/", 52 | 53 | /** 54 | * The path to the server build directory, relative to remix.config.js. 55 | * Defaults to `build`. The server build is a collection of JavaScript modules 56 | * that are created from building your routes. They are used on the server to 57 | * generate HTML. 58 | */ 59 | serverBuildDirectory: "build/app", 60 | 61 | /** 62 | * The port to use when running `remix run`. Defaults to 8002. 63 | */ 64 | devServerPort: 8002 65 | }; 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "app" }, 5 | { "path": "loaders" }, 6 | { "path": "functions" } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------