-
Notifications
You must be signed in to change notification settings - Fork 464
Closed
Description
When creating an app that has a layout variation on one route, I ended up having to have multiple copies of _layout.tsx in multiple folders (symlinks would not work as the real path is resolved) for multiple reasons.
Perhaps /routes could look a bit more like the following to make it trivial to update the layout and app specific content while still pulling in changes from this repo. (see the legal-notice and uses-different-layout routes)
/app/routes
├── $.tsx
├── _layout.tsx
├── _auth+
│ ├── auth.$provider.callback.test.ts
│ ├── auth.$provider.callback.ts
│ ├── auth.$provider.ts
│ ├── forgot-password.tsx
│ ├── login.server.ts
│ ├── login.tsx
│ ├── logout.tsx
│ ├── onboarding.server.ts
│ ├── onboarding.tsx
│ ├── onboarding_.$provider.server.ts
│ ├── onboarding_.$provider.tsx
│ ├── reset-password.server.ts
│ ├── reset-password.tsx
│ ├── signup.tsx
│ ├── verify.server.ts
│ └── verify.tsx
├── _home+
│ ├── index.tsx
│ └── tailwind-preset.ts
├── _seo+
│ ├── robots[.]txt.ts
│ └── sitemap[.]xml.ts
├── about
│ ├── index.tsx
├── admin+
│ ├── cache.tsx
│ ├── cache_.lru.$cacheKey.ts
│ ├── cache_.sqlite.$cacheKey.ts
│ ├── cache_.sqlite.server.ts
│ └── cache_.sqlite.tsx
├── api+
│ └── report-issue.tsx
├── uses-different-layout_+
│ ├── _layout.tsx
│ └── index.tsx
├── legal-notice_+
│ ├── _layout.tsx
│ ├── acceptable-use.tsx
│ ├── community-guidelines.tsx
│ ├── cookie.tsx
│ ├── data-subject-access-request.tsx
│ ├── disclaimer.tsx
│ ├── eula.tsx
│ ├── index.tsx
│ ├── privacy.tsx
│ ├── refund.tsx
│ └── terms-of-service.tsx
├── me.tsx
├── resources+
│ ├── download-user-data.tsx
│ ├── healthcheck.tsx
│ ├── theme-switch.tsx
│ └── user-images.$imageId.tsx
├── support.tsx
├── settings+
│ ├── profile.change-email.server.tsx
│ ├── profile.change-email.tsx
│ ├── profile.connections.tsx
│ ├── profile.index.tsx
│ ├── profile.password.tsx
│ ├── profile.password_.create.tsx
│ ├── profile.photo.tsx
│ ├── profile.tsx
│ ├── profile.two-factor.disable.tsx
│ ├── profile.two-factor.index.tsx
│ ├── profile.two-factor.tsx
│ └── profile.two-factor.verify.tsx
└── users+
├── $username.test.tsx
├── $username.tsx
├── $username_+
└── index.tsxThen remove all layout from root.tsx and perhaps add a cookie consent
import {
type HeadersFunction,
json,
type LinksFunction,
type LoaderFunctionArgs,
type MetaFunction,
} from '@remix-run/node'
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from '@remix-run/react'
import { withSentry } from '@sentry/remix'
import * as cookie from 'cookie'
import leafletStyleSheetUrl from 'leaflet/dist/leaflet.css?url'
import React from 'react'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import {
COOKIE_NAME,
type CookiePreferences,
} from '#app/utils/cookie-consent.ts'
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
import faviconAssetUrl from './assets/favicons/favicon.svg'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
import { EpicProgress } from './components/progress-bar.tsx'
import { useToast } from './components/toaster.tsx'
import { EpicToaster } from './components/ui/sonner.tsx'
import { useTheme } from './routes/resources+/theme-switch.tsx'
import tailwindStyleSheetUrl from './styles/tailwind.css?url'
import { getUserId, logout } from './utils/auth.server.ts'
import { ClientHintCheck, getHints } from './utils/client-hints.tsx'
import { prisma } from './utils/db.server.ts'
import { getEnv } from './utils/env.server.ts'
import { honeypot } from './utils/honeypot.server.ts'
import { combineHeaders, getDomainUrl } from './utils/misc.tsx'
import { useNonce } from './utils/nonce-provider.ts'
import { getTheme, type Theme } from './utils/theme.server.ts'
import { makeTimings, time } from './utils/timing.server.ts'
import { getToast } from './utils/toast.server.ts'
export const links: LinksFunction = () => {
return [
{
rel: 'icon',
href: '/favicon.ico',
sizes: '48x48',
},
{ rel: 'icon', type: 'image/svg+xml', href: faviconAssetUrl },
{ rel: 'apple-touch-icon', href: appleTouchIconAssetUrl },
{
rel: 'manifest',
href: '/site.webmanifest',
crossOrigin: 'use-credentials',
} as const,
{ rel: 'stylesheet', href: leafletStyleSheetUrl, as: 'style' },
{ rel: 'stylesheet', href: tailwindStyleSheetUrl },
].filter(Boolean)
}
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data ? 'App Name' : 'Error | App Name' },
{ name: 'description', content: `Your own captain's log` },
]
}
export async function loader({ request }: LoaderFunctionArgs) {
const timings = makeTimings('root loader')
const userId = await time(() => getUserId(request), {
timings,
type: 'getUserId',
desc: 'getUserId in root',
})
const user = userId
? await time(
() =>
prisma.user.findUniqueOrThrow({
select: {
id: true,
name: true,
username: true,
email: true,
image: { select: { id: true } },
roles: {
select: {
name: true,
permissions: {
select: { entity: true, action: true, access: true },
},
},
},
},
where: { id: userId },
}),
{ timings, type: 'find user', desc: 'find user in root' },
)
: null
if (userId && !user) {
console.info('something weird happened')
await logout({ request, redirectTo: '/' })
}
const { toast, headers: toastHeaders } = await getToast(request)
const honeyProps = honeypot.getInputProps()
let cookieConsent: CookiePreferences = {
preferences: false,
statistics: false,
marketing: false,
}
const cookieHeader = request.headers.get('cookie')
if (cookieHeader) {
const consentCookie = cookie.parse(cookieHeader)[COOKIE_NAME]
if (consentCookie) {
try {
cookieConsent = JSON.parse(
decodeURIComponent(consentCookie),
) as CookiePreferences
} catch {
cookieConsent = {
preferences: false,
statistics: false,
marketing: false,
}
}
}
}
return json(
{
user,
requestInfo: {
hints: getHints(request),
origin: getDomainUrl(request),
path: new URL(request.url).pathname,
userPrefs: {
theme: getTheme(request),
cookieConsent,
},
},
ENV: getEnv(),
toast,
honeyProps,
},
{
headers: combineHeaders(
{ 'Server-Timing': timings.toString() },
toastHeaders,
),
},
)
}
export const headers: HeadersFunction = ({ loaderHeaders }) => {
return {
'Server-Timing': loaderHeaders.get('Server-Timing') ?? '',
}
}
function Document({
children,
nonce,
theme = 'light',
env = {},
allowIndexing = true,
}: {
children: React.ReactNode
nonce: string
theme?: Theme
env?: Record<string, string>
allowIndexing?: boolean
}) {
return (
<html lang="en" className={`${theme} h-full overflow-x-hidden`}>
<head>
<ClientHintCheck nonce={nonce} />
<Meta />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
{allowIndexing ? null : (
<meta name="robots" content="noindex, nofollow" />
)}
<title>App Name</title>
<Links />
</head>
<body className="bg-background text-foreground">
{children}
<EpicToaster closeButton position="top-center" theme={theme} />
<EpicProgress />
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(env)}`,
}}
/>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
)
}
function App() {
const data = useLoaderData<typeof loader>()
const nonce = useNonce()
const theme = useTheme()
const allowIndexing = data.ENV.ALLOW_INDEXING !== 'false'
useToast(data.toast)
return (
<Document
nonce={nonce}
theme={theme}
allowIndexing={allowIndexing}
env={data.ENV}
>
<Outlet context={{ theme: data.requestInfo.userPrefs.theme }} />
</Document>
)
}
function AppWithProviders() {
const data = useLoaderData<typeof loader>()
return (
<HoneypotProvider {...data.honeyProps}>
<App />
</HoneypotProvider>
)
}
export default withSentry(AppWithProviders)
export function ErrorBoundary() {
const nonce = useNonce()
return (
<Document nonce={nonce}>
<GeneralErrorBoundary />
</Document>
)
}Metadata
Metadata
Assignees
Labels
No labels