├── .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 |
29 |

This page was rendered at {data.date.toLocaleString()}

30 |
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 | --------------------------------------------------------------------------------