├── .dockerignore
├── .env.example
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── deploy.yml
│ ├── refresh-all-content.yml
│ └── refresh-content.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── Dockerfile
├── README.md
├── app
├── assets
│ └── icons
│ │ ├── github.svg
│ │ ├── light-bulb.svg
│ │ ├── moon.svg
│ │ └── twitter.svg
├── components
│ ├── blog-item.tsx
│ ├── blog-list.tsx
│ ├── client-only.tsx
│ ├── footer.tsx
│ ├── link-or-anchor.tsx
│ ├── nav-link.tsx
│ ├── nav.tsx
│ └── theme-toggle.tsx
├── entry.client.tsx
├── entry.server.tsx
├── hooks
│ └── use-hydrated.ts
├── model
│ ├── content-state.server.ts
│ └── content.server.ts
├── root.tsx
├── routes
│ ├── _action
│ │ └── set-theme.ts
│ ├── _content
│ │ ├── refresh-content.ts
│ │ ├── refresh-content[.]json.ts
│ │ └── update-content.ts
│ ├── blog.$slug.tsx
│ ├── blog.rss[.]xml.ts
│ ├── blog.tsx
│ ├── healthcheck.ts
│ └── index.tsx
├── types.ts
└── utils
│ ├── compile-mdx.server.ts
│ ├── db.server.ts
│ ├── github.server.ts
│ ├── mdx.server.ts
│ ├── misc.ts
│ ├── p-queue.server.ts
│ ├── seo.ts
│ ├── theme-session.server.ts
│ └── theme.tsx
├── content
└── index.ts
├── cypress.json
├── cypress
├── .eslintrc.js
├── e2e
│ └── smoke.ts
├── fixtures
│ └── example.json
├── plugins
│ └── index.ts
├── support
│ └── index.ts
└── tsconfig.json
├── fly.toml
├── images
├── fly-sqlite-arch (light).png
└── fly-sqlite-arch.png
├── mocks
├── github.ts
├── index.js
└── start.ts
├── others
├── build-info.js
├── is-deployable.js
├── new-blog.ts
├── refresh-all-content.js
├── refresh-content.js
├── refresh-on-content-change.ts
└── utils.js
├── package.json
├── prisma
└── schema.prisma
├── public
├── favicon.ico
└── fonts
│ ├── Poppins-Bold.ttf
│ └── Poppins-Regular.ttf
├── remix.config.js
├── remix.env.d.ts
├── remix.init
├── index.js
├── package-lock.json
└── package.json
├── server
└── index.ts
├── start_with_migrations.sh
├── styles
└── app.css
├── tailwind.config.js
├── test
└── setup-test-env.ts
├── tsconfig.json
└── vitest.config.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | build
4 | public/build
5 | .env
6 | prisma/*.db
7 |
8 | content/
9 | mocks/
10 |
11 | app/styles
12 | app/**/*.ignore.*
13 |
14 | .eslintrc.js
15 | .eslintignore
16 |
17 | .prettierrc
18 | .prettierignore
19 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=file:./data.db
2 | REFRESH_TOKEN=my-secret
3 | GITHUB_TOKEN=my-secret
4 | SESSION_SECRETS=my-secret
5 | GITHUB_REPOSITORY=owner/repo
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache/
3 | build/
4 | public/build/
5 | **/*.js
6 | **/*.ignore.*
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@types/eslint').Linter.BaseConfig}
3 | */
4 | module.exports = {
5 | extends: [
6 | '@remix-run/eslint-config',
7 | '@remix-run/eslint-config/node',
8 | '@remix-run/eslint-config/jest',
9 | 'prettier',
10 | ],
11 | // We're using vitest which has a very similar API to jest
12 | // (so the linting plugins work nicely), but we have to
13 | // set the jest version explicitly.
14 | settings: {
15 | jest: {
16 | version: 27,
17 | },
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Deploy
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request: {}
7 |
8 | jobs:
9 | changes:
10 | name: 🔍 Determine Changes
11 | runs-on: ubuntu-latest
12 | outputs:
13 | DEPLOYABLE: ${{steps.changes.outputs.DEPLOYABLE}}
14 | steps:
15 | - name: 🛑 Cancel Previous Runs
16 | uses: styfle/cancel-workflow-action@0.9.1
17 |
18 | - name: ⬇️ Checkout repo
19 | uses: actions/checkout@v2
20 | with:
21 | fetch-depth: '50'
22 |
23 | - name: 🛠 Setup node
24 | uses: actions/setup-node@v2
25 | with:
26 | node-version: 16
27 |
28 | - name: ⚙️ Determine Changes
29 | id: changes
30 | run: >-
31 | echo ::set-output name=DEPLOYABLE::$(node ./others/is-deployable.js)
32 | env:
33 | FLY_APP_NAME: ${{ secrets.FLY_APP_NAME }}
34 |
35 | - name: ❓ Deployable
36 | run: >-
37 | echo "DEPLOYABLE: ${{steps.changes.outputs.DEPLOYABLE}}"
38 |
39 | vitest:
40 | name: ⚡ Vitest
41 | runs-on: ubuntu-latest
42 | needs: [changes]
43 | if: ${{ needs.changes.outputs.DEPLOYABLE == 'true' }}
44 | steps:
45 | - name: 🛑 Cancel Previous Runs
46 | uses: styfle/cancel-workflow-action@0.9.1
47 |
48 | - name: ⬇️ Checkout repo
49 | uses: actions/checkout@v2
50 | with:
51 | fetch-depth: '50'
52 |
53 | - name: 🛠 Setup node
54 | uses: actions/setup-node@v2
55 | with:
56 | node-version: 16
57 |
58 | - name: 📥 Download deps
59 | uses: bahmutov/npm-install@v1
60 |
61 | - name: ⚡ Run vitest
62 | run: npm run test -- --coverage --passWithNoTests
63 |
64 | lint:
65 | name: ⬣ ESLint
66 | runs-on: ubuntu-latest
67 | needs: [changes]
68 | if: ${{ needs.changes.outputs.DEPLOYABLE == 'true' }}
69 | steps:
70 | - name: 🛑 Cancel Previous Runs
71 | uses: styfle/cancel-workflow-action@0.9.1
72 |
73 | - name: ⬇️ Checkout repo
74 | uses: actions/checkout@v2
75 | with:
76 | fetch-depth: '50'
77 |
78 | - name: 🛠 Setup node
79 | uses: actions/setup-node@v2
80 | with:
81 | node-version: 16
82 |
83 | - name: 📥 Download deps
84 | uses: bahmutov/npm-install@v1
85 |
86 | - name: 🔬 Lint
87 | run: npm run lint
88 |
89 | typecheck:
90 | name: ʦ TypeScript
91 | runs-on: ubuntu-latest
92 | needs: [changes]
93 | if: ${{ needs.changes.outputs.DEPLOYABLE == 'true' }}
94 | steps:
95 | - name: 🛑 Cancel Previous Runs
96 | uses: styfle/cancel-workflow-action@0.9.1
97 |
98 | - name: ⬇️ Checkout repo
99 | uses: actions/checkout@v2
100 | with:
101 | fetch-depth: '50'
102 |
103 | - name: 🛠 Setup node
104 | uses: actions/setup-node@v2
105 | with:
106 | node-version: 16
107 |
108 | - name: 📥 Download deps
109 | uses: bahmutov/npm-install@v1
110 |
111 | - name: 🔎 Type check
112 | run: npm run typecheck
113 |
114 | cypress:
115 | name: ⚫️ Cypress
116 | runs-on: ubuntu-latest
117 | needs: [changes]
118 | if: ${{ needs.changes.outputs.DEPLOYABLE == 'true' }}
119 | steps:
120 | - name: 🛑 Cancel Previous Runs
121 | uses: styfle/cancel-workflow-action@0.9.1
122 |
123 | - name: ⬇️ Checkout repo
124 | uses: actions/checkout@v2
125 |
126 | - name: 🏄 Copy test env vars
127 | run: cp .env.example .env
128 |
129 | - name: ⎔ Setup node
130 | uses: actions/setup-node@v2
131 | with:
132 | node-version: 16
133 |
134 | - name: 📥 Download deps
135 | uses: bahmutov/npm-install@v1
136 |
137 | - name: ⚙️ Build
138 | run: npm run build
139 |
140 | - name: ▲ Prisma
141 | run: npx prisma migrate reset --force
142 |
143 | - name: 🌳 Cypress run
144 | uses: cypress-io/github-action@v2
145 | with:
146 | start: npm run start:mocks
147 | wait-on: 'http://localhost:8811'
148 | headless: true
149 | env:
150 | PORT: '8811'
151 | DISABLE_TELEMETRY: 'true'
152 |
153 | build:
154 | name: 🐳 Build
155 | needs: [changes]
156 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' &&
157 | needs.changes.outputs.DEPLOYABLE == 'true' }}
158 | runs-on: ubuntu-latest
159 | # only build/deploy main branch on pushes
160 | steps:
161 | - name: 🛑 Cancel Previous Runs
162 | uses: styfle/cancel-workflow-action@0.9.1
163 |
164 | - name: ⬇️ Checkout repo
165 | uses: actions/checkout@v2
166 |
167 | - name: 🐳 Set up Docker Buildx
168 | uses: docker/setup-buildx-action@v1
169 |
170 | # Setup cache
171 | - name: ⚡️ Cache Docker layers
172 | uses: actions/cache@v2
173 | with:
174 | path: /tmp/.buildx-cache
175 | key: ${{ runner.os }}-buildx-${{ github.sha }}
176 | restore-keys: |
177 | ${{ runner.os }}-buildx-
178 |
179 | - name: 🔑 Fly Registry Auth
180 | uses: docker/login-action@v1
181 | with:
182 | registry: registry.fly.io
183 | username: x
184 | password: ${{ secrets.FLY_API_TOKEN }}
185 |
186 | - name: 🐳 Docker build
187 | uses: docker/build-push-action@v2
188 | with:
189 | context: .
190 | push: true
191 | tags: registry.fly.io/${{ secrets.FLY_APP_NAME }}:${{ github.sha }}
192 | build-args: |
193 | COMMIT_SHA=${{ github.sha }}
194 | GITHUB_REPOSITORY=${{ github.repository }}
195 | cache-from: type=local,src=/tmp/.buildx-cache
196 | cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
197 |
198 | # This ugly bit is necessary if you don't want your cache to grow forever
199 | # till it hits GitHub's limit of 5GB.
200 | # Temp fix
201 | # https://github.com/docker/build-push-action/issues/252
202 | # https://github.com/moby/buildkit/issues/1896
203 | - name: Move cache
204 | run: |
205 | rm -rf /tmp/.buildx-cache
206 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache
207 |
208 | deploy:
209 | name: 🚀 Deploy
210 | runs-on: ubuntu-latest
211 | needs: [changes, build, lint, vitest, typecheck]
212 | # only build/deploy main branch on pushes
213 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' &&
214 | needs.changes.outputs.DEPLOYABLE == 'true' }}
215 |
216 | steps:
217 | - name: 🛑 Cancel Previous Runs
218 | uses: styfle/cancel-workflow-action@0.9.1
219 |
220 | - name: ⬇️ Checkout repo
221 | uses: actions/checkout@v2
222 |
223 | - name: 🚀 Deploy
224 | uses: superfly/flyctl-actions@1.1
225 | with:
226 | args:
227 | 'deploy -i registry.fly.io/${{ secrets.FLY_APP_NAME }}:${{ github.sha }}
228 | --strategy rolling'
229 | env:
230 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
231 |
--------------------------------------------------------------------------------
/.github/workflows/refresh-all-content.yml:
--------------------------------------------------------------------------------
1 | name: 🌀 Refresh all content
2 | on:
3 | workflow_dispatch:
4 |
5 | jobs:
6 | refresh-cache:
7 | name: 🌟 Refresh Content
8 | runs-on: ubuntu-latest
9 | if: ${{ github.ref == 'refs/heads/main' }}
10 | steps:
11 | - name: 🛑 Cancel Previous Runs
12 | uses: styfle/cancel-workflow-action@0.9.1
13 |
14 | - name: ⬇️ Checkout repo
15 | uses: actions/checkout@v2
16 | with:
17 | fetch-depth: '50'
18 |
19 | - name: 🛠 Setup node
20 | uses: actions/setup-node@v2
21 | with:
22 | node-version: 16
23 |
24 | - name: 💿 Refresh all Content
25 | run: node ./others/refresh-all-content.js
26 | env:
27 | REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }}
28 | FLY_APP_NAME: ${{ secrets.FLY_APP_NAME }}
29 |
--------------------------------------------------------------------------------
/.github/workflows/refresh-content.yml:
--------------------------------------------------------------------------------
1 | name: 🌟 Refresh Content
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | refresh-cache:
9 | name: 🌟 Refresh Content
10 | runs-on: ubuntu-latest
11 | if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
12 | steps:
13 | - name: 🛑 Cancel Previous Runs
14 | uses: styfle/cancel-workflow-action@0.9.1
15 |
16 | - name: ⬇️ Checkout repo
17 | uses: actions/checkout@v2
18 | with:
19 | fetch-depth: '50'
20 |
21 | - name: 🛠 Setup node
22 | uses: actions/setup-node@v2
23 | with:
24 | node-version: 16
25 |
26 | - name: 💿 Refresh Content
27 | run: node ./others/refresh-content.js
28 | env:
29 | REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }}
30 | FLY_APP_NAME: ${{ secrets.FLY_APP_NAME }}
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | build/
4 | /.cache
5 | /server/build
6 | /public/build
7 | coverage/
8 |
9 | .env
10 |
11 | prisma/*.db
12 | prisma/*.db*
13 |
14 | app/styles
15 | app/**/*.ignore.*
16 |
17 | cypress/videos/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache/
3 | build/
4 | public/build/
5 | app/styles
6 | **/*.ignore.*
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "jsxSingleQuote": true,
5 | "arrowParens": "avoid",
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # base node image
2 | FROM node:16-bullseye-slim as base
3 |
4 | # Install openssl for Prisma
5 | RUN apt-get update && apt-get install -y openssl
6 |
7 | # Install all node_modules, including dev dependencies
8 | FROM base as deps
9 |
10 | RUN mkdir /app
11 | WORKDIR /app
12 |
13 | ADD package.json package-lock.json ./
14 | RUN npm install --production=false
15 |
16 | # Setup production node_modules
17 | FROM base as production-deps
18 |
19 | RUN mkdir /app
20 | WORKDIR /app
21 |
22 | COPY --from=deps /app/node_modules /app/node_modules
23 | ADD package.json package-lock.json ./
24 | RUN npm prune --production
25 |
26 | # Build the app
27 | FROM base as build
28 |
29 | ARG COMMIT_SHA
30 | ENV COMMIT_SHA=$COMMIT_SHA
31 | ARG GITHUB_REPOSITORY
32 | ENV GITHUB_REPOSITORY=$GITHUB_REPOSITORY
33 | ENV NODE_ENV=production
34 |
35 | RUN mkdir /app
36 | WORKDIR /app
37 |
38 | COPY --from=deps /app/node_modules /app/node_modules
39 |
40 | ADD prisma .
41 | RUN npx prisma generate
42 |
43 | ADD . .
44 | RUN npm run build
45 |
46 | # Finally, build the production image with minimal footprint
47 | FROM base
48 |
49 | ARG GITHUB_REPOSITORY
50 | ENV GITHUB_REPOSITORY=$GITHUB_REPOSITORY
51 | ENV NODE_ENV=production
52 |
53 | RUN mkdir /app
54 | WORKDIR /app
55 |
56 | COPY --from=production-deps /app/node_modules /app/node_modules
57 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma
58 | COPY --from=build /app/build /app/build
59 | COPY --from=build /app/public /app/public
60 | ADD . .
61 |
62 | CMD ["npm", "run", "start"]
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Remix Speed Metal Stack
2 |
3 | Learn more about [Remix Stacks](https://remix.run/stacks).
4 |
5 | ```sh
6 | npx create-remix --template Girish21/speed-metal-stack
7 | ```
8 |
9 | ## Remix Blog 📖
10 |
11 | This blog starter template was inspired heavily by Kent C. Dodds implementation of [kentcdodds.com][kcd]. You can read more about the architecture and the idea behind it at [How I built a modern website in 2021][kcd-arch].
12 |
13 | ## Architecture 💡
14 |
15 | 
16 |
17 | ## Important 🚧
18 |
19 | Fly requires a globally unique name for all the apps, and we've used the directory name and random hash as the app name. Of course, you can change this anytime you want BEFORE launching the app with Fly CLI. But it's not a big deal since you can reassign the internal Fly URL to any custom domain by adding a [`CNAME`][cname] record to your custom domain pointing to the Fly internal URL. We'll see that later when deploying the app to production.
20 |
21 | ## Quickstart
22 |
23 | ```sh
24 | # run database migrations and set up the initial blog
25 | npm run setup
26 | # run the initial build for the development server
27 | npm run build
28 | # start the development server and run other processes in parallel in watch mode
29 | npm run dev
30 | ```
31 |
32 | ## Available scripts
33 |
34 | - `build` - compile and build the express server, Remix app, Tailwind in `production` mode
35 | - `dev` - starts the express server, Remix watcher, Tawilwind CLI in watch mode
36 | - `format` - runs prettier on the codebase and fixes fixable issues
37 | - `lint` - runs ESLint on the codebase
38 | - `new:blog` - create a new Blog post template from the command line
39 | - `start` - starts the express server (should only be executed after running `npm run build`)
40 | - `test` - runs `vitest`
41 | - `test:e2e:dev` - starts the cypress runner in development mode
42 | - `test:e2e:run` - starts the cypress runner in CI mode
43 | - `typecheck` - runs type check on the codebase
44 |
45 | ## Fly Setup 🛠
46 |
47 | 1. [Install Fly](https://fly.io/docs/getting-started/installing-flyctl/)
48 |
49 | 2. Sign up and log in to Fly
50 |
51 | ```sh
52 | flyctl auth signup
53 | ```
54 |
55 | ## Database 🗃
56 |
57 | We use [SQLite][sqlite] as the database in this template. SQLite is a fast database engine, a great option to persist data without reaching for advanced database engines like Postgres.
58 |
59 | ### Installation ⚙️
60 |
61 | SQLite comes pre-installed on all Macs. You can check the official installation guides for other OS's [SQLite Installation Guides][sqlite-installation]
62 |
63 | ### Why do we need a database ❓
64 |
65 | We use [MDX-Bundler][mdx-bundler] to compile the MDX files, and MDX-Bundler uses `esbuild` internally to compile the MDX files. Though `esbuild` is very fast, there is a noticeable lag during this process which is not suitable for the performance of the site and the user experience. And since there is no need to compile MDX on every request when the data does not change seems like a waste of time and performance. So instead, we can cache the compiled MDX and recompile it only when we know the content has changed.
66 |
67 | ### Prisma △
68 |
69 | We use [Prisma][prisma] as the ORM in this template. To create the SQLite database and initialise the database schema, run:
70 |
71 | ```sh
72 | npx prisma migrate dev
73 | ```
74 |
75 | The above command will prompt for a migration name, and you can name it as `initial migration`. This command will also install Prisma Client for interacting with the database.
76 |
77 | ## Development 💻
78 |
79 | We can start our development server with the migrations run and the SQLite database populated with the initial schema. Then, from a new tab in your terminal, run the command.
80 |
81 | ```sh
82 | npm run dev
83 | ```
84 |
85 | This command starts four processes concurrently.
86 |
87 | - The Remix dev server starts in development mode and rebuilds assets on file change.
88 | - Tailwind CLI which rebuilds the stylesheet when the styles change
89 | - An [MSW][msw] server which intercepts the API calls to GitHub and serves the content from the local instead of calling the remote API
90 | - A file watcher watches over the `content` directory and rebuilds the assets.
91 |
92 | ### Relavant files 🔍
93 |
94 | - Tailwind config [tailwind.config.js](./tailwind.config.js)
95 | - MSW API mock server [mock](./mocks/start.ts)
96 | - Content change watcher [refresh-on-content-change](./others/refresh-on-content-change.ts)
97 |
98 | ## Deployment 🚀
99 |
100 | ### Initial setup 👀
101 |
102 | Before proceeding to deploy our app, we have some steps to take care of:
103 |
104 | - Create a GitHub account [GitHub](https://repo.new)
105 | - Create a new app on Fly
106 |
107 | ```sh
108 | flyctl launch --name YOUR_APP_NAME --copy-config --no-deploy
109 | ```
110 |
111 | > ⚠️ Remember not to deploy since we have some setup steps left to complete!
112 |
113 | ### Environment variables and Secrets 🤫
114 |
115 | This template comes with GitHub actions workflows to automatically deploy the app to Fly.io. First, we need to set up our GitHub actions and the Fly app with some secrets. Let's do that now.
116 |
117 | To push the build image to the remote Fly registry from GitHub action, we need an access token from Fly. We can generate that using the Fly command line, run:
118 |
119 | ```sh
120 | flyctl auth token
121 | ```
122 |
123 | The command will generate an access token. You can then add this token to your GitHub actions secrets by visiting your GitHub repository's `settings` page `https://github.com/:owner/:repo/settings/secrets/actions` and then click `New repository secret`. Next, GitHub will prompt for a key and a value. The key should be `FLY_API_TOKEN`, and the value will be the token generated by the command line.
124 |
125 | We also need to set the Fly app name as a secret, the key should be `FLY_APP_NAME`, and the value will be the app name specified in [fly.toml](./fly.toml)
126 |
127 | Now we need to set up secrets in our Fly app.
128 |
129 | Since we're fetching the content from GitHub on demand instead of building all the pages upfront, we need an access token from GitHub to call the GitHub API and fetch the content. Also, GitHub won't rate-limit the app from calling the GitHub API more often. So, you can generate an access token at [Personal access token](https://github.com/settings/tokens). Then, you can copy the generated token and set it to your app's secret. We can do that by running the following command:
130 |
131 | ```sh
132 | flyctl secrets set GITHUB_TOKEN={GITHUB_TOKEN}
133 | ```
134 |
135 | We also need a secret to sign our session. We can do that by running the command:
136 |
137 | ```sh
138 | flyctl secrets set SESSION_SECRETS=$(openssl rand -hex 32)
139 | ```
140 |
141 | > If `openssl` is not available, you can generate a secure token using a password generating service like [`1Password`][generate-password].
142 |
143 | The last secret required is a token for securely communicating between the GitHub action and our app deployed on a remote server since we need a public-facing API for this communication.
144 |
145 | ```sh
146 | openssl rand -hex 32
147 | ```
148 |
149 | We have to set this secret as part of the GitHub actions secret and a Fly secret. The key should be `REFRESH_TOKEN`. You can create a new actions secret in GitHub and create a new secret for the Fly app by running the command.
150 |
151 | ```sh
152 | flyctl secrets set REFRESH_TOKEN={GENERATED_PASSWORD}
153 | ```
154 |
155 | ### Volumes 💾
156 |
157 | We also need to create a volume in Fly to persist our app data (SQLite DB) so that Fly can persist the data stored across deployments and container restarts. Again, we can do that using the Fly command line.
158 |
159 | ```sh
160 | flyctl volumes create data --region [REGION] --size 1
161 | ```
162 |
163 | > Note: REGION should be the region selected when launching the app. You can check the region chosen by running `flyctl regions list`.
164 |
165 | It's important to note that Volumes are bound to an app in a region and cannot be shared between apps in the same region or across multiple regions.
166 |
167 | You can learn more about Fly Volumes [here][volumes]
168 |
169 | ### Push to Prod 🥳
170 |
171 | We are ready for our first deployment. GitHub actions workflows are configured to run on push to the `main` branch. So let's push the local branch `main` to remote, triggering the workflows.
172 |
173 | Once all the checks are passed, and the deployment is complete, you can run:
174 |
175 | ```sh
176 | flyctl info
177 | ```
178 |
179 | To get the current app URL and IP address. The app URL will be `https://YOUR_APP_NAME.fly.dev`. You can visit that URL, and the site should be online. That's it. You have deployed your blog built using REMIX!.
180 |
181 | ### Adding Custom Domain 🔖
182 |
183 | To add a custom domain to the app, you first must buy a domain from a Domain Name Register, and you can choose one of your preferences. Some popular options are [Domain.com](https://www.domain.com/), [Google](https://domains.google.com/registrar), [Cloudflare](https://www.cloudflare.com/en-gb/products/registrar/).
184 |
185 | After buying the domain, we can add a DNS record to point to the domain or create a subdomain and point that to the Fly app URL. We can do that by adding a DNS record using the CNAME option and entering the Fly URL `https://YOUR_APP_NAME.fly.dev`.
186 |
187 | We also have to create an SSL certificate on Fly with the domain name. We can do that by running the command:
188 |
189 | ```sh
190 | flyctl certs create [DOMAIN]
191 | ```
192 |
193 | You can read more about this at [SSL for Custom Domains](https://fly.io/docs/app-guides/custom-domains-with-fly/)
194 |
195 | That's it, and we are ready to share our blog with the rest of the world! But there is one more step to take care of before sharing it.
196 |
197 | ### Scaling ⚖️
198 |
199 | There are two ways of scaling an application, vertical and horizontal.
200 |
201 | In vertical scaling, the system is scaled by adding more compute resources to the server (increasing the CPU/RAM). Fly supports vertical scaling, and you can check the docs here [scaling virtual machines](https://fly.io/docs/reference/scaling/#scaling-virtual-machines).
202 |
203 | Horizontal scaling is achieved by adding more replicas of the same application, either in the same region or in other regions worldwide. Fly supports many regions worldwide, and you can read more about them here [Fly regions](https://fly.io/docs/reference/scaling/#scaling-virtual-machines).
204 |
205 | Our app is currently deployed in only one region we selected when we ran `flyctl launch`. This is fine during prototyping and development, but when pushing for production, we would want our app to be accessible from regions worldwide and have similar performances for users worldwide. In this case, we can add more replicas of our app worldwide, at least in the regions with many targetted users, so that the app will run on the servers closer to the users, and all the users will have comparable performance.
206 |
207 | Since Fly [anchors the regions](https://fly.io/docs/reference/volumes/#creating-volumes) based on the volumes created, we can add more regions by creating a volume in the new region or adding a replica in the same region. For example, we can do that by:
208 |
209 | ```sh
210 | flyctl volumes create data --region [NEW_REGION] --size 1
211 | ```
212 |
213 | > You can check this list of available regions at [Fly regions][fly-regions]
214 |
215 | And then increase the [scale count](https://fly.io/docs/reference/scaling/#count-scaling) of the Fly app by running the command:
216 |
217 | ```sh
218 | flyctl scale count 2
219 | ```
220 |
221 | > The above command will set the scale count to 2, meaning two instances of our app will run on the specified regions where the volumes are created.
222 |
223 | You can read more about scaling at [Fly scaling][fly-scaling]
224 |
225 | ## API Mocks 🔸
226 |
227 | Our architecture is to fetch the blog content from the GitHub repository on demand and not bundle the content as part of the build. Therefore, we will be making a significant amount of API calls to GitHub. And with any API, there come restrictions such as rate limit, calls per minute, etc. And when we're writing an article, the process becomes tedious since we're making calls to the GitHub API; the article has to be on GitHub so that the API can return the content. This process is not ideal. We can do better.
228 |
229 | Instead, we can mock the API request to GitHub and serve the articles locally, providing a better experience. Instead of calling the GitHub API, we use [MSW][msw] to intercept the request and return a mocked response that serves the content from the local file system.
230 |
231 | ## Linting ⬡
232 |
233 | This template comes preconfigured with [ESLint][eslint] and [Prettier][prettier] rules and is integrated with the workflow as a build step. For example, you can run `npm run lint` to run ESLint on the project and `npm run format` to run prettier.
234 |
235 | ## Styling 💅🏻
236 |
237 | This template comes preconfigured with [Tailwindcss][tailwind] with all the scripts required during development and production.
238 |
239 | The template also comes with a theme toggler preconfigured and can detect the suitable theme and prevent [FART][fart]
240 |
241 | ## Testing 🔬
242 |
243 | This template comes preconfigured with [Jest][jest] and [React testing library][rtl] for unit testing and [Cypress][cypress] for e2e testing and is configured to run as part of the GitHub actions. You can run the `npm run test` command to run the Jest test suite. And `npm run test:e2e:run` to run the Cypress tests in a headless mode. You can check [package.json](./package.json) for the available commands.
244 |
245 | ## Type check ʦ
246 |
247 | You can run `npm run typecheck` to run `tsc` on the codebase. Type check is also included as part of the deployment workflow.
248 |
249 | ## Debugging
250 |
251 | Some helpful commands to debug the application on Fly using the command line
252 |
253 | ### Logs
254 |
255 | You can check the logs using the command `flyctl logs` from the project directory, containing the `fly.toml` file in the project's root. You can also check the logs from the console by visiting [fly.io/apps](https://fly.io/apps).
256 |
257 | ### Console
258 |
259 | You can also log in to the remote console using the `flyctl ssh console` command.
260 |
261 | ### Database
262 |
263 | After logging in to the console, you can also inspect the SQLite DB. But first, we have to install SQLite on the remote machine. We can do that using the `apt-get install sqlite3` command. Then, `cd` into the volume using the `cd data` command (Note: `data` refers to the volume's name created from the command line). And then run the command `sqlite3 sqlite.db` to open a command-line interface into the database.
264 |
265 | ## Important links
266 |
267 | - [Remix](https://remix.run/)
268 | - [Remix docs](https://remix.run/docs/en/v1)
269 | - [Fly.io](https://fly.io/)
270 | - [flyctl](https://fly.io/docs/flyctl/)
271 | - [Fly secrets](https://fly.io/docs/reference/secrets/)
272 | - [Fly scaling][fly-scaling]
273 | - [Fly volumes](https://fly.io/docs/reference/volumes/)
274 | - [Fly regions][fly-regions]
275 | - [Fly configuration](https://fly.io/docs/reference/configuration/)
276 | - [MDX Bundler][mdx-bundler]
277 | - [SQLite][sqlite]
278 |
279 | [kcd]: https://kentcdodds.com/
280 | [kcd-arch]: https://kentcdodds.com/blog/how-i-built-a-modern-website-in-2021
281 | [cname]: https://en.wikipedia.org/wiki/CNAME_record
282 | [sqlite]: https://www.sqlite.org/index.html
283 | [mdx-bundler]: https://github.com/kentcdodds/mdx-bundler
284 | [sqlite-installation]: https://www.sqlite.org/download.html
285 | [prisma]: https://prisma.io
286 | [msw]: https://mswjs.io/
287 | [tailwind]: https://tailwindcss.com/
288 | [fart]: https://css-tricks.com/flash-of-inaccurate-color-theme-fart/
289 | [generate-password]: https://1password.com/password-generator/
290 | [volumes]: https://fly.io/docs/reference/volumes/
291 | [eslint]: https://eslint.org/
292 | [prettier]: https://prettier.io/
293 | [cypress]: https://www.cypress.io/
294 | [jest]: https://jestjs.io/
295 | [rtl]: https://testing-library.com/
296 | [fly-regions]: https://fly.io/docs/reference/regions/
297 | [fly-scaling]: https://fly.io/docs/reference/scaling/
298 |
--------------------------------------------------------------------------------
/app/assets/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icons/light-bulb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icons/moon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/components/blog-item.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from '@remix-run/react'
2 | import type { getMdxListItems } from '~/utils/mdx.server'
3 |
4 | type BlogItemType = Awaited>[0]
5 |
6 | export default function BlogItem({ description, slug, title }: BlogItemType) {
7 | return (
8 |
9 |
14 |
15 | {title}
16 |
17 |
18 | {description}
19 |
20 |
21 | Read more
22 |
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/components/blog-list.tsx:
--------------------------------------------------------------------------------
1 | import type { getMdxListItems } from '~/utils/mdx.server'
2 | import BlogItem from './blog-item'
3 |
4 | type BlogListType = { blogList: Awaited> }
5 |
6 | export default function BlogList({ blogList }: BlogListType) {
7 | return (
8 |
9 | {blogList.map(blogItem => (
10 |
11 | ))}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/app/components/client-only.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/sergiodxa/remix-utils
2 | import * as React from 'react'
3 | import { useHydrated } from '../hooks/use-hydrated'
4 |
5 | /**
6 | * @deprecated Pass a function as children to avoid issues with client only
7 | * imported components
8 | */
9 | type DeprecatedProps = {
10 | children: React.ReactNode
11 | fallback?: React.ReactNode
12 | }
13 |
14 | type Props =
15 | | DeprecatedProps
16 | | {
17 | /**
18 | * You are encouraged to add a fallback that is the same dimensions
19 | * as the client rendered children. This will avoid content layout
20 | * shift which is disgusting
21 | */
22 | children: () => React.ReactNode
23 | fallback?: React.ReactNode
24 | }
25 |
26 | /**
27 | * Render the children only after the JS has loaded client-side. Use an optional
28 | * fallback component if the JS is not yet loaded.
29 | *
30 | * Example: Render a Chart component if JS loads, renders a simple FakeChart
31 | * component server-side or if there is no JS. The FakeChart can have only the
32 | * UI without the behavior or be a loading spinner or skeleton.
33 | * ```tsx
34 | * return (
35 | * }>
36 | * {() => }
37 | *
38 | * );
39 | * ```
40 | */
41 | export function ClientOnly({ children, fallback = null }: Props) {
42 | if (typeof children !== 'function') {
43 | console.warn(
44 | '[remix-utils] ClientOnly: Pass a function as children to avoid issues with client-only imported components',
45 | )
46 | }
47 |
48 | return useHydrated() ? (
49 | <>{typeof children === 'function' ? children() : children}>
50 | ) : (
51 | <>{fallback}>
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/app/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import LinkOrAnchor from './link-or-anchor'
4 | import GitHubSvg from '~/assets/icons/github.svg'
5 | import TwitterSvg from '~/assets/icons/twitter.svg'
6 |
7 | export function preloadFooterSvg() {
8 | return [
9 | { rel: 'preload', href: GitHubSvg, as: 'image', type: 'image/svg+xml' },
10 | { rel: 'preload', href: TwitterSvg, as: 'image', type: 'image/svg+xml' },
11 | ]
12 | }
13 |
14 | export default function Footer() {
15 | return (
16 |
45 | )
46 | }
47 |
48 | function Svg({ children }: { children: React.ReactNode }) {
49 | return (
50 |
56 | )
57 | }
58 |
59 | function Link({
60 | children,
61 | href,
62 | reload,
63 | }: {
64 | children: React.ReactNode
65 | href: string
66 | reload?: boolean
67 | }) {
68 | return (
69 |