Skip to content

Commit c8ade33

Browse files
committed
Hono Server added to the project + seo
1 parent 3ae639f commit c8ade33

9 files changed

Lines changed: 675 additions & 252 deletions

File tree

.vscode/settings.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"editor.defaultFormatter": "biomejs.biome",
32
"editor.formatOnSave": true,
43
"editor.formatOnType": false,
54
"editor.renderWhitespace": "all",
@@ -25,10 +24,10 @@
2524
"[yaml]": {
2625
"editor.defaultFormatter": "redhat.vscode-yaml"
2726
},
28-
"[typescriptreact]": {
27+
"biome.enabled": true,
28+
"editor.defaultFormatter": "biomejs.biome",
29+
"[javascript][typescript][typescriptreact][javascriptreact][json][jsonc][vue][astro][svelte][css][graphql]": {
2930
"editor.defaultFormatter": "biomejs.biome"
3031
},
31-
"[typescript]": {
32-
"editor.defaultFormatter": "biomejs.biome"
33-
}
32+
3433
}

app/entry.server.tsx

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
1-
import { resolve } from "node:path"
1+
22
import { PassThrough } from "node:stream"
3-
import type { EntryContext } from "@remix-run/node"
3+
import type { AppLoadContext, EntryContext } from "@remix-run/node"
44
import { RemixServer } from "@remix-run/react"
5-
import { Response } from "@remix-run/web-fetch"
5+
import type { Context } from "hono"
66
import { createInstance } from "i18next"
77
import Backend from "i18next-fs-backend"
88
import { isbot } from "isbot"
99
import { renderToPipeableStream } from "react-dom/server"
1010
import { I18nextProvider, initReactI18next } from "react-i18next"
11+
import { createHonoServer } from "react-router-hono-server/node"
12+
import { i18next } from "remix-hono/i18next"
1113
import i18n from "./localization/i18n" // your i18n configuration file
12-
import i18next, { returnLanguageFromRequest } from "./localization/i18n.server"
14+
import i18nextOpts from "./localization/i18n.server"
1315
import { resources } from "./localization/resource"
16+
import { getClientEnv, initEnv } from "./server/env.server"
1417

1518
const ABORT_DELAY = 5000
1619

