(
- gql`
- query Layout($domain: String!, $language: String!) {
- header: mainMenu(scope: { domain: $domain, language: $language }) {
- ...Header
- }
- }
-
- ${headerFragment}
- `,
- { ...scope },
- );
-}
-
-export { getLayout, Layout };
-export type PropsWithLayout = P & { layout: GQLLayoutQuery };
diff --git a/site/src/layout/header/Header.fragment.ts b/site/src/layout/header/Header.fragment.ts
new file mode 100644
index 000000000..86ed8f1b3
--- /dev/null
+++ b/site/src/layout/header/Header.fragment.ts
@@ -0,0 +1,18 @@
+import { gql } from "@comet/cms-site";
+
+import { pageLinkFragment } from "./PageLink.fragment";
+
+export const headerFragment = gql`
+ fragment Header on PageTreeNode {
+ id
+ name
+ ...PageLink
+ childNodes {
+ id
+ name
+ ...PageLink
+ }
+ }
+
+ ${pageLinkFragment}
+`;
diff --git a/site/src/layout/header/Header.tsx b/site/src/layout/header/Header.tsx
index 392694ecc..c4bf9a8e2 100644
--- a/site/src/layout/header/Header.tsx
+++ b/site/src/layout/header/Header.tsx
@@ -1,10 +1,10 @@
+"use client";
import { SvgUse } from "@src/common/helpers/SvgUse";
-import { gql } from "graphql-request";
import * as React from "react";
import styled from "styled-components";
-import { GQLHeaderFragment } from "./Header.generated";
-import { PageLink, pageLinkFragment } from "./PageLink";
+import { GQLHeaderFragment } from "./Header.fragment.generated";
+import { PageLink } from "./PageLink";
interface Props {
header: GQLHeaderFragment[];
@@ -36,21 +36,6 @@ function Header({ header }: Props): JSX.Element {
);
}
-const headerFragment = gql`
- fragment Header on PageTreeNode {
- id
- name
- ...PageLink
- childNodes {
- id
- name
- ...PageLink
- }
- }
-
- ${pageLinkFragment}
-`;
-
const Root = styled.header`
padding: 10px 20px;
`;
@@ -92,4 +77,4 @@ const Link = styled.a<{ $active: boolean }>`
}
`;
-export { Header, headerFragment };
+export { Header };
diff --git a/site/src/layout/header/PageLink.fragment.ts b/site/src/layout/header/PageLink.fragment.ts
new file mode 100644
index 000000000..8e3be6d94
--- /dev/null
+++ b/site/src/layout/header/PageLink.fragment.ts
@@ -0,0 +1,17 @@
+import { gql } from "@comet/cms-site";
+
+export const pageLinkFragment = gql`
+ fragment PageLink on PageTreeNode {
+ path
+ documentType
+ scope {
+ language
+ }
+ document {
+ __typename
+ ... on Link {
+ content
+ }
+ }
+ }
+`;
diff --git a/site/src/layout/header/PageLink.tsx b/site/src/layout/header/PageLink.tsx
index 4de4ee86f..3fcb28e0c 100644
--- a/site/src/layout/header/PageLink.tsx
+++ b/site/src/layout/header/PageLink.tsx
@@ -1,24 +1,11 @@
+"use client";
import { LinkBlock } from "@src/common/blocks/LinkBlock";
import { HiddenIfInvalidLink } from "@src/common/helpers/HiddenIfInvalidLink";
-import { gql } from "graphql-request";
import Link from "next/link";
-import { useRouter } from "next/router";
+import { usePathname } from "next/navigation";
import * as React from "react";
-import { GQLPageLinkFragment } from "./PageLink.generated";
-
-const pageLinkFragment = gql`
- fragment PageLink on PageTreeNode {
- path
- documentType
- document {
- __typename
- ... on Link {
- content
- }
- }
- }
-`;
+import { GQLPageLinkFragment } from "./PageLink.fragment.generated";
interface Props {
page: GQLPageLinkFragment;
@@ -26,8 +13,8 @@ interface Props {
}
function PageLink({ page, children }: Props): JSX.Element | null {
- const router = useRouter();
- const active = router.asPath === page.path;
+ const pathname = usePathname();
+ const active = pathname === page.path;
if (page.documentType === "Link") {
if (page.document === null || page.document.__typename !== "Link") {
@@ -41,7 +28,7 @@ function PageLink({ page, children }: Props): JSX.Element | null {
);
} else if (page.documentType === "Page") {
return (
-
+
{typeof children === "function" ? children(active) : children}
);
@@ -54,4 +41,4 @@ function PageLink({ page, children }: Props): JSX.Element | null {
}
}
-export { PageLink, pageLinkFragment };
+export { PageLink };
diff --git a/site/src/middleware.ts b/site/src/middleware.ts
new file mode 100644
index 000000000..7844daa52
--- /dev/null
+++ b/site/src/middleware.ts
@@ -0,0 +1,55 @@
+import { Rewrite } from "next/dist/lib/load-custom-routes";
+import type { NextRequest } from "next/server";
+import { NextResponse } from "next/server";
+
+import { createRedirects } from "./redirects/redirects";
+
+export async function middleware(request: NextRequest) {
+ const { pathname } = new URL(request.url);
+
+ const redirects = await createRedirects();
+
+ const redirect = redirects.get(pathname);
+ if (redirect) {
+ const destination: string = redirect.destination;
+ return NextResponse.redirect(new URL(destination, request.url), redirect.permanent ? 308 : 307);
+ }
+
+ const rewrites = await createRewrites();
+ const rewrite = rewrites.get(pathname);
+ if (rewrite) {
+ return NextResponse.rewrite(new URL(rewrite.destination, request.url));
+ }
+
+ return NextResponse.next();
+}
+
+type RewritesMap = Map;
+
+async function createRewrites(): Promise {
+ const rewritesMap = new Map();
+ return rewritesMap;
+}
+
+export const config = {
+ matcher: [
+ /*
+ * Match all request paths except for the ones starting with:
+ * - api (API routes)
+ * - _next/static (static files)
+ * - _next/image (image optimization files)
+ * - favicon.ico, favicon.svg, favicon.png
+ * - manifest.json
+ */
+ "/((?!api|_next/static|_next/image|favicon.ico|favicon.svg|favicon.png|manifest.json).*)",
+ ],
+ // TODO find a better solution for this (https://nextjs.org/docs/messages/edge-dynamic-code-evaluation)
+ unstable_allowDynamic: [
+ "/node_modules/graphql/**",
+ /*
+ * cache-manager uses lodash.clonedeep which uses dynamic code evaluation.
+ * See https://github.com/lodash/lodash/issues/5525.
+ */
+ "/node_modules/lodash.clonedeep/**",
+ ],
+};
diff --git a/site/src/pages/404.page.tsx b/site/src/pages/404.page.tsx
deleted file mode 100644
index ea3178e39..000000000
--- a/site/src/pages/404.page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as React from "react";
-
-interface NotFound404Props {
- children: React.ReactNode;
-}
-export default function NotFound404({ children }: NotFound404Props): JSX.Element {
- return (
- <>
- 404 - Page Not Found
- {children}
- >
- );
-}
diff --git a/site/src/pages/[[...path]].page.tsx b/site/src/pages/[[...path]].page.tsx
deleted file mode 100644
index c1cab4ae1..000000000
--- a/site/src/pages/[[...path]].page.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import { inferContentScopeFromContext } from "@src/common/contentScope/inferContentScopeFromContext";
-import { domain as configuredDomain } from "@src/config";
-import { Page as PageTypePage, pageQuery as PageTypePageQuery } from "@src/documents/pages/Page";
-import { GQLPage } from "@src/graphql.generated";
-import { getLayout } from "@src/layout/Layout";
-import NotFound404 from "@src/pages/404.page";
-import { createGraphQLClient } from "@src/util/createGraphQLClient";
-import { gql } from "graphql-request";
-import {
- GetServerSidePropsContext,
- GetServerSidePropsResult,
- GetStaticPaths,
- GetStaticProps,
- GetStaticPropsContext,
- GetStaticPropsResult,
- InferGetStaticPropsType,
-} from "next";
-import * as React from "react";
-
-import { GQLPagesQuery, GQLPagesQueryVariables, GQLPageTypeQuery, GQLPageTypeQueryVariables } from "./[[...path]].page.generated";
-
-interface PageProps {
- documentType: string;
- id: string;
-}
-export type PageUniversalProps = PageProps & GQLPage;
-
-export default function Page(props: InferGetStaticPropsType): JSX.Element {
- if (!pageTypes[props.documentType]) {
- return (
-
-
- unknown documentType: {props.documentType}
-
-
- );
- }
- const { component: Component } = pageTypes[props.documentType];
- return ;
-}
-
-const pageTypeQuery = gql`
- query PageType($path: String!, $scope: PageTreeNodeScopeInput!) {
- pageTreeNodeByPath(path: $path, scope: $scope) {
- id
- documentType
- }
- }
-`;
-
-const pageTypes = {
- Page: {
- query: PageTypePageQuery,
- component: PageTypePage,
- },
-};
-
-export const getStaticProps: GetStaticProps = async (context) => {
- const getUniversalProps = createGetUniversalProps();
- return getUniversalProps(context);
-};
-
-interface CreateGetUniversalPropsOptions {
- includeInvisibleBlocks?: boolean;
- includeInvisiblePages?: boolean;
- previewDamUrls?: boolean;
-}
-
-// a function to create a universal function which can be used as getStaticProps or getServerSideProps (preview)
-export function createGetUniversalProps({
- includeInvisibleBlocks = false,
- includeInvisiblePages = false,
- previewDamUrls = false,
-}: CreateGetUniversalPropsOptions = {}) {
- return async function getUniversalProps(
- context: Context,
- ): Promise : GetServerSidePropsResult> {
- const { params } = context;
-
- const client = createGraphQLClient({ includeInvisibleBlocks, includeInvisiblePages, previewDamUrls });
- const scope = inferContentScopeFromContext(context);
-
- const path = params?.path ?? "";
-
- //fetch pageType
- const data = await client.request(pageTypeQuery, {
- path: `/${Array.isArray(path) ? path.join("/") : path}`,
- scope,
- });
- if (!data.pageTreeNodeByPath?.documentType) {
- // eslint-disable-next-line no-console
- console.log("got no data from api", data, path);
- return { notFound: true };
- }
- const pageId = data.pageTreeNodeByPath.id;
-
- //pageType dependent query
- const { query: queryForPageType } = pageTypes[data.pageTreeNodeByPath.documentType];
-
- const [layout, pageTypeData] = await Promise.all([getLayout(client, scope), client.request(queryForPageType, { pageId })]);
-
- return {
- props: {
- layout,
- ...pageTypeData,
- documentType: data.pageTreeNodeByPath.documentType,
- id: pageId,
- scope,
- },
- };
- };
-}
-
-const pagesQuery = gql`
- query Pages($scope: PageTreeNodeScopeInput!) {
- pageTreeNodeList(scope: $scope) {
- id
- path
- documentType
- }
- }
-`;
-
-export const getStaticPaths: GetStaticPaths = async ({ locales = [] }) => {
- const paths: Array<{ params: { path: string[] }; locale: string }> = [];
- if (process.env.NEXT_PUBLIC_SITE_IS_PREVIEW !== "true") {
- for (const locale of locales) {
- const data = await createGraphQLClient().request(pagesQuery, {
- scope: { domain: configuredDomain, language: locale },
- });
-
- paths.push(
- ...data.pageTreeNodeList
- .filter((page) => page.documentType === "Page")
- .map((page) => {
- const path = page.path.split("/");
- path.shift(); // Remove "" caused by leading slash
- return { params: { path }, locale };
- }),
- );
- }
- }
- return {
- paths,
- fallback: false,
- };
-};
diff --git a/site/src/pages/_app.page.tsx b/site/src/pages/_app.page.tsx
deleted file mode 100644
index 64cc78c00..000000000
--- a/site/src/pages/_app.page.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import { SitePreviewProvider } from "@comet/cms-site";
-import { ContentScope, ContentScopeProvider } from "@src/common/contentScope/ContentScope";
-import { defaultLanguage, domain } from "@src/config";
-import { getMessages } from "@src/lang";
-import { theme } from "@src/theme";
-import { ResponsiveSpacingStyle } from "@src/util/ResponsiveSpacingStyle";
-import App, { AppProps, NextWebVitalsMetric } from "next/app";
-import Head from "next/head";
-import Script from "next/script";
-import * as React from "react";
-import { IntlProvider } from "react-intl";
-import { createGlobalStyle, ThemeProvider } from "styled-components";
-
-const GlobalStyle = createGlobalStyle`
- /* Fix a problem with Flexbox to avoid overflows: https://defensivecss.dev/tip/flexbox-min-content-size/ */
- * {
- min-width: 0;
- }
-
- /* Prevent font size adjustments after orientation changes in mobile devices*/
- html {
- -webkit-text-size-adjust: 100%;
- }
-
- body {
- margin: 0;
- /* Improve text rendering with font-smoothing */
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
- p {
- margin: 0;
- }
-
- /* Prevent sub and sup elements from affecting the line height in all browsers */
- sub,
- sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
- }
-
- sub {
- bottom: -0.25em;
- }
-
- sup {
- top: -0.5em;
- }
-
- button,
- input,
- select,
- textarea {
- /* Use the application's default font for form elements */
- font: inherit;
-
- /* Remove default border-radius that is added by iOS Safari */
- border-radius: 0;
- }
-
- /* Improve media defaults */
- img,
- picture,
- video,
- canvas,
- svg {
- display: block;
- max-width: 100%;
- }
-
- /* Create a root stacking context: https://www.joshwcomeau.com/css/custom-css-reset/#eight-root-stacking-context-9 */
- #root,
- #__next {
- isolation: isolate;
- }
-`;
-
-declare global {
- interface Window {
- dataLayer: Record[];
- }
-}
-
-export function reportWebVitals({ id, name, label, value }: NextWebVitalsMetric): void {
- // https://nextjs.org/docs/advanced-features/measuring-performance#sending-results-to-analytics
- if (process.env.NEXT_PUBLIC_GTM_ID) {
- const event = {
- event: "web-vitals",
- event_category: label === "web-vital" ? "Web Vitals" : "Next.js custom metric",
- event_action: name,
- event_value: Math.round(name === "CLS" ? value * 1000 : value), // values must be integers
- event_label: id, // id unique to current page load
- non_interaction: true, // avoids affecting bounce rate.
- };
- window.dataLayer.push(event);
- }
-}
-
-interface CustomAppProps extends AppProps {
- scope: ContentScope;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- messages: any;
-}
-
-export default function CustomApp({ Component, pageProps, scope, messages }: CustomAppProps): JSX.Element {
- return (
- // see https://github.com/vercel/next.js/tree/master/examples/with-react-intl
- // for a complete strategy to couple next with react-intl
-
-
-
-
- {process.env.NEXT_PUBLIC_GTM_ID && (
- <>
-
-
-