diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index 4a30e735dc6..d49bd1dc4d6 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -76,6 +76,8 @@ export interface MainLayoutProps canGoBack?: string; hideBackButton?: boolean; hideFeedbackWidget?: boolean; + /** Uses the server-selected layout shell before client boot resolves. */ + layoutVariant?: 'v1' | 'v2'; /** * Layout v2 only. Rendered above the floating feed card, alongside the * built-in reading-reminder TopHero. Pages can pass dynamic banners @@ -102,6 +104,7 @@ function MainLayoutComponent({ canGoBack, hideFeedbackWidget = false, topBanner, + layoutVariant, }: MainLayoutProps): ReactElement | null { const router = useRouter(); const { logEvent } = useLogContext(); @@ -130,7 +133,14 @@ function MainLayoutComponent({ const isLaptopXL = useViewSize(ViewSize.LaptopXL); const { screenCenteredOnMobileLayout } = useFeedLayout(); const { isNotificationsReady, unreadCount } = useNotificationContext(); - const { isV2, isLoading: isLayoutVariantLoading } = useLayoutVariant(); + const { isV2: evaluatedIsV2, isLoading: evaluatedLayoutVariantLoading } = + useLayoutVariant(); + const isLayoutVariantForced = layoutVariant !== undefined; + const isForcedV2 = layoutVariant === 'v2'; + const isV2 = isForcedV2 || (!isLayoutVariantForced && evaluatedIsV2); + const isLayoutVariantLoading = isLayoutVariantForced + ? false + : evaluatedLayoutVariantLoading; useRecordRecentPages(isV2); useNotificationParams(); useFeedbackShortcut(); @@ -206,7 +216,8 @@ function MainLayoutComponent({ // `isLaptop` alone made the server emit one and the client skip it, which // shifted `
` and broke hydration. const isLayoutChromeResolved = - !isHoldingChrome && (!isLaptop || !isLayoutVariantLoading); + isLayoutVariantForced || + (!isHoldingChrome && (!isLaptop || !isLayoutVariantLoading)); // Extension new tab mounts its own `ExtensionTopBanners` strip, so // the webapp strip is suppressed there to avoid duplicate cards. @@ -221,7 +232,11 @@ function MainLayoutComponent({ // floating-card treatment, and the global feedback widget is suppressed // because the rail provides its own. const sidebarOwnsHeader = - isV2 && (isLoggedIn || isExtension) && showSidebar && sidebarRendered; + isV2 && + (isForcedV2 || isLoggedIn || isExtension) && + showSidebar && + (isForcedV2 || sidebarRendered); + const shouldRenderHeader = !sidebarOwnsHeader && isLayoutChromeResolved; useEffect(() => { if (!isNotificationsReady || unreadCount === 0 || hasLoggedImpression) { @@ -334,13 +349,10 @@ function MainLayoutComponent({ /> )} - {/* Temporary while layout v2 is experimental: production users are on - v1, so render its header in the initial HTML instead of waiting for - feature resolution and delaying the post page's LCP. */} - {!sidebarOwnsHeader && ( + {shouldRenderHeader && ( @@ -350,7 +362,7 @@ function MainLayoutComponent({ 'flex flex-col', animateContentPadding && 'transition-[padding] duration-300 ease-in-out', - !sidebarOwnsHeader && 'laptop:pt-16', + shouldRenderHeader && 'laptop:pt-16', showSidebar && (isV2 ? v2CollapsedPadding : 'tablet:pl-16 laptop:pl-11'), className, @@ -358,7 +370,7 @@ function MainLayoutComponent({ showSidebar && (sidebarExpanded || forceSidebarExpanded) && (isV2 ? v2ExpandedPadding : !isScreenCentered && 'laptop:!pl-60'), - isBannerAvailable && !sidebarOwnsHeader && 'laptop:pt-24', + isBannerAvailable && shouldRenderHeader && 'laptop:pt-24', )} > {isAuthReady && isLayoutChromeResolved && showSidebar && ( diff --git a/packages/shared/src/lib/feature.ts b/packages/shared/src/lib/feature.ts new file mode 100644 index 00000000000..67b14eb7c03 --- /dev/null +++ b/packages/shared/src/lib/feature.ts @@ -0,0 +1,12 @@ +import type { JSONValue } from '@growthbook/growthbook'; + +export class Feature { + readonly id: string; + + readonly defaultValue: T; + + constructor(id: string, defaultValue: T) { + this.id = id; + this.defaultValue = defaultValue; + } +} diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index 8c57460e9b1..03c7e4a0ec7 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -1,4 +1,3 @@ -import type { JSONValue } from '@growthbook/growthbook'; import type { FeedAdTemplate } from './feed'; import type { FeedSettingsKeys } from '../contexts/FeedContext'; import type { PlusItemStatus } from '../components/plus/PlusListItem'; @@ -6,17 +5,10 @@ import { isDevelopment } from './constants'; import { BriefingType } from '../graphql/posts'; import type { HeroCardsConfig } from '../types'; import { PostType } from '../types'; +import { Feature } from './feature'; -export class Feature { - readonly id: string; - - readonly defaultValue: T; - - constructor(id: string, defaultValue: T) { - this.id = id; - this.defaultValue = defaultValue; - } -} +export { Feature } from './feature'; +export { featureLayoutV2 } from './serverFeatures'; const feature = { showError: new Feature('show_error', false), @@ -258,8 +250,6 @@ export const featureOnboardingChrome = new Feature( OnboardingChromeVariant.Control, ); -export const featureLayoutV2 = new Feature('layout_v2', false); - export const featureEngagementBarV2 = new Feature('engagement_bar_v2', false); export const featureHeroCards = new Feature('hero_cards', { diff --git a/packages/shared/src/lib/serverFeatureValue.ts b/packages/shared/src/lib/serverFeatureValue.ts new file mode 100644 index 00000000000..707ede46db8 --- /dev/null +++ b/packages/shared/src/lib/serverFeatureValue.ts @@ -0,0 +1,39 @@ +import { GrowthBook } from '@growthbook/growthbook'; +import type { JSONValue } from '@growthbook/growthbook'; +import type { Feature } from './feature'; + +const DEFAULT_API_HOST = 'https://cdn.growthbook.io'; +const DEFAULT_TIMEOUT = 2000; + +interface GetServerFeatureValueOptions { + attributes: Record; + clientKey?: string; + feature: Feature; +} + +export const getServerFeatureValue = async ({ + attributes, + clientKey, + feature, +}: GetServerFeatureValueOptions): Promise => { + const { defaultValue, id } = feature; + + if (!clientKey) { + return defaultValue; + } + + const growthbook = new GrowthBook({ + apiHost: process.env.GROWTHBOOK_API_HOST ?? DEFAULT_API_HOST, + attributes, + clientKey, + }); + + try { + await growthbook.loadFeatures({ timeout: DEFAULT_TIMEOUT }); + return growthbook.getFeatureValue(id, defaultValue) as T; + } catch { + return defaultValue; + } finally { + growthbook.destroy(); + } +}; diff --git a/packages/shared/src/lib/serverFeatures.ts b/packages/shared/src/lib/serverFeatures.ts new file mode 100644 index 00000000000..cc651e065c4 --- /dev/null +++ b/packages/shared/src/lib/serverFeatures.ts @@ -0,0 +1,5 @@ +import { Feature } from './feature'; + +// Features evaluated before the application bundle loads belong here so +// server entry points do not pull in featureManagement's client dependencies. +export const featureLayoutV2 = new Feature('layout_v2', false); diff --git a/packages/webapp/__tests__/MainLayoutPaintHold.tsx b/packages/webapp/__tests__/MainLayoutPaintHold.tsx index 445fb0dcc43..5a41fa517b6 100644 --- a/packages/webapp/__tests__/MainLayoutPaintHold.tsx +++ b/packages/webapp/__tests__/MainLayoutPaintHold.tsx @@ -46,13 +46,17 @@ describe('MainLayout before boot resolves', () => { .mockReturnValue({ isV2: false, isLoading: true }); }); - const renderLayout = (): RenderResult => + const renderLayout = ({ + layoutVariant: forcedVariant, + }: { + layoutVariant?: 'v1' | 'v2'; + } = {}): RenderResult => render( - +

prerendered page content

, @@ -73,11 +77,32 @@ describe('MainLayout before boot resolves', () => { expect(content.closest('div.antialiased')).not.toHaveClass('invisible'); }); - it('renders the header before the layout experiment resolves', () => { + it('does not reserve header space before the layout resolves', () => { mockRouter('/posts/[id]'); renderLayout(); + const content = screen.getByText('prerendered page content'); + expect(screen.queryByRole('banner')).not.toBeInTheDocument(); + expect(content.closest('main')).not.toHaveClass('laptop:pt-16'); + }); + + it('renders the server-selected v1 header and its spacing', () => { + mockRouter('/posts/[id]'); + renderLayout({ layoutVariant: 'v1' }); + + const content = screen.getByText('prerendered page content'); expect(screen.getByRole('banner')).toBeInTheDocument(); + expect(content.closest('main')).toHaveClass('laptop:pt-16'); + }); + + it('renders the server-selected layout v2 shell before boot resolves', () => { + mockRouter('/layout-v2/posts/[id]'); + renderLayout({ layoutVariant: 'v2' }); + + const content = screen.getByText('prerendered page content'); + expect(screen.queryByRole('banner')).not.toBeInTheDocument(); + expect(content.closest('main')).not.toHaveClass('laptop:pt-16'); + expect(content.closest('div.laptop\\:rounded-24')).toBeInTheDocument(); }); it('still renders nothing for feed-shaped pages', () => { diff --git a/packages/webapp/__tests__/layoutVariantMiddleware.ts b/packages/webapp/__tests__/layoutVariantMiddleware.ts new file mode 100644 index 00000000000..8af2eefe75a --- /dev/null +++ b/packages/webapp/__tests__/layoutVariantMiddleware.ts @@ -0,0 +1,99 @@ +/** @jest-environment node */ + +import { NextRequest } from 'next/server'; +import { getServerFeatureValue } from '@dailydotdev/shared/src/lib/serverFeatureValue'; +import { featureLayoutV2 } from '@dailydotdev/shared/src/lib/serverFeatures'; +import { + isDesktopRequest, + isLayoutV2EligiblePath, + resolveLayoutV2, +} from '../lib/layoutVariantMiddleware'; + +jest.mock('@dailydotdev/shared/src/lib/serverFeatureValue', () => ({ + getServerFeatureValue: jest.fn(), +})); + +const createRequest = ({ + cookie = 'da2=tracking-id; __Secure-dast=session', + mobile, + userAgent, +}: { + cookie?: string; + mobile?: string; + userAgent?: string; +} = {}): NextRequest => + new NextRequest('https://app.daily.dev/posts/test-post', { + headers: { + cookie, + ...(mobile && { 'sec-ch-ua-mobile': mobile }), + ...(userAgent && { 'user-agent': userAgent }), + }, + }); + +describe('layout variant middleware resolver', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(getServerFeatureValue).mockResolvedValue(false); + }); + + it.each([ + '/kramer', + '/kramer/work', + '/posts/best-of', + '/posts/best-of/2025/8', + '/tags/javascript/best-of/2025', + '/sources/thenewstack/best-of', + '/gear', + '/jobs/test-job', + '/standups/test-standup', + '/squads/test-squad', + '/squads/discover/featured-category', + '/quiz/ai-fluency', + ])('recognizes the v2 route %s', (pathname) => { + expect(isLayoutV2EligiblePath(pathname)).toBe(true); + }); + + it.each([ + '/popular', + '/favicon.ico', + '/settings', + '/kramer/unknown-section', + '/posts/latest', + '/posts/test.md', + '/squads/new', + '/tags/javascript', + ])('leaves the non-v2 route %s unchanged', (pathname) => { + expect(isLayoutV2EligiblePath(pathname)).toBe(false); + }); + + it('uses the tracking id for the same allocation attributes as the client', async () => { + jest.mocked(getServerFeatureValue).mockResolvedValue(true); + + await expect(resolveLayoutV2(createRequest())).resolves.toBe(true); + expect(getServerFeatureValue).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.objectContaining({ + deviceId: 'tracking-id', + loggedIn: true, + userId: 'tracking-id', + }), + feature: featureLayoutV2, + }), + ); + }); + + it('fails closed without a tracking id', async () => { + await expect(resolveLayoutV2(createRequest({ cookie: '' }))).resolves.toBe( + false, + ); + expect(getServerFeatureValue).not.toHaveBeenCalled(); + }); + + it('does not allocate the desktop-only layout to mobile requests', async () => { + const request = createRequest({ mobile: '?1' }); + + expect(isDesktopRequest(request)).toBe(false); + await expect(resolveLayoutV2(request)).resolves.toBe(false); + expect(getServerFeatureValue).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/webapp/__tests__/middleware.ts b/packages/webapp/__tests__/middleware.ts new file mode 100644 index 00000000000..d253834ed29 --- /dev/null +++ b/packages/webapp/__tests__/middleware.ts @@ -0,0 +1,47 @@ +/** @jest-environment node */ + +import { NextRequest } from 'next/server'; +import { middleware } from '../middleware'; +import { resolveLayoutV2 } from '../lib/layoutVariantMiddleware'; + +jest.mock('../lib/layoutVariantMiddleware', () => ({ + isLayoutV2EligiblePath: jest.fn(() => true), + resolveLayoutV2: jest.fn(), +})); + +const createRequest = (accept = 'text/html'): NextRequest => + new NextRequest('https://app.daily.dev/posts/test-post?ref=test', { + headers: { accept }, + }); + +describe('post middleware', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(resolveLayoutV2).mockResolvedValue(false); + }); + + it('rewrites enabled HTML requests to the layout v2 post page', async () => { + jest.mocked(resolveLayoutV2).mockResolvedValue(true); + + const response = await middleware(createRequest()); + + expect(response.headers.get('x-middleware-rewrite')).toBe( + 'https://app.daily.dev/layout-v2/posts/test-post?ref=test', + ); + }); + + it('keeps disabled HTML requests on the original post page', async () => { + const response = await middleware(createRequest()); + + expect(response.headers.get('x-middleware-next')).toBe('1'); + }); + + it('keeps markdown negotiation ahead of layout resolution', async () => { + const response = await middleware(createRequest('text/markdown')); + + expect(response.headers.get('x-middleware-rewrite')).toBe( + 'https://app.daily.dev/api/md/posts/test-post?ref=test', + ); + expect(resolveLayoutV2).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/webapp/components/layouts/ProfileLayout/index.tsx b/packages/webapp/components/layouts/ProfileLayout/index.tsx index 6fa31514ee2..bd0da7f1f8a 100644 --- a/packages/webapp/components/layouts/ProfileLayout/index.tsx +++ b/packages/webapp/components/layouts/ProfileLayout/index.tsx @@ -27,6 +27,7 @@ import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext'; import { LogEvent, TargetType } from '@dailydotdev/shared/src/lib/log'; import { usePostReferrerContext } from '@dailydotdev/shared/src/contexts/PostReferrerContext'; import { PageHeader } from '@dailydotdev/shared/src/components/layout/PageHeader'; +import type { MainLayoutProps } from '@dailydotdev/shared/src/components/MainLayout'; import { useLayoutVariant } from '@dailydotdev/shared/src/hooks/layout/useLayoutVariant'; import { getLayout as getFooterNavBarLayout } from '../FooterNavBarLayout'; import { getLayout as getMainLayout } from '../MainLayout'; @@ -182,11 +183,14 @@ export default function ProfileLayout({ export const getLayout = ( page: ReactNode, props: ProfileLayoutProps, + layoutProps?: MainLayoutProps, ): ReactNode => getFooterNavBarLayout( getMainLayout({page}, undefined, { screenCentered: false, customBanner: , + layoutVariant: 'v1', + ...layoutProps, }), ); diff --git a/packages/webapp/lib/layoutVariantMiddleware.ts b/packages/webapp/lib/layoutVariantMiddleware.ts new file mode 100644 index 00000000000..4ab59c998cf --- /dev/null +++ b/packages/webapp/lib/layoutVariantMiddleware.ts @@ -0,0 +1,159 @@ +import type { NextRequest } from 'next/server'; +import { getServerFeatureValue } from '@dailydotdev/shared/src/lib/serverFeatureValue'; +import { featureLayoutV2 } from '@dailydotdev/shared/src/lib/serverFeatures'; +import { RESERVED_POST_SLUGS } from './markdownRoutes'; + +const TRACKING_COOKIE = 'da2'; +const AUTH_SESSION_COOKIES = ['__Secure-dast', 'dast']; +const MOBILE_USER_AGENT = + /Android|iPhone|iPad|iPod|IEMobile|Opera Mini|Mobile/i; +const PROFILE_SECTIONS = new Set([ + 'achievements', + 'certification', + 'education', + 'opensource', + 'posts', + 'project', + 'replies', + 'upvoted', + 'volunteering', + 'work', +]); +const RESERVED_PROFILE_SLUGS = new Set([ + '404', + 'activate', + 'agent', + 'agents', + 'analytics', + 'api', + 'backoffice', + 'bookmarks', + 'briefing', + 'callback', + 'cores', + 'daily', + 'daily-quests', + 'dev', + 'discussed', + 'embed', + 'error', + 'explore', + 'feed-by-ids', + 'feeds', + 'following', + 'game-center', + 'gear', + 'giveback', + 'hackathon', + 'helloworld', + 'highlights', + 'history', + 'image-generator', + 'isr', + 'jobs', + 'join', + 'layout-v2', + 'my-feed', + 'notifications', + 'onboarding', + 'pay', + 'plus', + 'popular', + 'popup', + 'posts', + 'quiz', + 'recruiter', + 'recruiter-spam-to-cores', + 'reset-password', + 'scheduled', + 'search', + 'settings', + 'sources', + 'squads', + 'standups', + 'tags', + 'team', + 'upvoted', + 'users', + 'verification', + 'wallet', + 'watercooler', + 'welcome', + 'world', +]); +const ARCHIVE_PATH = + /^\/(?:posts|tags\/[^/]+|sources\/[^/]+)\/best-of(?:\/\d{4}(?:\/\d{1,2})?)?$/; +const PUBLIC_STATIC_PATHS = new Set([ + '/gear', + '/giveback', + '/hackathon', + '/jobs', + '/jobs/how-it-works', + '/quiz/ai-fluency', +]); + +export const isLayoutV2EligiblePath = (pathname: string): boolean => { + if (PUBLIC_STATIC_PATHS.has(pathname) || ARCHIVE_PATH.test(pathname)) { + return true; + } + + const postId = pathname.match(/^\/posts\/([^/]+)$/)?.[1]; + if ( + (postId && + !postId.endsWith('.md') && + !RESERVED_POST_SLUGS.includes(postId)) || + /^\/jobs\/[^/]+$/.test(pathname) || + /^\/standups\/[^/]+$/.test(pathname) || + /^\/squads\/discover\/[^/]+$/.test(pathname) + ) { + return true; + } + + const squad = pathname.match(/^\/squads\/([^/]+)$/)?.[1]; + if (squad && !['create', 'discover', 'moderate', 'new'].includes(squad)) { + return true; + } + + const [, userId, section] = pathname.split('/'); + return ( + !!userId && + !userId.includes('.') && + !RESERVED_PROFILE_SLUGS.has(userId) && + (!section || PROFILE_SECTIONS.has(section)) && + pathname.split('/').length <= 3 + ); +}; + +export const isDesktopRequest = (req: NextRequest): boolean => { + const clientHint = req.headers.get('sec-ch-ua-mobile'); + + if (clientHint === '?1') { + return false; + } + + return !MOBILE_USER_AGENT.test(req.headers.get('user-agent') ?? ''); +}; + +export const resolveLayoutV2 = async (req: NextRequest): Promise => { + const identifier = req.cookies.get(TRACKING_COOKIE)?.value; + + // The client only evaluates layout v2 on laptop+. Missing hints can produce + // false positives, so recognizable mobile requests stay on the v1 route. + if (!identifier || !isDesktopRequest(req)) { + return false; + } + + return getServerFeatureValue({ + attributes: { + deviceId: identifier, + loggedIn: AUTH_SESSION_COOKIES.some((cookie) => req.cookies.has(cookie)), + mobile: false, + platform: 'webapp', + url: req.url, + userId: identifier, + version: process.env.CURRENT_VERSION, + }, + clientKey: process.env.GROWTHBOOK_CLIENT_KEY, + feature: featureLayoutV2, + }); +}; diff --git a/packages/webapp/lib/layoutVariantPage.tsx b/packages/webapp/lib/layoutVariantPage.tsx new file mode 100644 index 00000000000..b424102904e --- /dev/null +++ b/packages/webapp/lib/layoutVariantPage.tsx @@ -0,0 +1,23 @@ +import type { ComponentType, ReactElement, ReactNode } from 'react'; +import React from 'react'; +import type { MainLayoutProps } from '@dailydotdev/shared/src/components/MainLayout'; + +type LayoutPage = ComponentType & { + getLayout?: (...args: never[]) => ReactNode; + layoutProps?: Record; +}; + +export const withLayoutVariant = ( + Page: LayoutPage, + layoutVariant: NonNullable, +): LayoutPage => { + const LayoutVariantPage = (props: Props): ReactElement => ; + + LayoutVariantPage.getLayout = Page.getLayout; + LayoutVariantPage.layoutProps = { + ...Page.layoutProps, + layoutVariant, + }; + + return LayoutVariantPage; +}; diff --git a/packages/webapp/middleware.ts b/packages/webapp/middleware.ts index 6dbfa897ced..392501f382d 100644 --- a/packages/webapp/middleware.ts +++ b/packages/webapp/middleware.ts @@ -1,31 +1,62 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { acceptsMarkdown } from './lib/contentNegotiation'; +import { + isLayoutV2EligiblePath, + resolveLayoutV2, +} from './lib/layoutVariantMiddleware'; import { POST_MARKDOWN_PATH, RESERVED_POST_SLUGS } from './lib/markdownRoutes'; const POSTS_PREFIX = '/posts/'; +const LAYOUT_V2_PREFIX = '/layout-v2'; export const config = { - matcher: '/posts/:id', + matcher: [ + '/posts/:path*', + '/tags/:path*', + '/sources/:path*', + '/gear', + '/jobs/:path*', + '/standups/:path*', + '/squads/:path*', + '/giveback', + '/hackathon', + '/quiz/:path*', + '/:userId', + '/:userId/:section', + ], }; -export function middleware(req: NextRequest): NextResponse { +export async function middleware(req: NextRequest): Promise { const { pathname } = req.nextUrl; - const id = pathname.slice(POSTS_PREFIX.length); + const postId = pathname.startsWith(POSTS_PREFIX) + ? pathname.slice(POSTS_PREFIX.length) + : undefined; // `.md` URLs are handled by the beforeFiles rewrite, which runs after // middleware. Rewriting here too would pass the id along with its suffix. - if ( - !id || - id.endsWith('.md') || - RESERVED_POST_SLUGS.includes(id) || - !acceptsMarkdown(req.headers.get('accept')) - ) { + const isPostPage = + !!postId && + !postId.includes('/') && + !postId.endsWith('.md') && + !RESERVED_POST_SLUGS.includes(postId); + + if (postId && !postId.includes('/') && !isPostPage && postId !== 'best-of') { return NextResponse.next(); } - const url = req.nextUrl.clone(); - url.pathname = `${POST_MARKDOWN_PATH}/${id}`; + if (isPostPage && acceptsMarkdown(req.headers.get('accept'))) { + const url = req.nextUrl.clone(); + url.pathname = `${POST_MARKDOWN_PATH}/${postId}`; + + return NextResponse.rewrite(url); + } + if (!isLayoutV2EligiblePath(pathname) || !(await resolveLayoutV2(req))) { + return NextResponse.next(); + } + + const url = req.nextUrl.clone(); + url.pathname = `${LAYOUT_V2_PREFIX}${pathname}`; return NextResponse.rewrite(url); } diff --git a/packages/webapp/next.config.ts b/packages/webapp/next.config.ts index d4a153f1c7a..156848336d7 100644 --- a/packages/webapp/next.config.ts +++ b/packages/webapp/next.config.ts @@ -263,6 +263,12 @@ const nextConfig: NextConfig = { destination: '/posts/:id', permanent: false, }, + // layout v2 pages are selected through middleware only + { + source: '/layout-v2/:path*', + destination: '/:path*', + permanent: false, + }, // so we can't access /plus/gift route directly { source: '/plus/gift', diff --git a/packages/webapp/pages/[userId]/achievements.tsx b/packages/webapp/pages/[userId]/achievements.tsx index b99e7456f8c..83c09b3dcbd 100644 --- a/packages/webapp/pages/[userId]/achievements.tsx +++ b/packages/webapp/pages/[userId]/achievements.tsx @@ -11,6 +11,7 @@ import { import { ProfileAchievements } from '@dailydotdev/shared/src/features/profile/components/achievements/ProfileAchievements'; import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext'; import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext'; +import type { MainLayoutProps } from '@dailydotdev/shared/src/components/MainLayout'; import type { ProfileLayoutProps } from '../../components/layouts/ProfileLayout'; import { getLayout as getProfileLayout, @@ -75,6 +76,11 @@ const ProfileAchievementsPage = ({ ProfileAchievementsPage.getLayout = ( page: ReactNode, props: ProfileLayoutProps, + layoutProps?: MainLayoutProps, ): ReactNode => - getProfileLayout(page, { ...props, pageHeaderTitle: 'Achievements' }); + getProfileLayout( + page, + { ...props, pageHeaderTitle: 'Achievements' }, + layoutProps, + ); export default ProfileAchievementsPage; diff --git a/packages/webapp/pages/gear/index.tsx b/packages/webapp/pages/gear/index.tsx index c49e822b131..7d5ce3c56ad 100644 --- a/packages/webapp/pages/gear/index.tsx +++ b/packages/webapp/pages/gear/index.tsx @@ -120,6 +120,7 @@ GearPage.getLayout = getGearPageLayout; GearPage.layoutProps = { screenCentered: false, seo, + layoutVariant: 'v1', }; export default GearPage; diff --git a/packages/webapp/pages/giveback/index.tsx b/packages/webapp/pages/giveback/index.tsx index 0c92518eaf9..c92ba2822f1 100644 --- a/packages/webapp/pages/giveback/index.tsx +++ b/packages/webapp/pages/giveback/index.tsx @@ -61,6 +61,10 @@ const getGivebackLayout: typeof getLayout = (...props) => getFooterNavBarLayout(getLayout(...props)); GivebackRoute.getLayout = getGivebackLayout; -GivebackRoute.layoutProps = { screenCentered: false, seo }; +GivebackRoute.layoutProps = { + screenCentered: false, + seo, + layoutVariant: 'v1', +}; export default GivebackRoute; diff --git a/packages/webapp/pages/hackathon/index.tsx b/packages/webapp/pages/hackathon/index.tsx index 5c2858039db..2a1a84e7ae4 100644 --- a/packages/webapp/pages/hackathon/index.tsx +++ b/packages/webapp/pages/hackathon/index.tsx @@ -88,6 +88,10 @@ const getHackathonLayout: typeof getLayout = (...props) => getFooterNavBarLayout(getLayout(...props)); HackathonPage.getLayout = getHackathonLayout; -HackathonPage.layoutProps = { screenCentered: false, seo }; +HackathonPage.layoutProps = { + screenCentered: false, + seo, + layoutVariant: 'v1', +}; export default HackathonPage; diff --git a/packages/webapp/pages/jobs/[id]/index.tsx b/packages/webapp/pages/jobs/[id]/index.tsx index ffdd5dfa6ba..caf5b87a10a 100644 --- a/packages/webapp/pages/jobs/[id]/index.tsx +++ b/packages/webapp/pages/jobs/[id]/index.tsx @@ -1090,6 +1090,7 @@ JobPage.getLayout = getPageLayout; JobPage.layoutProps = { screenCentered: false, seo, + layoutVariant: 'v1', }; export default JobPage; diff --git a/packages/webapp/pages/jobs/how-it-works.tsx b/packages/webapp/pages/jobs/how-it-works.tsx index 4b0edd2217c..89cf49040b0 100644 --- a/packages/webapp/pages/jobs/how-it-works.tsx +++ b/packages/webapp/pages/jobs/how-it-works.tsx @@ -77,6 +77,10 @@ const geOpportunityLayout: typeof getLayout = (...props) => getFooterNavBarLayout(getLayout(...props)); JobsHowItWorksPage.getLayout = geOpportunityLayout; -JobsHowItWorksPage.layoutProps = { screenCentered: false, seo }; +JobsHowItWorksPage.layoutProps = { + screenCentered: false, + seo, + layoutVariant: 'v1', +}; export default JobsHowItWorksPage; diff --git a/packages/webapp/pages/jobs/index.tsx b/packages/webapp/pages/jobs/index.tsx index 1e5610c89f4..bddf4d39d60 100644 --- a/packages/webapp/pages/jobs/index.tsx +++ b/packages/webapp/pages/jobs/index.tsx @@ -154,6 +154,6 @@ const geOpportunityLayout: typeof getLayout = (...props) => getFooterNavBarLayout(getLayout(...props)); JobsPage.getLayout = geOpportunityLayout; -JobsPage.layoutProps = { screenCentered: false }; +JobsPage.layoutProps = { screenCentered: false, layoutVariant: 'v1' }; export default JobsPage; diff --git a/packages/webapp/pages/layout-v2/[userId]/achievements.tsx b/packages/webapp/pages/layout-v2/[userId]/achievements.tsx new file mode 100644 index 00000000000..f28b3f46481 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/achievements.tsx @@ -0,0 +1,8 @@ +import ProfileAchievementsPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/achievements'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(ProfileAchievementsPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/certification.tsx b/packages/webapp/pages/layout-v2/[userId]/certification.tsx new file mode 100644 index 00000000000..9ab3c7baa98 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/certification.tsx @@ -0,0 +1,8 @@ +import CertificationsPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/certification'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(CertificationsPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/education.tsx b/packages/webapp/pages/layout-v2/[userId]/education.tsx new file mode 100644 index 00000000000..d93dbf09274 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/education.tsx @@ -0,0 +1,8 @@ +import EducationPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/education'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(EducationPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/index.tsx b/packages/webapp/pages/layout-v2/[userId]/index.tsx new file mode 100644 index 00000000000..0d7ce94c4b0 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/index.tsx @@ -0,0 +1,5 @@ +import ProfilePage, { getStaticPaths, getStaticProps } from '../../[userId]'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(ProfilePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/opensource.tsx b/packages/webapp/pages/layout-v2/[userId]/opensource.tsx new file mode 100644 index 00000000000..358c092e62b --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/opensource.tsx @@ -0,0 +1,8 @@ +import OpensourcePage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/opensource'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(OpensourcePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/posts.tsx b/packages/webapp/pages/layout-v2/[userId]/posts.tsx new file mode 100644 index 00000000000..7ad4869ddf1 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/posts.tsx @@ -0,0 +1,8 @@ +import ProfilePostsPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/posts'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(ProfilePostsPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/project.tsx b/packages/webapp/pages/layout-v2/[userId]/project.tsx new file mode 100644 index 00000000000..c357c9c638f --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/project.tsx @@ -0,0 +1,8 @@ +import ProjectsPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/project'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(ProjectsPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/replies.tsx b/packages/webapp/pages/layout-v2/[userId]/replies.tsx new file mode 100644 index 00000000000..2b11ee80571 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/replies.tsx @@ -0,0 +1,8 @@ +import ProfileCommentsPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/replies'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(ProfileCommentsPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/upvoted.tsx b/packages/webapp/pages/layout-v2/[userId]/upvoted.tsx new file mode 100644 index 00000000000..5b93aaacfb1 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/upvoted.tsx @@ -0,0 +1,8 @@ +import ProfileUpvotedPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/upvoted'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(ProfileUpvotedPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/volunteering.tsx b/packages/webapp/pages/layout-v2/[userId]/volunteering.tsx new file mode 100644 index 00000000000..15098149cb1 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/volunteering.tsx @@ -0,0 +1,8 @@ +import VolunteeringPage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/volunteering'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(VolunteeringPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/[userId]/work.tsx b/packages/webapp/pages/layout-v2/[userId]/work.tsx new file mode 100644 index 00000000000..9709655a687 --- /dev/null +++ b/packages/webapp/pages/layout-v2/[userId]/work.tsx @@ -0,0 +1,8 @@ +import WorkExperiencePage, { + getStaticPaths, + getStaticProps, +} from '../../[userId]/work'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(WorkExperiencePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/gear/index.tsx b/packages/webapp/pages/layout-v2/gear/index.tsx new file mode 100644 index 00000000000..c67272efcb6 --- /dev/null +++ b/packages/webapp/pages/layout-v2/gear/index.tsx @@ -0,0 +1,5 @@ +import GearPage, { getStaticProps } from '../../gear'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getStaticProps }; +export default withLayoutVariant(GearPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/giveback/index.tsx b/packages/webapp/pages/layout-v2/giveback/index.tsx new file mode 100644 index 00000000000..58b1f7de752 --- /dev/null +++ b/packages/webapp/pages/layout-v2/giveback/index.tsx @@ -0,0 +1,4 @@ +import GivebackRoute from '../../giveback'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export default withLayoutVariant(GivebackRoute, 'v2'); diff --git a/packages/webapp/pages/layout-v2/hackathon/index.tsx b/packages/webapp/pages/layout-v2/hackathon/index.tsx new file mode 100644 index 00000000000..c07c4f9b0f7 --- /dev/null +++ b/packages/webapp/pages/layout-v2/hackathon/index.tsx @@ -0,0 +1,4 @@ +import HackathonPage from '../../hackathon'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export default withLayoutVariant(HackathonPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/jobs/[id]/index.tsx b/packages/webapp/pages/layout-v2/jobs/[id]/index.tsx new file mode 100644 index 00000000000..5d67ea5cf30 --- /dev/null +++ b/packages/webapp/pages/layout-v2/jobs/[id]/index.tsx @@ -0,0 +1,5 @@ +import JobPage, { getStaticPaths, getStaticProps } from '../../../jobs/[id]'; +import { withLayoutVariant } from '../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(JobPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/jobs/how-it-works.tsx b/packages/webapp/pages/layout-v2/jobs/how-it-works.tsx new file mode 100644 index 00000000000..6e0c2474aaa --- /dev/null +++ b/packages/webapp/pages/layout-v2/jobs/how-it-works.tsx @@ -0,0 +1,4 @@ +import JobsHowItWorksPage from '../../jobs/how-it-works'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export default withLayoutVariant(JobsHowItWorksPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/jobs/index.tsx b/packages/webapp/pages/layout-v2/jobs/index.tsx new file mode 100644 index 00000000000..a466bfac4f3 --- /dev/null +++ b/packages/webapp/pages/layout-v2/jobs/index.tsx @@ -0,0 +1,4 @@ +import JobsPage from '../../jobs'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export default withLayoutVariant(JobsPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/posts/[id].tsx b/packages/webapp/pages/layout-v2/posts/[id].tsx new file mode 100644 index 00000000000..c2ee74dd91c --- /dev/null +++ b/packages/webapp/pages/layout-v2/posts/[id].tsx @@ -0,0 +1,23 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { + getStaticPaths, + getStaticProps, + PostPage, + postPageLayoutProps, +} from '../../posts/[id]/index'; +import type { Props } from '../../posts/[id]/index'; +import { getLayout } from '../../../components/layouts/MainLayout'; + +const LayoutV2PostPage = (props: Props): ReactElement => ( + +); + +LayoutV2PostPage.getLayout = getLayout; +LayoutV2PostPage.layoutProps = { + ...postPageLayoutProps, + layoutVariant: 'v2', +}; + +export { getStaticPaths, getStaticProps }; +export default LayoutV2PostPage; diff --git a/packages/webapp/pages/layout-v2/posts/best-of/[year].tsx b/packages/webapp/pages/layout-v2/posts/best-of/[year].tsx new file mode 100644 index 00000000000..3e78a563c08 --- /dev/null +++ b/packages/webapp/pages/layout-v2/posts/best-of/[year].tsx @@ -0,0 +1,8 @@ +import GlobalYearlyArchivePage, { + getStaticPaths, + getStaticProps, +} from '../../../posts/best-of/[year]'; +import { withLayoutVariant } from '../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(GlobalYearlyArchivePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/posts/best-of/[year]/[month].tsx b/packages/webapp/pages/layout-v2/posts/best-of/[year]/[month].tsx new file mode 100644 index 00000000000..b73f9fde8a3 --- /dev/null +++ b/packages/webapp/pages/layout-v2/posts/best-of/[year]/[month].tsx @@ -0,0 +1,8 @@ +import GlobalMonthlyArchivePage, { + getStaticPaths, + getStaticProps, +} from '../../../../posts/best-of/[year]/[month]'; +import { withLayoutVariant } from '../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(GlobalMonthlyArchivePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/posts/best-of/index.tsx b/packages/webapp/pages/layout-v2/posts/best-of/index.tsx new file mode 100644 index 00000000000..f0b096c0f10 --- /dev/null +++ b/packages/webapp/pages/layout-v2/posts/best-of/index.tsx @@ -0,0 +1,5 @@ +import GlobalArchiveIndexPage, { getStaticProps } from '../../../posts/best-of'; +import { withLayoutVariant } from '../../../../lib/layoutVariantPage'; + +export { getStaticProps }; +export default withLayoutVariant(GlobalArchiveIndexPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/quiz/ai-fluency.tsx b/packages/webapp/pages/layout-v2/quiz/ai-fluency.tsx new file mode 100644 index 00000000000..2482e31bb4f --- /dev/null +++ b/packages/webapp/pages/layout-v2/quiz/ai-fluency.tsx @@ -0,0 +1,4 @@ +import AiFluencyQuizPage from '../../quiz/ai-fluency'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export default withLayoutVariant(AiFluencyQuizPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/sources/[source]/best-of/[year].tsx b/packages/webapp/pages/layout-v2/sources/[source]/best-of/[year].tsx new file mode 100644 index 00000000000..1d36ed88c94 --- /dev/null +++ b/packages/webapp/pages/layout-v2/sources/[source]/best-of/[year].tsx @@ -0,0 +1,8 @@ +import SourceYearlyArchivePage, { + getStaticPaths, + getStaticProps, +} from '../../../../sources/[source]/best-of/[year]'; +import { withLayoutVariant } from '../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(SourceYearlyArchivePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/sources/[source]/best-of/[year]/[month].tsx b/packages/webapp/pages/layout-v2/sources/[source]/best-of/[year]/[month].tsx new file mode 100644 index 00000000000..f111cb95e6b --- /dev/null +++ b/packages/webapp/pages/layout-v2/sources/[source]/best-of/[year]/[month].tsx @@ -0,0 +1,8 @@ +import SourceMonthlyArchivePage, { + getStaticPaths, + getStaticProps, +} from '../../../../../sources/[source]/best-of/[year]/[month]'; +import { withLayoutVariant } from '../../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(SourceMonthlyArchivePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/sources/[source]/best-of/index.tsx b/packages/webapp/pages/layout-v2/sources/[source]/best-of/index.tsx new file mode 100644 index 00000000000..511a536e9bb --- /dev/null +++ b/packages/webapp/pages/layout-v2/sources/[source]/best-of/index.tsx @@ -0,0 +1,8 @@ +import SourceArchiveIndexPage, { + getStaticPaths, + getStaticProps, +} from '../../../../sources/[source]/best-of'; +import { withLayoutVariant } from '../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(SourceArchiveIndexPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/squads/[handle]/index.tsx b/packages/webapp/pages/layout-v2/squads/[handle]/index.tsx new file mode 100644 index 00000000000..25f49e8deee --- /dev/null +++ b/packages/webapp/pages/layout-v2/squads/[handle]/index.tsx @@ -0,0 +1,5 @@ +import SquadPage, { getServerSideProps } from '../../../squads/[handle]'; +import { withLayoutVariant } from '../../../../lib/layoutVariantPage'; + +export { getServerSideProps }; +export default withLayoutVariant(SquadPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/squads/discover/[id].tsx b/packages/webapp/pages/layout-v2/squads/discover/[id].tsx new file mode 100644 index 00000000000..69567214c23 --- /dev/null +++ b/packages/webapp/pages/layout-v2/squads/discover/[id].tsx @@ -0,0 +1,7 @@ +import SquadCategoryPage, { + getServerSideProps, +} from '../../../squads/discover/[id]'; +import { withLayoutVariant } from '../../../../lib/layoutVariantPage'; + +export { getServerSideProps }; +export default withLayoutVariant(SquadCategoryPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/standups/[id].tsx b/packages/webapp/pages/layout-v2/standups/[id].tsx new file mode 100644 index 00000000000..bf94eb730ed --- /dev/null +++ b/packages/webapp/pages/layout-v2/standups/[id].tsx @@ -0,0 +1,5 @@ +import StandupPage, { getServerSideProps } from '../../standups/[id]'; +import { withLayoutVariant } from '../../../lib/layoutVariantPage'; + +export { getServerSideProps }; +export default withLayoutVariant(StandupPage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/tags/[tag]/best-of/[year].tsx b/packages/webapp/pages/layout-v2/tags/[tag]/best-of/[year].tsx new file mode 100644 index 00000000000..a48b5597c18 --- /dev/null +++ b/packages/webapp/pages/layout-v2/tags/[tag]/best-of/[year].tsx @@ -0,0 +1,8 @@ +import TagYearlyArchivePage, { + getStaticPaths, + getStaticProps, +} from '../../../../tags/[tag]/best-of/[year]'; +import { withLayoutVariant } from '../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(TagYearlyArchivePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/tags/[tag]/best-of/[year]/[month].tsx b/packages/webapp/pages/layout-v2/tags/[tag]/best-of/[year]/[month].tsx new file mode 100644 index 00000000000..53cdaa9999c --- /dev/null +++ b/packages/webapp/pages/layout-v2/tags/[tag]/best-of/[year]/[month].tsx @@ -0,0 +1,8 @@ +import TagMonthlyArchivePage, { + getStaticPaths, + getStaticProps, +} from '../../../../../tags/[tag]/best-of/[year]/[month]'; +import { withLayoutVariant } from '../../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(TagMonthlyArchivePage, 'v2'); diff --git a/packages/webapp/pages/layout-v2/tags/[tag]/best-of/index.tsx b/packages/webapp/pages/layout-v2/tags/[tag]/best-of/index.tsx new file mode 100644 index 00000000000..3fa0667370c --- /dev/null +++ b/packages/webapp/pages/layout-v2/tags/[tag]/best-of/index.tsx @@ -0,0 +1,8 @@ +import TagArchiveIndexPage, { + getStaticPaths, + getStaticProps, +} from '../../../../tags/[tag]/best-of'; +import { withLayoutVariant } from '../../../../../lib/layoutVariantPage'; + +export { getStaticPaths, getStaticProps }; +export default withLayoutVariant(TagArchiveIndexPage, 'v2'); diff --git a/packages/webapp/pages/posts/[id]/index.tsx b/packages/webapp/pages/posts/[id]/index.tsx index 955659d6d19..4b5c1f012dd 100644 --- a/packages/webapp/pages/posts/[id]/index.tsx +++ b/packages/webapp/pages/posts/[id]/index.tsx @@ -52,6 +52,7 @@ import { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditio import { isPostRedesignEligible } from '@dailydotdev/shared/src/hooks/post/usePostRedesign'; import { featurePostRedesign } from '@dailydotdev/shared/src/lib/featureManagement'; import { PostFocusCard } from '@dailydotdev/shared/src/components/post/focus/PostFocusCard'; +import type { MainLayoutProps } from '@dailydotdev/shared/src/components/MainLayout'; import { getPageSeoTitles } from '../../../components/layouts/utils'; import { getLayout } from '../../../components/layouts/MainLayout'; import FooterNavBarLayout from '../../../components/layouts/FooterNavBarLayout'; @@ -127,6 +128,7 @@ export interface Props extends DynamicSeoProps { initialData?: PostData; topComments?: Comment[]; error?: ApiError; + isLayoutV2?: boolean; } type PostContentComponent = ComponentType; @@ -169,6 +171,7 @@ export const PostPage = ({ initialData, topComments, error, + isLayoutV2, }: Props): ReactElement => { useJoinReferral(); const { logEvent } = useLogContext(); @@ -199,6 +202,10 @@ export const PostPage = ({ const featureTheme = useFeatureTheme(); const containerClass = classNames( 'mb-16 min-h-page max-w-[69.25rem] tablet:mb-8 laptop:mb-0 laptop:pb-6 laptopL:pb-0', + // PageBodyContainer uses `m-auto` for horizontal centering. Inside the v2 + // flex card, its auto top margin consumes spare height and pushes the + //
/