1720
export default async function handleRequest(
1821
request: Request,
1922
responseStatusCode: number,
2023
responseHeaders: Headers,
21-
remixContext: EntryContext
24+
remixContext: EntryContext,
25+
appContext: AppLoadContext
2226
) {
2327
const callbackName = isbot(request.headers.get("user-agent")) ? "onAllReady" : "onShellReady"
2428
const instance = createInstance()
25-
const lng = await returnLanguageFromRequest(request)
26-
const ns = i18next.getRouteNamespaces(remixContext)
29+
const lng = appContext.lang;
30+
const ns = i18nextOpts.getRouteNamespaces(remixContext)
2731

2832
await instance
2933
.use(initReactI18next) // Tell our instance to use react-i18next
@@ -49,7 +53,8 @@ export default async function handleRequest(
4953
responseHeaders.set("Content-Type", "text/html")
5054

5155
resolve(
52-
new Response(body, {
56+
// @ts-expect-error - We purposely do not define the body as existent so it's not used inside loaders as it's injected there as well
57+
appContext.body(body, {
5358
headers: responseHeaders,
5459
status: didError ? 500 : responseStatusCode,
5560
})
@@ -71,3 +76,42 @@ export default async function handleRequest(
7176
setTimeout(abort, ABORT_DELAY)
7277
})
7378
}
79+
80+
81+
// Code below used to initialize our own Hono server!
82+
// Setup the .env vars
83+
const env = initEnv()
84+
85+
const getLoadContext = async (c: Context) => {
86+
// get the locale from the context
87+
const locale = i18next.getLocale(c)
88+
// get t function for the default namespace
89+
const t = await i18next.getFixedT(c)
90+
91+
const clientEnv = getClientEnv()
92+
return {
93+
lang: locale,
94+
t,
95+
env,
96+
clientEnv,
97+
// We do not add this to AppLoadContext type because it's not needed in the loaders, but it's used above to handle requests
98+
body: c.body,
99+
}
100+
}
101+
102+
interface LoadContext extends Awaited<ReturnType<typeof getLoadContext>> {}
103+
104+
/**
105+
* Declare our loaders and actions context type
106+
*/
107+
declare module "@remix-run/node" {
108+
interface AppLoadContext extends Omit<LoadContext, "body"> {}
109+
}
110+
111+
export const server = await createHonoServer({
112+
configure(server) {
113+
server.use("*", i18next(i18nextOpts))
114+
},
115+
defaultLogger: false,
116+
getLoadContext,
117+
})

app/library/icon/icons/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// This file is generated by icon spritesheet generator
22

3-
export const iconNames = ["ShoppingCart"] as const
3+
export const iconNames = [
4+
"ShoppingCart",
5+
] as const
46

5-
export type IconName = (typeof iconNames)[number]
7+
export type IconName = typeof iconNames[number]

app/localization/i18n.server.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { resolve } from "node:path"
22
import { RemixI18Next } from "remix-i18next/server"
33
import i18n from "~/localization/i18n" // your i18n configuration file
4-
import type { Language } from "~/localization/resource"
54

65
const i18next = new RemixI18Next({
76
detection: {
@@ -19,8 +18,3 @@ const i18next = new RemixI18Next({
1918
})
2019

2120
export default i18next
22-
23-
export const returnLanguageFromRequest = async (request: Request) => {
24-
const lang = await i18next.getLocale(request)
25-
return lang as Language
26-
}

app/root.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,25 @@ import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from "
44
import { useTranslation } from "react-i18next"
55
import { useChangeLanguage } from "remix-i18next/react"
66
import { LanguageSwitcher } from "./library/language-switcher"
7-
import { returnLanguageFromRequest } from "./localization/i18n.server"
87
import tailwindcss from "./tailwind.css?url"
98

10-
export async function loader({ request }: LoaderFunctionArgs) {
11-
const locale = await returnLanguageFromRequest(request)
12-
return json({ locale })
9+
export async function loader({ context: { lang, clientEnv } }: LoaderFunctionArgs) {
10+
return json({ lang, clientEnv })
1311
}
1412

1513
export const links: LinksFunction = () => [{ rel: "stylesheet", href: tailwindcss }]
1614

1715
export const handle = {
18-
// In the handle export, we can add a i18n key with namespaces our route
19-
// will need to load. This key can be a single string or an array of strings.
20-
// TIP: In most cases, you should set this to your defaultNS from your i18n config
21-
// or if you did not set one, set it to the i18next default namespace "translation"
2216
i18n: "common",
2317
}
2418

2519
export default function App() {
26-
const { locale } = useLoaderData<typeof loader>()
20+
const { lang, clientEnv } = useLoaderData<typeof loader>()
2721
const { i18n } = useTranslation()
28-
useChangeLanguage(locale)
22+
useChangeLanguage(lang)
2923

3024
return (
31-
<html className="overflow-y-auto overflow-x-hidden" lang={locale} dir={i18n.dir()}>
25+
<html className="overflow-y-auto overflow-x-hidden" lang={lang} dir={i18n.dir()}>
3226
<head>
3327
<meta charSet="utf-8" />
3428
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -40,6 +34,8 @@ export default function App() {
4034
<Outlet />
4135
<ScrollRestoration />
4236
<Scripts />
37+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: We set the window.env variable to the client env */}
38+
<script dangerouslySetInnerHTML={{ __html: `window.env = ${JSON.stringify(clientEnv)}` }} />
4339
</body>
4440
</html>
4541
)

app/server/env.server.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { z } from "zod"
2+
3+
const envSchema = z.object({
4+
NODE_ENV: z.enum(["development", "production", "test"]),
5+
6+
})
7+
8+
type APP_ENV = z.infer<typeof envSchema>
9+
let env: APP_ENV
10+
/**
11+
* Helper method used for initializing .env vars in your entry.server.ts file. It uses
12+
* zod to validate your .env and throws if it's not valid.
13+
* @returns Initialized env vars
14+
*/
15+
export const initEnv = () => {
16+
const envData = envSchema.safeParse(process.env)
17+
18+
if (!envData.success) {
19+
console.error("❌ Invalid environment variables:", envData.error.flatten().fieldErrors)
20+
throw new Error("Invalid environment variables")
21+
}
22+
23+
env = envData.data
24+
25+
// Do not log the message when running tests
26+
if (env.NODE_ENV !== "test") {
27+
console.log("✅ Environment variables loaded successfully")
28+
}
29+
return envData.data
30+
}
31+
32+
/**
33+
* Helper method for you to return client facing .env vars, only return vars that are needed on the client.
34+
* Otherwise you would expose your server vars to the client if you returned them from here as this is
35+
* directly sent in the root to the client and set on the window.env
36+
* @returns Subset of the whole process.env to be passed to the client and used there
37+
*/
38+
export const getClientEnv = () => {
39+
const serverEnv = env
40+
return {
41+
NODE_ENV: serverEnv.NODE_ENV,
42+
}
43+
}
44+
45+
type CLIENT_ENV = ReturnType<typeof getClientEnv>
46+
47+
declare global {
48+
interface Window {
49+
env: CLIENT_ENV
50+
}
51+
namespace NodeJS {
52+
interface ProcessEnv extends APP_ENV {}
53+
}
54+
}

0 commit comments

Comments
 (0)