├── .gitignore
├── .nvmrc
├── app
├── entry.client.tsx
├── entry.server.tsx
├── root.tsx
├── routes
│ ├── 404.tsx
│ └── index.tsx
├── styles
│ ├── global.css
│ └── index.css
└── tsconfig.json
├── functions
└── app
│ ├── adapter.js
│ ├── fetchGlobals.js
│ └── index.js
├── netlify.toml
├── package-lock.json
├── package.json
├── public
└── favicon.ico
└── remix.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /functions/app/build
2 | /public/build
3 | # Local Netlify folder
4 | .netlify
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom";
2 | import Remix from "@remix-run/react/browser";
3 | ReactDOM.hydrate(
4 | // @types/react-dom says the 2nd argument to ReactDOM.hydrate() must be a
5 | // `Element | DocumentFragment | null` but React 16 allows you to pass the
6 | // `document` object as well. This is a bug in @types/react-dom that we can
7 | // safely ignore for now.
8 | // @ts-ignore
9 | ,
10 | document
11 | );
12 |
--------------------------------------------------------------------------------
/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOMServer from "react-dom/server";
2 | import type { EntryContext } from "@remix-run/core";
3 | import Remix from "@remix-run/react/server";
4 |
5 | export default function handleRequest(
6 | request: Request,
7 | responseStatusCode: number,
8 | responseHeaders: Headers,
9 | remixContext: EntryContext
10 | ) {
11 | let markup = ReactDOMServer.renderToString(
12 |
13 | );
14 |
15 | return new Response("" + markup, {
16 | status: responseStatusCode,
17 | headers: {
18 | ...Object.fromEntries(responseHeaders),
19 | "Content-Type": "text/html",
20 | },
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import type { LinksFunction, LoaderFunction } from "@remix-run/react";
2 | import { Meta, Links, Scripts, useRouteData } from "@remix-run/react";
3 | import { Outlet } from "react-router-dom";
4 |
5 | import styles from "url:./styles/global.css";
6 |
7 | export let links: LinksFunction = () => {
8 | return [{ rel: "stylesheet", href: styles }];
9 | };
10 |
11 | export let loader: LoaderFunction = () => {
12 | return { date: new Date() };
13 | };
14 |
15 | export default function App() {
16 | let data = useRouteData();
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | export function ErrorBoundary({ error }: { error: Error }) {
38 | return (
39 |
40 |
41 |
42 | Oops!
43 |
44 |
45 |
46 |
App Error
47 |
{error.message}
48 |
49 | Replace this UI with what you want users to see when your app throws
50 | uncaught errors. The file is at app/App.tsx
.
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/app/routes/404.tsx:
--------------------------------------------------------------------------------
1 | export function meta() {
2 | return { title: "Ain't nothing here" };
3 | }
4 |
5 | export default function FourOhFour() {
6 | return (
7 |
8 |
404
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/app/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import type {
2 | MetaFunction,
3 | LinksFunction,
4 | LoaderFunction
5 | } from "@remix-run/react";
6 | import { useRouteData } from "@remix-run/react";
7 |
8 | import styles from "url:../styles/index.css";
9 |
10 | export let meta: MetaFunction = () => {
11 | return {
12 | title: "Remix Starter",
13 | description: "Welcome to remix!"
14 | };
15 | };
16 |
17 | export let links: LinksFunction = () => {
18 | return [{ rel: "stylesheet", href: styles }];
19 | };
20 |
21 | export let loader: LoaderFunction = () => {
22 | return { message: "this is awesome 😎" };
23 | };
24 |
25 | export default function Index() {
26 | let data = useRouteData();
27 |
28 | return (
29 |
30 |
Welcome to Remix!
31 |
32 | Check out the docs to get
33 | started.
34 |
35 |
Message from the loader: {data.message}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/app/styles/global.css:
--------------------------------------------------------------------------------
1 | :focus:not(:focus-visible) {
2 | outline: none;
3 | }
4 |
5 | body {
6 | font-family: sans-serif;
7 | }
8 |
9 | footer {
10 | text-align: center;
11 | color: #ccc;
12 | padding-top: 80px;
13 | }
14 |
--------------------------------------------------------------------------------
/app/styles/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | * when the user visits this page, this style will apply, when they leave, it
3 | * will get unloaded, so don't worry so much about conflicting styles between
4 | * pages!
5 | */
6 |
--------------------------------------------------------------------------------
/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "jsx": "react-jsx",
5 | "moduleResolution": "node",
6 | "target": "es2019",
7 | "strict": true,
8 | "skipLibCheck": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/functions/app/adapter.js:
--------------------------------------------------------------------------------
1 | var url = require("url");
2 | var core = require("@remix-run/core");
3 | require("./fetchGlobals");
4 |
5 | function createRequestHandler({
6 | build,
7 | getLoadContext,
8 | mode = process.env.NODE_ENV,
9 | }) {
10 | let handleRequest = core.createRequestHandler(build, mode);
11 | return async (event, context) => {
12 | let request = createRemixRequest(event);
13 | let loadContext =
14 | typeof getLoadContext === "function"
15 | ? getLoadContext(event, context)
16 | : undefined;
17 | let response = await handleRequest(request, loadContext);
18 | let body = await response.text();
19 | return {
20 | statusCode: response.status,
21 | // TODO: use this for multiple set-cookie
22 | // multiValueHeaders: getMultiValueHeaders(response.headers),
23 | headers: Object.fromEntries(response.headers),
24 | body: body || undefined,
25 | };
26 | };
27 | }
28 |
29 | function createRemixRequest(event) {
30 | let host = event.headers["x-forwarded-host"] || event.headers.host;
31 | let rawPath = getRawPath(event);
32 | let url$1 = new url.URL(rawPath, `https://${host}`);
33 | let init = { method: event.httpMethod, headers: event.headers };
34 |
35 | if (event.httpMethod !== "GET" && event.httpMethod !== "HEAD") {
36 | init.body = event.isBase64Encoded
37 | ? Buffer.from(event.body, "base64").toString()
38 | : event.body;
39 | }
40 | return new core.Request(url$1.toString(), init);
41 | }
42 |
43 | // TODO: Figure why netlify urls lose information?
44 | function getRawPath(event) {
45 | let searchParams = new URLSearchParams();
46 | let paramKeys = Object.keys(event.multiValueQueryStringParameters);
47 | for (let key of paramKeys) {
48 | let values = event.multiValueQueryStringParameters[key];
49 | for (let val of values) {
50 | searchParams.append(key, val);
51 | }
52 | }
53 | let rawParams = searchParams.toString();
54 |
55 | let rawPath = event.path;
56 | if (rawParams) rawPath += `?${rawParams}`;
57 |
58 | return rawPath;
59 | }
60 |
61 | exports.createRequestHandler = createRequestHandler;
62 |
--------------------------------------------------------------------------------
/functions/app/fetchGlobals.js:
--------------------------------------------------------------------------------
1 | var core = require("@remix-run/core");
2 |
3 | let fetch = (input, init) =>
4 | core.fetch(input, {
5 | compress: false,
6 | ...init,
7 | });
8 |
9 | global.Headers = core.Headers;
10 | global.Request = core.Request;
11 | global.Response = core.Response;
12 | global.fetch = fetch;
13 |
--------------------------------------------------------------------------------
/functions/app/index.js:
--------------------------------------------------------------------------------
1 | const { createRequestHandler } = require("./adapter");
2 | exports.handler = createRequestHandler({ build: require("./build") });
3 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | functions = "functions"
3 | publish = "public"
4 |
5 | # [[redirects]]
6 | # from = "/_static/*"
7 | # to = "/public/:splat"
8 | # status = 200
9 |
10 | [[redirects]]
11 | from = "/*"
12 | to = "/.netlify/functions/app"
13 | status = 200
14 |
15 | [[headers]]
16 | for = "/build/*"
17 | [headers.values]
18 | "Cache-Control" = "public, max-age=31536000, s-maxage=31536000"
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netlify-app",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "@remix-run/core": "^0.14.1",
13 | "@remix-run/data": "^0.14.0",
14 | "@remix-run/react": "^0.14.0",
15 | "react": "^17.0.1",
16 | "react-dom": "^17.0.1",
17 | "react-router": "^6.0.0-beta.0",
18 | "react-router-dom": "^6.0.0-beta.0"
19 | },
20 | "devDependencies": {
21 | "netlify-cli": "^3.10.11",
22 | "@remix-run/dev": "^0.14.0",
23 | "@types/react": "^17.0.0",
24 | "@types/react-dom": "^17.0.0",
25 | "concurrently": "^5.3.0",
26 | "typescript": "^4.1.2"
27 | },
28 | "engines": {
29 | "node": "14.x"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryanflorence/netlify-starter/fd3bebffe7c76ead0554a15fbcab0b232b169a90/public/favicon.ico
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | appDirectory: "app",
3 | browserBuildDirectory: "public/build",
4 | publicPath: "/build/",
5 | serverBuildDirectory: "build",
6 | devServerPort: 8002,
7 | };
8 |
--------------------------------------------------------------------------------