diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..97d817a4c --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +BACKEND_URL= +NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID= +NEXT_PUBLIC_ALGOLIA_APP_ID= +NEXT_PUBLIC_ALGOLIA_API_KEY= +NEXT_PUBLIC_ALGOLIA_INDEX= +NEWSLETTER_BASE_URL= diff --git a/.env.local b/.env.local deleted file mode 100644 index a496da6f9..000000000 --- a/.env.local +++ /dev/null @@ -1,5 +0,0 @@ -NEXT_PUBLIC_BACKEND_URL=https://profilio-staging.sandbox-london-b.fetch-ai.com -NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID=G-GYC9HYF1LD -NEXT_PUBLIC_ALGOLIA_APP_ID=J27DIPDG4S -NEXT_PUBLIC_ALGOLIA_API_KEY=601cad4cf7041d99c1bdf42f4d4843d6 -NEXT_PUBLIC_ALGOLIA_INDEX=12-6-24-index \ No newline at end of file diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 0a42e5ab1..a448da5a2 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -43,9 +43,6 @@ jobs: NEXT_PUBLIC_ALGOLIA_API_KEY: ${{ secrets.NEXT_PUBLIC_ALGOLIA_API_KEY }} NEXT_PUBLIC_ALGOLIA_INDEX: ${{ secrets.NEXT_PUBLIC_ALGOLIA_INDEX }} NEWSLETTER_BASE_URL: ${{ secrets.NEWSLETTER_BASE_URL }} - NEXT_PUBLIC_FETCH_ACCOUNTS_URL: ${{ secrets.NEXT_PUBLIC_FETCH_ACCOUNTS_URL }} - NEXT_PUBLIC_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_CLIENT_ID }} - NEXT_PUBLIC_COOKIE_PASSWORD: ${{ secrets.NEXT_PUBLIC_COOKIE_PASSWORD }} - name: Trigger Image Update run: | diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 8358c2ee7..e07ad8b3b 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -43,9 +43,6 @@ jobs: NEXT_PUBLIC_ALGOLIA_API_KEY: ${{ secrets.NEXT_PUBLIC_ALGOLIA_API_KEY }} NEXT_PUBLIC_ALGOLIA_INDEX: ${{ secrets.NEXT_PUBLIC_ALGOLIA_INDEX }} NEWSLETTER_BASE_URL: ${{ secrets.NEWSLETTER_BASE_URL }} - NEXT_PUBLIC_FETCH_ACCOUNTS_URL: ${{ secrets.NEXT_PUBLIC_FETCH_ACCOUNTS_URL }} - NEXT_PUBLIC_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_CLIENT_ID }} - NEXT_PUBLIC_COOKIE_PASSWORD: ${{ secrets.NEXT_PUBLIC_COOKIE_PASSWORD }} deploy: name: Deployment diff --git a/.github/workflows/ephemeral-deploy.yaml b/.github/workflows/ephemeral-deploy.yaml index d391e3d5e..634c16e24 100644 --- a/.github/workflows/ephemeral-deploy.yaml +++ b/.github/workflows/ephemeral-deploy.yaml @@ -44,10 +44,6 @@ jobs: NEXT_PUBLIC_ALGOLIA_API_KEY: ${{ secrets.NEXT_PUBLIC_ALGOLIA_API_KEY }} NEXT_PUBLIC_ALGOLIA_INDEX: ${{ secrets.NEXT_PUBLIC_ALGOLIA_INDEX }} NEWSLETTER_BASE_URL: ${{ secrets.NEWSLETTER_BASE_URL }} - NEXT_PUBLIC_FETCH_ACCOUNTS_URL: ${{ secrets.NEXT_PUBLIC_FETCH_ACCOUNTS_URL }} - NEXT_PUBLIC_CLIENT_ID: ${{ secrets.NEXT_PUBLIC_CLIENT_ID }} - NEXT_PUBLIC_COOKIE_PASSWORD: ${{ secrets.NEXT_PUBLIC_COOKIE_PASSWORD }} - deploy: name: Ephermeral Deployment runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index 8225e50ee..7a7760558 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,6 @@ ARG NEXT_PUBLIC_ALGOLIA_APP_ID="" ARG NEXT_PUBLIC_ALGOLIA_API_KEY="" ARG NEXT_PUBLIC_ALGOLIA_INDEX="" ARG NEWSLETTER_BASE_URL="" -ARG NEXT_PUBLIC_FETCH_ACCOUNTS_URL="" -ARG NEXT_PUBLIC_CLIENT_ID="" -ARG NEXT_PUBLIC_COOKIE_PASSWORD="" RUN apk add tree && corepack prepare pnpm@8.6.10 --activate && corepack enable @@ -27,9 +24,6 @@ RUN echo BACKEND_URL="${BACKEND_URL}" > .env.local && \ echo NEXT_PUBLIC_ALGOLIA_API_KEY="${NEXT_PUBLIC_ALGOLIA_API_KEY}" >> .env.local && \ echo NEXT_PUBLIC_ALGOLIA_INDEX="${NEXT_PUBLIC_ALGOLIA_INDEX}" >> .env.local && \ echo NEWSLETTER_BASE_URL="${NEWSLETTER_BASE_URL}" >> .env.local && \ - echo NEXT_PUBLIC_FETCH_ACCOUNTS_URL="${NEXT_PUBLIC_FETCH_ACCOUNTS_URL}" >> .env.local && \ - echo NEXT_PUBLIC_CLIENT_ID="${NEXT_PUBLIC_CLIENT_ID}" >> .env.local && \ - echo NEXT_PUBLIC_COOKIE_PASSWORD="${NEXT_PUBLIC_COOKIE_PASSWORD}" >> .env.local && \ pnpm build ENTRYPOINT ["pnpm"] diff --git a/components/api-endpoint.tsx b/components/api-endpoint.tsx index db4a494a8..a961260e2 100644 --- a/components/api-endpoint.tsx +++ b/components/api-endpoint.tsx @@ -9,10 +9,8 @@ import { Tab, DropDownTabs, } from "./mdx"; -import { useUserContext } from "theme/fetch-ai-docs/contexts/context-provider"; import Tooltip from "./tooltip"; import Link from "next/link"; -import fetchJson from "src/lib/fetch-json"; interface PropertyType { name: string; @@ -347,29 +345,27 @@ export const ApiEndpointRequestResponse: React.FC<{ setIsModalOpen(false); }; - const context = useUserContext(); - - const hitRequest = async () => { + const hitRequestWithoutLogin = async () => { try { setLoading(true); setError(""); const requestPayloadJSON = JSON.parse(requestPayload || "{}"); const apiUrlWithParams = (properties.apiUrl + replacePathParameters(properties.path, pathParameters)) as string; - - const response: { data: unknown } = await fetchJson( - `/docs/api/api-requests?url=${apiUrlWithParams}`, - { - method: properties.method, - body: properties.method.includes("GET") ? null : requestPayloadJSON, + const response = await fetch(apiUrlWithParams, { + method: properties.method, + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${bearerToken}`, }, - ); - const responseText = JSON.stringify(response.data, null, 2); + body: properties.method.includes("GET") + ? null + : JSON.stringify(requestPayloadJSON), + }); + const data = await response.json(); + const responseText = JSON.stringify(data, null, 2); setActualResponse(responseText); } catch (error) { - if (error.response.status === 422) { - context.signOut(); - } setError(`Error: ${error.message}`); } finally { setLoading(false); @@ -420,7 +416,7 @@ export const ApiEndpointRequestResponse: React.FC<{
- {!context?.isLoggedIn && isBearerTokenRequired && ( + {isBearerTokenRequired && (

@@ -530,7 +526,7 @@ export const ApiEndpointRequestResponse: React.FC<{

diff --git a/k8s/build-img.py b/k8s/build-img.py index c66539649..3893ef236 100755 --- a/k8s/build-img.py +++ b/k8s/build-img.py @@ -23,9 +23,6 @@ 'NEXT_PUBLIC_ALGOLIA_API_KEY', 'NEXT_PUBLIC_ALGOLIA_INDEX', 'NEWSLETTER_BASE_URL', - 'NEXT_PUBLIC_FETCH_ACCOUNTS_URL', - 'NEXT_PUBLIC_CLIENT_ID', - 'NEXT_PUBLIC_COOKIE_PASSWORD' ) def _profile(text: str) -> str: diff --git a/pages/api/profile.ts b/pages/api/profile.ts deleted file mode 100644 index fb0b60933..000000000 --- a/pages/api/profile.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { withIronSessionApiRoute } from "iron-session/next"; -import { sessionOptions } from "src/core/session"; -import { NextApiRequest, NextApiResponse } from "next"; - -type User = { - email: string; - avatarHref: string; - id: string; - walletAddress?: string; -}; - -type Credentials = { - apiKey: string; - expiresAt: number; - group: string; - sub: string; -}; - -export type CombinedData = { - user?: User; - credentials?: Credentials; - isLoggedIn: boolean; -}; - -async function userRoute( - request: NextApiRequest, - response: NextApiResponse, -) { - response.setHeader("Cache-Control", "no-cache, maxage=0, must-revalidate"); - if (request.session.user || request.session.credentials) { - const user: User = { - email: request.session.user?.email ?? "", - walletAddress: request.session.user?.walletAddress ?? "", - avatarHref: request.session.user?.avatarHref ?? "", - id: request.session.user?.id ?? "", - }; - - const credentials: Credentials = { - apiKey: request.session.credentials?.apiKey ?? "", - expiresAt: request.session.credentials?.expiresAt ?? 0, - group: request.session.credentials?.group ?? "", - sub: request.session.credentials?.sub ?? "", - }; - - const combinedData: CombinedData = { - user, - credentials, - isLoggedIn: true, - }; - - response.json(combinedData); - } else { - response.json({ - isLoggedIn: false, - }); - } -} - -export default withIronSessionApiRoute(userRoute, sessionOptions); diff --git a/pages/api/search.ts b/pages/api/search.ts index bca506776..a2fc5bfc4 100644 --- a/pages/api/search.ts +++ b/pages/api/search.ts @@ -18,8 +18,7 @@ export default async function handler( }); const apiResponse = await response.json(); return res.status(response.status).json({ apiResponse }); - } catch (error) { - console.error(error); + } catch { return res.status(500).json({ error: "Internal Server Error" }); } } diff --git a/pages/api/signout.ts b/pages/api/signout.ts deleted file mode 100644 index ecf6848d6..000000000 --- a/pages/api/signout.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { withIronSessionApiRoute } from "iron-session/next"; -import { sessionOptions } from "src/core/session"; -import { NextApiRequest, NextApiResponse } from "next"; -import { CombinedData } from "./profile"; - -async function userRoute( - request: NextApiRequest, - response: NextApiResponse, -) { - response.setHeader("Cache-Control", "no-cache, maxage=0, must-revalidate"); - request.session.destroy(); - response.json({ - isLoggedIn: false, - user: { email: "", avatarHref: "", id: "" }, - credentials: { apiKey: "", expiresAt: 0, group: "", sub: "" }, - }); -} - -export default withIronSessionApiRoute(userRoute, sessionOptions); diff --git a/pages/auth.tsx b/pages/auth.tsx deleted file mode 100644 index 5d1ea5ffc..000000000 --- a/pages/auth.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from "react"; -import { withIronSessionSsr } from "iron-session/next"; -import { parseJwt } from "src/core/jwt"; -import { getTokenFromAuthCode } from "src/core/fauna"; -import { setCookie } from "cookies-next"; -import { sessionOptions } from "src/core/session"; - -export function flatten(normalized = ""): string { - if (Array.isArray(normalized)) { - return normalized.join(" "); - } - return normalized; -} - -const Page: React.FC = () => { - return <>This is the auth page; -}; - -export const getServerSideProps = withIronSessionSsr(async function ({ - req, - res, - query, -}) { - const authFailure = async () => { - req.session.user; - req.session.credentials; - await req.session.save(); - }; - - const authCode = flatten(query.code as string); - if (authCode === "") { - await authFailure(); - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - const token = await getTokenFromAuthCode(authCode); - setCookie("fauna", token.access_token, { req, res }); - setCookie("refresh_token", token.refresh_token, { req, res }); - - const r = await fetch( - `${process.env.NEXT_PUBLIC_FETCH_ACCOUNTS_URL}/v1/profile`, - { - method: "GET", - headers: { - Authorization: `bearer ${token.access_token}`, - }, - }, - ); - if (!r.ok) { - await authFailure(); - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - const profile = await r.json(); - const email: string | undefined = profile.email; - const walletAddress: string | undefined = profile.wallet_address; - const givenName: string | undefined = profile.given_name; - const familyName: string | undefined = profile.family_name; - const avatarHref: string | undefined = profile.image_url; - const id: string | undefined = profile.client_id; - - const isEmailUser = email !== undefined; - const isWalletUser = walletAddress !== undefined; - const isValidUser = isEmailUser || isWalletUser; - - if (!isValidUser) { - await authFailure(); - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - req.session.user = { - email: email ?? "", - walletAddress: walletAddress ?? "", - avatarHref, - familyName, - givenName, - id, - }; - const metadata = parseJwt(token.access_token); - - req.session.credentials = { - apiKey: token.access_token, - expiresAt: metadata.expiry, - group: metadata.group, - sub: metadata.sub, - }; - - await req.session.save(); - - return { - redirect: { - destination: "/", - permanent: false, - }, - }; -}, sessionOptions); - -export default Page; diff --git a/src/core/fauna.tsx b/src/core/fauna.tsx deleted file mode 100644 index 83d67720f..000000000 --- a/src/core/fauna.tsx +++ /dev/null @@ -1,66 +0,0 @@ -export async function getTokenFromAuthCode(authCode: string): Promise<{ - access_token: string; - refresh_token: string; -}> { - const r = await fetch( - `${process.env.NEXT_PUBLIC_FETCH_ACCOUNTS_URL}/v1/tokens`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - grant_type: "authorization_code", - code: authCode, - client_id: process.env.NEXT_PUBLIC_CLIENT_ID, - }), - }, - ); - - if (!r.ok) { - throw new Error("Failed to get token"); - } - - const { access_token, refresh_token } = await r.json(); - return { - access_token: access_token, - refresh_token: refresh_token, - }; -} - -export async function getNewAccessToken(currentToken: string) { - if (!currentToken) { - return; - } - const r = await fetch( - `${process.env.NEXT_PUBLIC_FETCH_ACCOUNTS_URL}/v1/tokens`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - grant_type: "refresh_token", - refresh_token: currentToken, - }), - }, - ); - - if (!r.ok) { - console.error("Could not request new token."); - return; - } - - const refresh_token = await r.json(); - - if (!refresh_token) { - return; - } - - return { - accessToken: refresh_token.access_token, - expiresIn: (refresh_token.expires_in as number) * 1000, - group: refresh_token.grp, - id: "", - }; -} diff --git a/src/core/jwt.tsx b/src/core/jwt.tsx deleted file mode 100644 index 38ca992a6..000000000 --- a/src/core/jwt.tsx +++ /dev/null @@ -1,15 +0,0 @@ -interface TokenInformation { - expiry: number; - group?: "internal"; - sub: string; -} - -export function parseJwt(token: string): TokenInformation { - const payload = JSON.parse( - Buffer.from(token.split(".")[1], "base64").toString(), - ); - - const expiry = (payload.exp as number) * 1000; - - return { expiry: expiry, group: payload.grp, sub: payload.sub }; -} diff --git a/src/core/session.tsx b/src/core/session.tsx deleted file mode 100644 index 7d475b511..000000000 --- a/src/core/session.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { IronSessionOptions } from "iron-session"; -import { - Credentials, - User, -} from "theme/fetch-ai-docs/contexts/context-provider"; - -export const sessionOptions: IronSessionOptions = { - password: process.env.NEXT_PUBLIC_COOKIE_PASSWORD, - cookieName: process.env.NEXT_PUBLIC_CLIENT_ID, - cookieOptions: { - secure: process.env.NODE_ENV === "production", - }, -}; - -declare module "iron-session" { - interface IronSessionData { - user: User | null; - credentials: Credentials | null; - } -} diff --git a/theme/fetch-ai-docs/contexts/context-provider.tsx b/theme/fetch-ai-docs/contexts/context-provider.tsx deleted file mode 100644 index d638a86e8..000000000 --- a/theme/fetch-ai-docs/contexts/context-provider.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import fetchJson from "src/lib/fetch-json"; -import useSWR, { mutate } from "swr"; -import { deleteCookie } from "cookies-next"; -export interface User { - email: string; - id?: string; - walletAddress?: string; - givenName?: string; - familyName?: string; - avatarHref?: string; -} - -export interface Credentials { - apiKey: string; - expiresAt: number; - group?: "internal"; - sub?: string; -} - -interface UserContextData { - user: User; - credentials: Credentials; -} - -export interface UserContextProperties { - session?: UserContextData; -} - -export interface UserInformation { - user: User; - credentials: Credentials; - signOut(): void; - isLoading: boolean; - isLoggedIn: boolean; - isFetchAccount: boolean; -} - -const DEFAULT_USER: User = { - email: "", - walletAddress: "", -}; - -const DEFAULT_CREDENTIALS: Credentials = { - apiKey: "", - expiresAt: 0, - group: undefined, - sub: undefined, -}; - -export const UserContext = React.createContext({ - user: DEFAULT_USER, - credentials: DEFAULT_CREDENTIALS, - // eslint-disable-next-line unicorn/empty-brace-spaces - signOut: () => {}, - isLoading: false, - isLoggedIn: false, - isFetchAccount: false, -}); - -export function useUserContext() { - return useContext(UserContext); -} - -export const UserInfoProvider: React.FC<{ - children?: React.ReactNode | undefined; -}> = ({ children }) => { - const { data, isLoading } = useSWR("/docs/api/profile", fetchJson); - const context = data as UserInformation; - - const [user, setUser] = useState(context?.user ?? DEFAULT_USER); - const [isFetchAccount, setIsFetchAccount] = useState(false); - const [credentials, setCredentials] = useState( - context?.credentials ?? DEFAULT_CREDENTIALS, - ); - const [isLoggedIn, setIsLoggedIn] = useState(context?.isLoggedIn); - const router = useRouter(); - - const signOut = async () => { - deleteCookie("fauna"); - deleteCookie("refresh_token"); - setUser(DEFAULT_USER); - setCredentials(DEFAULT_CREDENTIALS); - mutate(await fetchJson("/docs/api/signout", { method: "POST" }), { - revalidate: true, - }); - setIsLoggedIn(false); - }; - - const resetUserName = () => { - setUser(DEFAULT_USER); - setCredentials(DEFAULT_CREDENTIALS); - }; - - const userEmail = context?.user?.email; - useEffect(() => { - if ( - context?.user != undefined && - context?.credentials != undefined && - context?.isLoggedIn !== undefined && - context?.credentials?.expiresAt > credentials?.expiresAt && - context?.credentials.expiresAt > Date.now() - ) { - setUser(context?.user); - setCredentials(context?.credentials); - setIsLoggedIn(context?.isLoggedIn); - } else if ( - !context?.credentials || - (context?.credentials?.expiresAt > 0 && - context?.credentials?.expiresAt <= Date.now()) - ) { - resetUserName(); - } - if (userEmail && userEmail.includes("fetch.ai")) { - setIsFetchAccount(true); - } else { - setIsFetchAccount(false); - } - }, [context, router]); - - useEffect(() => { - setUser({ - ...user, - email: context?.user?.email, - walletAddress: context?.user?.walletAddress, - avatarHref: context?.user?.avatarHref, - }); - setCredentials({ - ...credentials, - apiKey: context?.credentials?.apiKey, - expiresAt: context?.credentials?.expiresAt, - sub: context?.credentials?.sub, - }); - setIsLoggedIn(context?.isLoggedIn); - }, [ - context?.credentials?.apiKey, - context?.user?.email, - context?.user?.walletAddress, - context?.isLoggedIn, - ]); - - return ( - - {children} - - ); -}; diff --git a/theme/fetch-ai-docs/helpers.ts b/theme/fetch-ai-docs/helpers.ts index 502b505e2..bfb48080c 100644 --- a/theme/fetch-ai-docs/helpers.ts +++ b/theme/fetch-ai-docs/helpers.ts @@ -1,18 +1,3 @@ -import router from "next/router"; -export const handleSignin = () => { - const currentProtocol = window.location.protocol; - const currentHostname = window.location.hostname; - const currentPort = window.location.port; - const redirectUri = `${currentProtocol}//${currentHostname}:${currentPort}/docs/auth`; - const loginUrl = - `${process.env.NEXT_PUBLIC_FETCH_ACCOUNTS_URL}/login/` + - `?redirect_uri=${encodeURIComponent(redirectUri)}` + - `&client_id=${process.env.NEXT_PUBLIC_CLIENT_ID}` + - `&response_type=code` + - `&sso=`; - router.push(loginUrl); -}; - export const isLinkInResponse = (response) => { const flattenedArray = response?.flat(); const isPresent = flattenedArray?.includes( diff --git a/theme/fetch-ai-docs/index.tsx b/theme/fetch-ai-docs/index.tsx index ed8114fd1..d2ced8369 100644 --- a/theme/fetch-ai-docs/index.tsx +++ b/theme/fetch-ai-docs/index.tsx @@ -28,7 +28,6 @@ import React from "react"; import FeedbackComponent from "components/feedback"; import type { Item } from "nextra/normalize-pages"; import { setCookie } from "cookies-next"; -import { UserInfoProvider, useUserContext } from "./contexts/context-provider"; import Error404 from "components/error-404"; import { useActiveAnchor } from "./contexts"; @@ -207,8 +206,6 @@ const InnerLayout = ({ timestamp, children, }: PageOpts & { children: ReactNode }): ReactElement => { - const context = useUserContext(); - const config = useConfig(); const { locale = DEFAULT_LOCALE, defaultLocale } = useRouter(); const fsPath = useFSRoute(); @@ -268,11 +265,6 @@ const InnerLayout = ({ ? localeConfig.direction === "rtl" : config.direction === "rtl"; const direction = isRTL ? "rtl" : "ltr"; - - const check = activePath.at(-1)?.permission?.length - ? activePath.at(-1)?.permission.includes("fetch.ai") && - context.isFetchAccount - : true; return ( // This makes sure that selectors like `[dir=ltr] .nextra-container` work // before hydration as Tailwind expects the `dir` attribute to exist on the @@ -318,36 +310,34 @@ const InnerLayout = ({ includePlaceholder={themeContext.layout === "default"} /> - {check && ( - - ) : null - } - timestamp={timestamp} - navigation={ - activeType !== "page" && themeContext.pagination ? ( - - ) : null - } - tags={activePath.at(-1)?.tags ?? undefined} - directoriesWithTags={directoriesWithTags} + + ) : null + } + timestamp={timestamp} + navigation={ + activeType !== "page" && themeContext.pagination ? ( + + ) : null + } + tags={activePath.at(-1)?.tags ?? undefined} + directoriesWithTags={directoriesWithTags} + > + - - {children} - - - )} + {children} + +
{themeContext.footer && @@ -362,11 +352,9 @@ export default function Layout({ }: NextraThemeLayoutProps): ReactElement { return ( - - - {children} - - + + {children} + ); }