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')