diff --git a/.changesets/10441.md b/.changesets/10441.md
new file mode 100644
index 000000000000..de04d1ae4272
--- /dev/null
+++ b/.changesets/10441.md
@@ -0,0 +1,10 @@
+- feat(og-gen): Update implementation of useLocation | Update App template (#10441) by @dac09
+**Updated App.tsx template**
+We modified the `App.tsx` template to accept possible children, and render them if present. This lets the og:image handler inject your component into the Document tree, without including the entire Router, but still style your og:image component using whatever you used to style the rest of your app (Tailwind, perhaps?)
+
+**Updated useLocation implementation**
+We also modified the `useLocation()` hook to now return everything that the [URL API](https://developer.mozilla.org/en-US/docs/Web/API/URL) returns. Previously it only returned three attributes of the url (pathname, search, hash), now it returns everything available to a call to `new URL()` (origin, href, searchParams, etc.).
+
+The reason for this is now that we have SSR, we can get access to more details in the hook - in this case we needed origin
+
+Both changes should be non-breaking!
diff --git a/__fixtures__/empty-project/web/src/App.tsx b/__fixtures__/empty-project/web/src/App.tsx
index 97fb5e02520d..ad3ce3697d95 100644
--- a/__fixtures__/empty-project/web/src/App.tsx
+++ b/__fixtures__/empty-project/web/src/App.tsx
@@ -6,11 +6,11 @@ import Routes from 'src/Routes'
import './index.css'
-const App = () => (
+const App = ({ children }) => (
-
+ {children ? children : }
diff --git a/__fixtures__/test-project/web/src/App.tsx b/__fixtures__/test-project/web/src/App.tsx
index 65419d60c7d6..0c5a48d728bf 100644
--- a/__fixtures__/test-project/web/src/App.tsx
+++ b/__fixtures__/test-project/web/src/App.tsx
@@ -1,3 +1,5 @@
+import type { ReactNode } from 'react'
+
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
@@ -8,13 +10,16 @@ import { AuthProvider, useAuth } from './auth'
import './scaffold.css'
import './index.css'
+interface AppProps {
+ children?: ReactNode
+}
-const App = () => (
+const App = ({ children }: AppProps) => (
-
+ {children ? children : }
diff --git a/packages/create-redwood-app/templates/js/web/src/App.jsx b/packages/create-redwood-app/templates/js/web/src/App.jsx
index 97fb5e02520d..ad3ce3697d95 100644
--- a/packages/create-redwood-app/templates/js/web/src/App.jsx
+++ b/packages/create-redwood-app/templates/js/web/src/App.jsx
@@ -6,11 +6,11 @@ import Routes from 'src/Routes'
import './index.css'
-const App = () => (
+const App = ({ children }) => (
-
+ {children ? children : }
diff --git a/packages/create-redwood-app/templates/ts/web/src/App.tsx b/packages/create-redwood-app/templates/ts/web/src/App.tsx
index 97fb5e02520d..235df87826da 100644
--- a/packages/create-redwood-app/templates/ts/web/src/App.tsx
+++ b/packages/create-redwood-app/templates/ts/web/src/App.tsx
@@ -1,3 +1,5 @@
+import type { ReactNode } from 'react'
+
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
@@ -5,12 +7,15 @@ import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
import './index.css'
+interface AppProps {
+ children?: ReactNode
+}
-const App = () => (
+const App = ({ children }: AppProps) => (
-
+ {children ? children : }
diff --git a/packages/ogimage-gen/package.json b/packages/ogimage-gen/package.json
index ea11714f82a0..798ec6b1e4c4 100644
--- a/packages/ogimage-gen/package.json
+++ b/packages/ogimage-gen/package.json
@@ -19,7 +19,6 @@
"files": [
"dist",
"cjsWrappers"
-
],
"scripts": {
"build": "tsx ./build.mts && yarn build:types",
diff --git a/packages/prerender/src/runPrerender.tsx b/packages/prerender/src/runPrerender.tsx
index ef3fc172ab8c..dd7316bd531d 100644
--- a/packages/prerender/src/runPrerender.tsx
+++ b/packages/prerender/src/runPrerender.tsx
@@ -124,8 +124,16 @@ async function recursivelyRender(
}),
)
+ // `renderPath` is *just* a path, but the LocationProvider needs a full URL
+ // object so if you need the domain to be something specific when
+ // pre-rendering (because you're showing it in HTML output or the og:image
+ // uses useLocation().host) you can set the RWJS_PRERENDER_ORIGIN env variable
+ // so that it doesn't just default to localhost
+ const prerenderUrl =
+ process.env.RWJS_PRERENDER_ORIGIN || 'http://localhost' + renderPath
+
const componentAsHtml = ReactDOMServer.renderToString(
-
+
diff --git a/packages/router/src/location.tsx b/packages/router/src/location.tsx
index 875f96434121..b3a3e3e1286b 100644
--- a/packages/router/src/location.tsx
+++ b/packages/router/src/location.tsx
@@ -4,19 +4,12 @@ import { createNamedContext } from './createNamedContext'
import { gHistory } from './history'
import type { TrailingSlashesTypes } from './util'
-export interface LocationContextType {
- pathname: string
- search?: string
- hash?: string
-}
+export interface LocationContextType extends URL {}
const LocationContext = createNamedContext('Location')
-interface Location {
- pathname: string
- search?: string
- hash?: string
-}
+interface Location extends URL {}
+
interface LocationProviderProps {
location?: Location
trailingSlashes?: TrailingSlashesTypes
@@ -24,7 +17,7 @@ interface LocationProviderProps {
}
interface LocationProviderState {
- context: Location
+ context: Location | undefined
}
class LocationProvider extends React.Component<
@@ -75,18 +68,10 @@ class LocationProvider extends React.Component<
break
}
- windowLocation = window.location
- } else {
- windowLocation = {
- pathname: this.context?.pathname || '',
- search: this.context?.search || '',
- hash: this.context?.hash || '',
- }
+ windowLocation = new URL(window.location.href)
}
- const { pathname, search, hash } = this.props.location || windowLocation
-
- return { pathname, search, hash }
+ return this.props.location || this.context || windowLocation
}
componentDidMount() {
@@ -94,8 +79,8 @@ class LocationProvider extends React.Component<
const context = this.getContext()
this.setState((lastState) => {
if (
- context.pathname !== lastState.context.pathname ||
- context.search !== lastState.context.search
+ context?.pathname !== lastState?.context?.pathname ||
+ context?.search !== lastState?.context?.search
) {
globalThis?.scrollTo(0, 0)
}
diff --git a/packages/vite/src/streaming/createReactStreamingHandler.ts b/packages/vite/src/streaming/createReactStreamingHandler.ts
index ef7abfb82873..05494596bb0b 100644
--- a/packages/vite/src/streaming/createReactStreamingHandler.ts
+++ b/packages/vite/src/streaming/createReactStreamingHandler.ts
@@ -64,13 +64,13 @@ export const createReactStreamingHandler = async (
let currentRoute: RWRouteManifestItem | undefined
let parsedParams: any = {}
- const { pathname: currentPathName } = new URL(req.url)
+ const currentUrl = new URL(req.url)
// @TODO validate this is correct
for (const route of routes) {
const { match, ...rest } = matchPath(
route.pathDefinition,
- currentPathName,
+ currentUrl.pathname,
)
if (match) {
currentRoute = route
@@ -174,7 +174,7 @@ export const createReactStreamingHandler = async (
{
ServerEntry,
FallbackDocument,
- currentPathName,
+ currentUrl,
metaTags,
cssLinks,
isProd,
diff --git a/packages/vite/src/streaming/streamHelpers.ts b/packages/vite/src/streaming/streamHelpers.ts
index 65878a9e8116..9ae897e0ab89 100644
--- a/packages/vite/src/streaming/streamHelpers.ts
+++ b/packages/vite/src/streaming/streamHelpers.ts
@@ -28,7 +28,7 @@ import { createServerInjectionTransform } from './transforms/serverInjectionTran
interface RenderToStreamArgs {
ServerEntry: ServerEntryType
FallbackDocument: React.FunctionComponent
- currentPathName: string
+ currentUrl: URL
metaTags: TagDescriptor[]
cssLinks: string[]
isProd: boolean
@@ -64,7 +64,7 @@ export async function reactRenderToStreamResponse(
const {
ServerEntry,
FallbackDocument,
- currentPathName,
+ currentUrl,
metaTags,
cssLinks,
isProd,
@@ -103,7 +103,7 @@ export async function reactRenderToStreamResponse(
const timeoutTransform = createTimeoutTransform(timeoutHandle)
- const renderRoot = (path: string) => {
+ const renderRoot = (url: URL) => {
return React.createElement(
ServerAuthProvider,
{
@@ -112,9 +112,7 @@ export async function reactRenderToStreamResponse(
React.createElement(
LocationProvider,
{
- location: {
- pathname: path,
- },
+ location: url,
},
React.createElement(
ServerHtmlProvider,
@@ -157,7 +155,7 @@ export async function reactRenderToStreamResponse(
},
}
- const root = renderRoot(currentPathName)
+ const root = renderRoot(currentUrl)
const reactStream: ReactDOMServerReadableStream =
await renderToReadableStream(root, renderToStreamOptions)
diff --git a/packages/web/src/components/FetchConfigProvider.test.tsx b/packages/web/src/components/__tests__/FetchConfigProvider.test.tsx
similarity index 95%
rename from packages/web/src/components/FetchConfigProvider.test.tsx
rename to packages/web/src/components/__tests__/FetchConfigProvider.test.tsx
index edcafd975501..adc551689f04 100644
--- a/packages/web/src/components/FetchConfigProvider.test.tsx
+++ b/packages/web/src/components/__tests__/FetchConfigProvider.test.tsx
@@ -7,7 +7,7 @@ import type { AuthContextInterface } from '@redwoodjs/auth'
globalThis.RWJS_API_GRAPHQL_URL = 'https://api.example.com/graphql'
-import { FetchConfigProvider, useFetchConfig } from './FetchConfigProvider.js'
+import { FetchConfigProvider, useFetchConfig } from '../FetchConfigProvider.js'
const FetchConfigToString: React.FunctionComponent = () => {
const c = useFetchConfig()
diff --git a/packages/web/src/components/GraphQLHooksProvider.test.tsx b/packages/web/src/components/__tests__/GraphQLHooksProvider.test.tsx
similarity index 99%
rename from packages/web/src/components/GraphQLHooksProvider.test.tsx
rename to packages/web/src/components/__tests__/GraphQLHooksProvider.test.tsx
index e3d0e7b64ddc..fe9e4e949107 100644
--- a/packages/web/src/components/GraphQLHooksProvider.test.tsx
+++ b/packages/web/src/components/__tests__/GraphQLHooksProvider.test.tsx
@@ -8,7 +8,7 @@ import {
useQuery,
useMutation,
useSubscription,
-} from './GraphQLHooksProvider'
+} from '../GraphQLHooksProvider'
const TestUseQueryHook: React.FunctionComponent = () => {
// @ts-expect-error - Purposefully not passing in a DocumentNode type here.
diff --git a/packages/web/src/components/Metadata.test.tsx b/packages/web/src/components/__tests__/Metadata.test.tsx
similarity index 99%
rename from packages/web/src/components/Metadata.test.tsx
rename to packages/web/src/components/__tests__/Metadata.test.tsx
index 21746517f4bb..c0523b851370 100644
--- a/packages/web/src/components/Metadata.test.tsx
+++ b/packages/web/src/components/__tests__/Metadata.test.tsx
@@ -3,7 +3,7 @@ import React from 'react'
import { render } from '@testing-library/react'
import { describe, beforeAll, it, expect } from 'vitest'
-import { Metadata } from './Metadata.js'
+import { Metadata } from '../Metadata.js'
// DOCS: can return a structured object from the database and just give it to `og` and it works
diff --git a/packages/web/src/components/portalHead.test.tsx b/packages/web/src/components/__tests__/PortalHead.test.tsx
similarity index 93%
rename from packages/web/src/components/portalHead.test.tsx
rename to packages/web/src/components/__tests__/PortalHead.test.tsx
index cf35a4d229f0..97ac115de972 100644
--- a/packages/web/src/components/portalHead.test.tsx
+++ b/packages/web/src/components/__tests__/PortalHead.test.tsx
@@ -3,8 +3,8 @@ import React from 'react'
import { render } from '@testing-library/react'
import { vi, describe, it, expect } from 'vitest'
-import PortalHead from './PortalHead.js'
-import * as ServerInject from './ServerInject.js'
+import PortalHead from '../PortalHead.js'
+import * as ServerInject from '../ServerInject.js'
const serverInsertionHookSpy = vi
.spyOn(ServerInject, 'useServerInsertedHTML')