mirror of
https://github.com/BradNut/awesome-uses
synced 2025-09-08 17:40:31 +00:00
commit
f84fef215d
10 changed files with 181 additions and 126 deletions
63
netlify-server.ts
Normal file
63
netlify-server.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { AppLoadContext, ServerBuild } from '@netlify/remix-runtime'
|
||||
import { createRequestHandler as createRemixRequestHandler } from '@netlify/remix-runtime'
|
||||
import type { Context } from '@netlify/edge-functions'
|
||||
|
||||
type LoadContext = AppLoadContext & Context
|
||||
|
||||
/**
|
||||
* A function that returns the value to use as `context` in route `loader` and
|
||||
* `action` functions.
|
||||
*
|
||||
* You can think of this as an escape hatch that allows you to pass
|
||||
* environment/platform-specific values through to your loader/action.
|
||||
*/
|
||||
export type GetLoadContextFunction = (request: Request, context: Context) => Promise<LoadContext> | LoadContext
|
||||
|
||||
export type RequestHandler = (request: Request, context: LoadContext) => Promise<Response | void>
|
||||
|
||||
export function createRequestHandler({
|
||||
build,
|
||||
mode,
|
||||
getLoadContext,
|
||||
}: {
|
||||
build: ServerBuild
|
||||
mode?: string
|
||||
getLoadContext?: GetLoadContextFunction
|
||||
}): RequestHandler {
|
||||
const remixHandler = createRemixRequestHandler(build, mode)
|
||||
|
||||
const assetPath = build.assets.url.split('/').slice(0, -1).join('/')
|
||||
|
||||
return async (request: Request, context: LoadContext): Promise<Response | void> => {
|
||||
const { pathname } = new URL(request.url)
|
||||
// Skip the handler for static files
|
||||
if (pathname.startsWith(`${assetPath}/`)) {
|
||||
// Temporary fix - passing the request to the Netlify static asset handler causes a 203 Not Content error. Passing the through remix works, but I asssume isn't ideal
|
||||
// console.log('Skipping Remix handler for static file', pathname)
|
||||
// return;
|
||||
}
|
||||
try {
|
||||
const loadContext = (await getLoadContext?.(request, context)) || context
|
||||
|
||||
const response = await remixHandler(request, loadContext)
|
||||
|
||||
// A useful header for debugging
|
||||
response.headers.set('x-nf-runtime', 'Edge')
|
||||
|
||||
if (response.status === 404) {
|
||||
// Check if there is a matching static file
|
||||
const originResponse = await context.next({
|
||||
sendConditionalRequest: true,
|
||||
})
|
||||
if (originResponse.status !== 404) {
|
||||
return originResponse
|
||||
}
|
||||
}
|
||||
return response
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
|
||||
return new Response('Internal Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
15
netlify.toml
15
netlify.toml
|
|
@ -1,18 +1,13 @@
|
|||
[build]
|
||||
command = "remix build"
|
||||
functions = "netlify/functions"
|
||||
publish = "public"
|
||||
|
||||
[dev]
|
||||
command = "remix watch"
|
||||
port = 3000
|
||||
autoLaunch = false
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/.netlify/functions/server"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/build/*"
|
||||
[headers.values]
|
||||
"Cache-Control" = "public, max-age=31536000, s-maxage=31536000"
|
||||
# [[headers]]
|
||||
# for = "/build/*"
|
||||
# [headers.values]
|
||||
# "Cache-Control" = "public, max-age=31536000, s-maxage=31536000"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
"@actions/github": "^5.1.1",
|
||||
"@babel/core": "^7.21.0",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@netlify/edge-functions": "^2.0.0",
|
||||
"@netlify/functions": "^1.4.0",
|
||||
"@netlify/remix-edge-adapter": "^1.0.0",
|
||||
"@remix-run/dev": "^1.13.0",
|
||||
"@remix-run/netlify": "^1.13.0",
|
||||
"@remix-run/node": "^1.13.0",
|
||||
|
|
@ -28,6 +30,7 @@
|
|||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"country-emoji": "^1.5.6",
|
||||
"isbot": "^3.6.6",
|
||||
"joi": "^17.8.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ specifiers:
|
|||
'@actions/github': ^5.1.1
|
||||
'@babel/core': ^7.21.0
|
||||
'@babel/preset-react': ^7.18.6
|
||||
'@netlify/edge-functions': ^2.0.0
|
||||
'@netlify/functions': ^1.4.0
|
||||
'@netlify/remix-edge-adapter': ^1.0.0
|
||||
'@remix-run/dev': ^1.13.0
|
||||
'@remix-run/netlify': ^1.13.0
|
||||
'@remix-run/node': ^1.13.0
|
||||
|
|
@ -21,6 +23,7 @@ specifiers:
|
|||
eslint: ^8.34.0
|
||||
eslint-config-wesbos: ^3.2.3
|
||||
husky: ^8.0.3
|
||||
isbot: ^3.6.6
|
||||
joi: ^17.8.1
|
||||
lint-staged: ^13.1.2
|
||||
normalize.css: ^8.0.1
|
||||
|
|
@ -41,7 +44,9 @@ dependencies:
|
|||
'@actions/github': 5.1.1
|
||||
'@babel/core': 7.21.0
|
||||
'@babel/preset-react': 7.18.6_@babel+core@7.21.0
|
||||
'@netlify/edge-functions': 2.0.0
|
||||
'@netlify/functions': 1.4.0
|
||||
'@netlify/remix-edge-adapter': 1.0.0
|
||||
'@remix-run/dev': 1.13.0_@remix-run+serve@1.13.0
|
||||
'@remix-run/netlify': 1.13.0_@netlify+functions@1.4.0
|
||||
'@remix-run/node': 1.13.0
|
||||
|
|
@ -52,6 +57,7 @@ dependencies:
|
|||
'@types/react': 18.0.28
|
||||
'@types/react-dom': 18.0.11
|
||||
country-emoji: 1.5.6
|
||||
isbot: 3.6.6
|
||||
joi: 17.8.1
|
||||
normalize.css: 8.0.1
|
||||
prop-types: 15.8.1
|
||||
|
|
@ -1382,6 +1388,17 @@ packages:
|
|||
postcss-selector-parser: 6.0.11
|
||||
dev: true
|
||||
|
||||
/@deno/shim-deno-test/0.3.3:
|
||||
resolution: {integrity: sha512-Ge0Tnl7zZY0VvEfgsyLhjid8DzI1d0La0dgm+3m0/A8gZXgp5xwlyIyue5e4SCUuVB/3AH/0lun9LcJhhTwmbg==}
|
||||
dev: false
|
||||
|
||||
/@deno/shim-deno/0.10.0:
|
||||
resolution: {integrity: sha512-E7rQ0Hk33V45xQXKEnCxizdSP5C+hhqw1H3xWXsct3kYFWgG93B5gN3LKlyvcxbckt8d67jVa6s+y5duRYawvg==}
|
||||
dependencies:
|
||||
'@deno/shim-deno-test': 0.3.3
|
||||
which: 2.0.2
|
||||
dev: false
|
||||
|
||||
/@emotion/hash/0.9.0:
|
||||
resolution: {integrity: sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==}
|
||||
dev: false
|
||||
|
|
@ -1715,6 +1732,13 @@ packages:
|
|||
'@jridgewell/resolve-uri': 3.1.0
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
|
||||
/@netlify/edge-functions/2.0.0:
|
||||
resolution: {integrity: sha512-mRVGnPNA4YayDLPwnO1ZrcWwBODPj5BQPbx3/FUlQtZ5ow2D+PjMPQr8IcFm0HfMJQgtHZS39p9VS6PRSi1ePw==}
|
||||
engines: {node: ^14.16.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@deno/shim-deno': 0.10.0
|
||||
dev: false
|
||||
|
||||
/@netlify/functions/1.4.0:
|
||||
resolution: {integrity: sha512-gy7ULTIRroc2/jyFVGx1djCmmBMVisIwrvkqggq5B6iDcInRSy2Tpkm+V5C63hKJVkNRskKWtLQKm9ecCaQTjA==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
|
|
@ -1722,6 +1746,18 @@ packages:
|
|||
is-promise: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@netlify/remix-edge-adapter/1.0.0:
|
||||
resolution: {integrity: sha512-NdTDRW8X/Iyh/ywHcf5H7HUkLoc/k2oxxOMIO5/rm0LHMN2bGFhbOxMBfi0PtFsa/Jn+efbNTkCM15JXoITzSw==}
|
||||
dependencies:
|
||||
'@netlify/remix-runtime': 1.0.0
|
||||
dev: false
|
||||
|
||||
/@netlify/remix-runtime/1.0.0:
|
||||
resolution: {integrity: sha512-w1nLlMABrW5uJoQc3eADWZ4wgnRqcqpNOQ+ZMQnoXSwukUGGN6JwGs3/BP+f/8F4CCy7sakbbF+akBmyKIKciQ==}
|
||||
dependencies:
|
||||
'@remix-run/server-runtime': 1.13.0
|
||||
dev: false
|
||||
|
||||
/@nicolo-ribaudo/eslint-scope-5-internals/5.1.1-v1:
|
||||
resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
|
||||
dependencies:
|
||||
|
|
@ -4881,6 +4917,11 @@ packages:
|
|||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
dev: true
|
||||
|
||||
/isbot/3.6.6:
|
||||
resolution: {integrity: sha512-98aGl1Spbx1led422YFrusDJ4ZutSNOymb2avZ2V4BCCjF3MqAF2k+J2zoaLYahubaFkb+3UyvbVDVlk/Ngrew==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/isexe/2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
const { config } = require("@netlify/remix-edge-adapter");
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
...config,
|
||||
appDirectory: "src",
|
||||
future: {
|
||||
unstable_postcss: true,
|
||||
|
|
@ -9,5 +11,5 @@ module.exports = {
|
|||
process.env.NETLIFY || process.env.NETLIFY_LOCAL
|
||||
? "./server.js"
|
||||
: undefined,
|
||||
serverBuildPath: ".netlify/functions-internal/server.js",
|
||||
// serverBuildPath: ".netlify/functions-internal/server.js",
|
||||
};
|
||||
|
|
|
|||
47
server.js
47
server.js
|
|
@ -1,47 +0,0 @@
|
|||
import { createRequestHandler } from "@remix-run/netlify";
|
||||
import * as build from "@remix-run/dev/server-build";
|
||||
|
||||
/*
|
||||
* Returns a context object with at most 3 keys:
|
||||
* - `netlifyGraphToken`: raw authentication token to use with Netlify Graph
|
||||
* - `clientNetlifyGraphAccessToken`: For use with JWTs generated by
|
||||
* `netlify-graph-auth`.
|
||||
* - `netlifyGraphSignature`: a signature for subscription events. Will be
|
||||
* present if a secret is set.
|
||||
*/
|
||||
function getLoadContext(event, context) {
|
||||
let rawAuthorizationString;
|
||||
let netlifyGraphToken;
|
||||
|
||||
if (event.authlifyToken != null) {
|
||||
netlifyGraphToken = event.authlifyToken;
|
||||
}
|
||||
|
||||
const authHeader = event.headers["authorization"];
|
||||
const graphSignatureHeader = event.headers["x-netlify-graph-signature"];
|
||||
|
||||
if (authHeader != null && /Bearer /gi.test(authHeader)) {
|
||||
rawAuthorizationString = authHeader.split(" ")[1];
|
||||
}
|
||||
|
||||
const loadContext = {
|
||||
clientNetlifyGraphAccessToken: rawAuthorizationString,
|
||||
netlifyGraphToken: netlifyGraphToken,
|
||||
netlifyGraphSignature: graphSignatureHeader,
|
||||
};
|
||||
|
||||
// Remove keys with undefined values
|
||||
Object.keys(loadContext).forEach((key) => {
|
||||
if (loadContext[key] == null) {
|
||||
delete loadContext[key];
|
||||
}
|
||||
});
|
||||
|
||||
return loadContext;
|
||||
}
|
||||
|
||||
export const handler = createRequestHandler({
|
||||
build,
|
||||
getLoadContext,
|
||||
mode: process.env.NODE_ENV,
|
||||
});
|
||||
15
server.ts
Normal file
15
server.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Import path interpreted by the Remix compiler
|
||||
import * as build from "@remix-run/dev/server-build";
|
||||
// import { createRequestHandler } from "@netlify/remix-edge-adapter";
|
||||
import { createRequestHandler } from "./netlify-server";
|
||||
|
||||
export default createRequestHandler({
|
||||
build,
|
||||
// process.env.NODE_ENV is provided by Remix at compile time
|
||||
mode: process.env.NODE_ENV,
|
||||
});
|
||||
|
||||
export const config = {
|
||||
cache: "manual",
|
||||
path: "/*",
|
||||
};
|
||||
|
|
@ -1,25 +1,35 @@
|
|||
import { PassThrough } from 'stream';
|
||||
import type { EntryContext } from '@remix-run/node';
|
||||
import { Response } from '@remix-run/node';
|
||||
import { RemixServer } from '@remix-run/react';
|
||||
import { renderToPipeableStream } from 'react-dom/server';
|
||||
import { renderToReadableStream } from 'react-dom/server';
|
||||
|
||||
const ABORT_DELAY = 5000;
|
||||
|
||||
export async function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {
|
||||
let result = '';
|
||||
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
result += value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type CachedResponse = {
|
||||
html: string;
|
||||
date: Date;
|
||||
}
|
||||
const cache = new Map<string, CachedResponse>();
|
||||
|
||||
|
||||
export default function handleRequest(
|
||||
export default async function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
console.log(request.url);
|
||||
// check if we have a cached response in memory
|
||||
const cachedResponse = cache.get(request.url);
|
||||
if (cachedResponse) {
|
||||
|
|
@ -37,48 +47,35 @@ export default function handleRequest(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
const body = await renderToReadableStream(
|
||||
<RemixServer context={remixContext} url={request.url} />,
|
||||
{
|
||||
onShellReady: () => {
|
||||
const body = new PassThrough();
|
||||
|
||||
body
|
||||
.on('data', (data) => {
|
||||
chunks.push(data);
|
||||
})
|
||||
.on('end', () => {
|
||||
const html = Buffer.concat(chunks).toString('utf8');
|
||||
cache.set(request.url, { html: html.replace('Rendered Fresh', `Served from Cache ${new Date().toString()}`), date: new Date() });
|
||||
})
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError: (err: unknown) => {
|
||||
reject(err);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
didError = true;
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
// tee the stream so we can cache it and send it to the client
|
||||
const [toReponse, toCache] = body.tee();
|
||||
|
||||
streamToText(toCache).then(html => {
|
||||
console.log('Caching', request.url);
|
||||
cache.set(request.url, {
|
||||
html: html.replace('Rendered Fresh',`Rendered from cache ${new Date().toISOString()}`),
|
||||
date: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
const headers = new Headers(responseHeaders);
|
||||
headers.set("Content-Type", "text/html");
|
||||
const response = new Response(toReponse, {
|
||||
headers,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export function tags() {
|
|||
const counts = allTags.reduce(countInstances, {});
|
||||
// sort and filter for any tags that only have 1
|
||||
const tags = Object.entries(counts)
|
||||
.sort(([, countA], [, countB]) => countB - countA)
|
||||
// Only show the tag if this topic has 3 or more people in it
|
||||
.filter(([, count]) => count >= 3)
|
||||
.map(([name, count]) => ({ name, count }));
|
||||
|
|
@ -85,8 +84,11 @@ export function tags() {
|
|||
delete lowercaseTagMap[normalizedName];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
}, [])
|
||||
// Sort by name first
|
||||
.sort((a, b) => b.name.toLowerCase() > a.name.toLowerCase())
|
||||
// Sort by count
|
||||
.sort((a, b) => b.count - a.count);
|
||||
return [{ name: 'all', count: people.length }, ...normalizedTags];
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +118,7 @@ const normalizedTagMap = tags().reduce((acc, tag) => {
|
|||
}, {});
|
||||
|
||||
export function getPeople(tag?: string) {
|
||||
return people
|
||||
return [...people]
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.map((person) => {
|
||||
const normalizedPerson = {
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
/* Fonts */
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
src: url('../src/fonts/fira_mono-regular-webfont.woff2') format('woff2'),
|
||||
url('../src/fonts/fira_mono-regular-webfont.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Fira Mono';
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
src: url('../src/fonts/fira_mono-regular_italic-webfont.woff2') format('woff2'), url('../src/fonts/fira_mono-regular_italic-webfont.woff') format('woff');
|
||||
font-display: swap;
|
||||
}
|
||||
Loading…
Reference in a new issue