diff --git a/apps/web/src/app/[username]/page.tsx b/apps/web/src/app/[username]/page.tsx index c6c90b28..dd61157b 100644 --- a/apps/web/src/app/[username]/page.tsx +++ b/apps/web/src/app/[username]/page.tsx @@ -25,6 +25,7 @@ import Link from 'next/link' import { useTheme } from 'next-themes' import { GitHubCalendar } from 'react-github-calendar' import { Logo } from '@/components/logo' +import { MdtStatusPage } from '@/components/mdt-status-page' import { TECH_CATALOG } from '@/components/tech-stack-picker' type PublicProfile = { @@ -230,43 +231,31 @@ export default function PublicProfilePage() { /* ── Error / 404 state ── */ if (error || !profile) { return ( -
- {/* Ambient glow */} -
-
-
- -
- -
-

- 404 -

-

- This profile doesn't exist yet. -

-

- The username @{username} hasn't been claimed. -
Create your own developer profile for free. -

-
-
- - - Create Profile - - - Explore Tools - -
-
-
+ + + + Create Profile + + + Explore Tools + + ) } diff --git a/apps/web/src/app/[username]/public-profile-client.tsx b/apps/web/src/app/[username]/public-profile-client.tsx index f7616114..0af94c0c 100644 --- a/apps/web/src/app/[username]/public-profile-client.tsx +++ b/apps/web/src/app/[username]/public-profile-client.tsx @@ -40,6 +40,7 @@ import { GitHubCalendar } from 'react-github-calendar' import { toast } from 'sonner' import { Logo } from '@/components/logo' import { TECH_CATALOG } from '@/components/tech-stack-picker' +import { MdtStatusPage } from '@/components/mdt-status-page' import { cn } from '@/lib/utils' /* ──────────────────────────────────────────────────────────────────────── */ @@ -788,42 +789,31 @@ export default function PublicProfileClient({ username: usernameParam }: { usern if (error || !profile) { return ( -
-
-
-
-
- -
-

- 404 -

-

- This profile doesn't exist yet. -

-

- The username @{usernameSegment} hasn't been claimed. -
- Create your own developer profile for free. -

-
-
- - - Create Profile - - - Explore Tools - -
-
-
+ + + + Create Profile + + + Explore Tools + + ) } diff --git a/apps/web/src/app/error.tsx b/apps/web/src/app/error.tsx new file mode 100644 index 00000000..6fa84252 --- /dev/null +++ b/apps/web/src/app/error.tsx @@ -0,0 +1,47 @@ +'use client' + +import Link from 'next/link' +import { Home, RotateCw } from 'lucide-react' +import { MdtStatusPage } from '@/components/mdt-status-page' + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( + + + + + Back home + + + ) +} diff --git a/apps/web/src/app/global-error.tsx b/apps/web/src/app/global-error.tsx new file mode 100644 index 00000000..6a22c3fa --- /dev/null +++ b/apps/web/src/app/global-error.tsx @@ -0,0 +1,42 @@ +'use client' + +import { GeistSans } from 'geist/font/sans' +import { GeistMono } from 'geist/font/mono' +import { RotateCw } from 'lucide-react' +import { MdtStatusPage } from '@/components/mdt-status-page' +import './globals.css' + +// Catches errors thrown in the root layout itself — must render its own /. +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return ( + + + + + + + + ) +} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index cc526de1..cade53e4 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -1525,6 +1525,27 @@ html { overflow-x: hidden; } position: absolute; inset: 0; pointer-events: none; opacity: .28; mix-blend-mode: soft-light; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); } +/* CRT scanlines — status pages (subtle, matches the boot loader) */ +.mdt-status-scanlines { + pointer-events: none; opacity: .5; + background-image: repeating-linear-gradient( + to bottom, + rgba(255, 255, 255, 0.03) 0px, + rgba(255, 255, 255, 0.03) 1px, + transparent 1px, + transparent 3px + ); + mask-image: radial-gradient(ellipse 80% 60% at 50% 45%, #000 30%, transparent 100%); + -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 45%, #000 30%, transparent 100%); +} +@media (prefers-reduced-motion: no-preference) { + .mdt-status-scanlines { animation: mdt-scanline-roll 8s linear infinite; } +} +@keyframes mdt-scanline-roll { + from { background-position: 0 0; } + to { background-position: 0 6px; } +} + /* content stacks above the backdrop naturally; backdrop sits at z-index:-1. (No blanket position rule — it would override fixed/sticky children.) */ diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index e59c85e2..63ccfe4c 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -1,10 +1,12 @@ import type { Metadata } from "next"; import Link from "next/link"; +import { ArrowLeft, Cloud, ShieldCheck, Zap } from "lucide-react"; import { LegalAgreementFooter } from "@/components/legal-agreement-footer"; import { LoginForm } from "@/components/login-form"; import { LoginRedirectIfAuthed } from "@/components/login-redirect-if-authed"; import { Logo } from "@/components/logo"; -import { ArrowLeft } from "lucide-react"; +import { Magnetic } from "@/components/mdt-magnetic"; +import MdtAurora from "@/components/mdt-aurora"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://mydevtools.tech"; @@ -14,52 +16,129 @@ export const metadata: Metadata = { alternates: { canonical: `${baseUrl}/login` }, }; +const FEATURES = [ + { + icon: Zap, + title: "60+ tools, zero installs", + body: "Formatters, generators, API client, SQL, crypto — all in one tab.", + }, + { + icon: ShieldCheck, + title: "Encrypted, cross-device sync", + body: "Sensitive data is AES-256 encrypted before it ever leaves your browser.", + }, + { + icon: Cloud, + title: "Self-host or cloud", + body: "Run it yourself for free, or sign in and let us handle the infrastructure.", + }, +]; + +const TOOL_PILLS = [ + "JSON", + "JWT", + "Regex", + "Base64", + "UUID", + "API Client", + "SQL", + "GraphQL", +]; + export default function LoginPage() { return ( -
+
-
-
-
+ {/* deck backdrop */} +
+ +
+
+
+
+ + + + + + + + + +
- - - Back to home - + + + + Back to home + +
-
-
-
+
+ {/* ── Showcase (desktop only) ── */} +
+

Welcome back

+

+ Your entire dev toolkit,{" "} + one tab away. +

+

+ Sign in to sync your work across devices and pick up exactly where you left off. +

+ +
    + {FEATURES.map(({ icon: Icon, title, body }) => ( +
  • + + + +
    +

    {title}

    +

    {body}

    +
    +
  • + ))} +
+ +
+ {TOOL_PILLS.map((pill) => ( + + {pill} + + ))} +
+
+ + {/* ── Login card ── */} +
+
+
+
-

- Welcome back -

-

- Sign in with your preferred provider to open your workspace. +

+ Sign in to MyDevTools +

+

+ Continue with your preferred provider to open your workspace.

+ +

+ No passwords. One-click OAuth — we never see your credentials. +

- -
+ +
); diff --git a/apps/web/src/app/maintenance/page.tsx b/apps/web/src/app/maintenance/page.tsx new file mode 100644 index 00000000..c92ae8f9 --- /dev/null +++ b/apps/web/src/app/maintenance/page.tsx @@ -0,0 +1,41 @@ +import type { Metadata } from 'next' +import Link from 'next/link' +import { Wrench } from 'lucide-react' +import { MdtStatusPage } from '@/components/mdt-status-page' + +export const metadata: Metadata = { + title: 'Down for maintenance', + description: 'MyDevTools is temporarily down for scheduled maintenance.', + robots: { index: false, follow: false }, +} + +export default function MaintenancePage() { + return ( + } + kicker="Scheduled maintenance" + title="We're tuning things up." + description="MyDevTools is temporarily offline for maintenance. We'll be back shortly — thanks for your patience." + diagnostics={[ + 'systems offline for scheduled maintenance', + 'status: upgrade in progress…', + 'eta: back shortly — hang tight', + ]} + > + + Retry + + + Self-host meanwhile + + + ) +} diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx new file mode 100644 index 00000000..4b5ef6b5 --- /dev/null +++ b/apps/web/src/app/not-found.tsx @@ -0,0 +1,34 @@ +import Link from 'next/link' +import { ArrowRight, Home } from 'lucide-react' +import { MdtStatusPage } from '@/components/mdt-status-page' + +export default function NotFound() { + return ( + + + + Back home + + + Browse all tools + + + + ) +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index eeb6e9fe..d5204294 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -29,9 +29,7 @@ import { Globe, CheckCircle2, Search, - Server, Cloud, - Crown, } from "lucide-react"; import { sidebarData } from "@/components/sidebar/data/sidebar-data"; import { homepageFaqItems } from "@/lib/seo/structured-data"; @@ -41,6 +39,7 @@ import { Magnetic } from "@/components/mdt-magnetic"; import { Tilt } from "@/components/mdt-tilt"; import { MdtBoot } from "@/components/mdt-boot"; import { MdtDashboard } from "@/components/mdt-dashboard"; +import { PricingTiers } from "@/components/pricing-tiers"; // ─── Animation Variants ──────────────────────────────────────────────────────── @@ -710,170 +709,10 @@ export default function Page() { {/* ── Pricing ─────────────────────────────────────────────────────────── */}
-
-
- - - - Pricing - -

- Self-host free. Cloud is paid. -

-

- Same open-source product everywhere. Run it yourself at no cost, - or let us handle hosting with a paid subscription. -

-
- -
- {/* Self-host */} - -
-
- -
-
-

Self-hosted

-

- $0{" "} - - forever - -

-

- Clone the repo, deploy the web app and backend, and use every - tool with no license fee and no usage limits from us. -

-
    - {[ - "Full source under GPL-3.0", - "All tools and features included", - "You control data & infrastructure", - ].map((line) => ( -
  • - - {line} -
  • - ))} -
- - View on GitHub - - -
- - {/* Cloud */} - - - Paid - -
-
- -
-
-

MyDevTools Cloud

-

- Subscription -

-

- Managed hosting with secure sync and backups. -

-

- We run the infrastructure so you do not have to. Your sensitive - data stays encrypted before sync; plaintext stays in your browser. -

-
    - {[ - "Same product, zero deploy hassle", - "Secure sync and backups from us", - "Cancel any time", - ].map((line) => ( -
  • - - {line} -
  • - ))} -
- -
- - {/* Premium */} - - - Coming soon - -
-
- -
-
-

Premium

-

Add-ons

-

- Optional paid capabilities on top of Cloud for teams, advanced - workflows, and managed collaboration. Self-hosting remains fully - capable for individual developers. -

-
    - {[ - "Advanced workflows for power users", - "Team-oriented collaboration features", - "Self-host remains fully capable", - ].map((line) => ( -
  • - - {line} -
  • - ))} -
- -
-
-
-
+
{/* ── Tools teaser ────────────────────────────────────────────────────── */} diff --git a/apps/web/src/app/pricing/page.tsx b/apps/web/src/app/pricing/page.tsx index 04ec7dde..ddfce11a 100644 --- a/apps/web/src/app/pricing/page.tsx +++ b/apps/web/src/app/pricing/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import { MarketingSeoPage } from '@/components/marketing-seo-page' +import { PricingTiers } from '@/components/pricing-tiers' import { getPlatformSeoPage } from '@/lib/seo/platform-pages' const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://mydevtools.tech' @@ -28,5 +29,9 @@ export const metadata: Metadata = { export default function PricingPage() { if (!page) return null - return + return ( + + + + ) } diff --git a/apps/web/src/components/marketing-seo-page.tsx b/apps/web/src/components/marketing-seo-page.tsx index b811df99..6c75b9cb 100644 --- a/apps/web/src/components/marketing-seo-page.tsx +++ b/apps/web/src/components/marketing-seo-page.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import Link from 'next/link' import { ArrowRight, CheckCircle2 } from 'lucide-react' import { Header } from '@/components/header' @@ -50,7 +51,13 @@ function CtaLink({ ) } -export function MarketingSeoPage({ page }: { page: PlatformSeoPage }) { +export function MarketingSeoPage({ + page, + children, +}: { + page: PlatformSeoPage + children?: ReactNode +}) { const popularTools = popularToolSlugs .filter((slug) => publicToolSlugs.includes(slug) && toolsMetadata[slug]) .map((slug) => ({ slug, ...toolsMetadata[slug] })) @@ -118,6 +125,8 @@ export function MarketingSeoPage({ page }: { page: PlatformSeoPage }) {
+ {children} +
{page.sections.map((section) => ( diff --git a/apps/web/src/components/mdt-status-page.tsx b/apps/web/src/components/mdt-status-page.tsx new file mode 100644 index 00000000..7e9fcc33 --- /dev/null +++ b/apps/web/src/components/mdt-status-page.tsx @@ -0,0 +1,245 @@ +'use client' + +import { useEffect, useState, type ReactNode } from 'react' +import { + motion, + useMotionValue, + useSpring, + useTransform, + useReducedMotion, +} from 'framer-motion' +import MdtAurora from '@/components/mdt-aurora' + +// ── Glitching gradient code (404 / 500) ──────────────────────────────────── +// Base gradient glyphs + two RGB-split clones that jitter on a long delay. +function GlitchCode({ text, reduce }: { text: string; reduce: boolean }) { + const common = + 'absolute inset-0 select-none font-bold leading-none tracking-tight' + const jitter = (color: string, sign: number) => + reduce + ? {} + : { + animate: { x: [0, sign * 3, sign * -2, 0], opacity: [0, 0.9, 0.6, 0] }, + transition: { + duration: 0.45, + times: [0, 0.3, 0.6, 1], + repeat: Infinity, + repeatDelay: 3.2, + ease: 'easeInOut' as const, + }, + style: { color, mixBlendMode: 'screen' as const }, + } + return ( +
+ {/* readable, gradient-filled base */} + + {text} + + + {text} + + + {text} + +
+ ) +} + +// ── Typing diagnostic terminal ────────────────────────────────────────────── +function severityClass(line: string) { + const l = line.toLowerCase() + if (/(404|500|exception|error|offline|fail)/.test(l)) return 'text-amber-300/90' + if (/(ok|hint|eta|back|done|ready|retry)/.test(l)) return 'text-emerald-300/90' + return 'text-foreground/70' +} + +function DiagnosticTerminal({ + lines, + reduce, +}: { + lines: string[] + reduce: boolean +}) { + const [shown, setShown] = useState(reduce ? lines.length : 0) + + useEffect(() => { + if (reduce || shown >= lines.length) return + const id = setTimeout(() => setShown((n) => n + 1), shown === 0 ? 350 : 520) + return () => clearTimeout(id) + }, [shown, lines.length, reduce]) + + return ( +
+
+
+ + + + + mydevtools · diagnostics + +
+
+ {lines.slice(0, shown).map((line, i) => ( + + + + {line} + {!reduce && i === shown - 1 && shown < lines.length ? ( + + ) : null} + + + ))} +
+
+ ) +} + +// ── Drifting particles (deterministic → no hydration mismatch) ────────────── +const PARTICLES = Array.from({ length: 16 }, (_, i) => ({ + left: (i * 61.8) % 100, + delay: (i % 8) * 1.1, + duration: 9 + (i % 5) * 2, + size: 1 + (i % 3), +})) + +function Particles() { + return ( +
+ {PARTICLES.map((p, i) => ( + + ))} +
+ ) +} + +// ── Shell ──────────────────────────────────────────────────────────────────── +export function MdtStatusPage({ + code, + kicker, + title, + description, + diagnostics, + children, +}: { + code: ReactNode + kicker: string + title: string + description: string + diagnostics?: string[] + children?: ReactNode +}) { + const reduce = useReducedMotion() ?? false + const px = useMotionValue(0) + const py = useMotionValue(0) + const sx = useSpring(px, { stiffness: 120, damping: 18, mass: 0.6 }) + const sy = useSpring(py, { stiffness: 120, damping: 18, mass: 0.6 }) + + // Fixed parallax layers — hooks called unconditionally, depth = px shift. + const codeX = useTransform(sx, (v) => v * 38) + const codeY = useTransform(sy, (v) => v * 38) + const kickX = useTransform(sx, (v) => v * 20) + const kickY = useTransform(sy, (v) => v * 20) + const titleX = useTransform(sx, (v) => v * 14) + const titleY = useTransform(sy, (v) => v * 14) + + const onMove = (e: React.MouseEvent) => { + if (reduce) return + px.set(e.clientX / window.innerWidth - 0.5) + py.set(e.clientY / window.innerHeight - 0.5) + } + + return ( +
+ {/* fixed backdrop: aurora + grid + noise + gradient def */} +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + {typeof code === 'string' ? ( + + ) : ( + + {code} + + )} + + + + {kicker} + + + {title} + +

+ {description} +

+ + {diagnostics && diagnostics.length > 0 ? ( +
+ +
+ ) : null} + +
+ {children} +
+
+ ) +} diff --git a/apps/web/src/components/pricing-tiers.tsx b/apps/web/src/components/pricing-tiers.tsx new file mode 100644 index 00000000..4c1d7414 --- /dev/null +++ b/apps/web/src/components/pricing-tiers.tsx @@ -0,0 +1,197 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' +import { ArrowRight, Check } from 'lucide-react' + +type Billing = 'monthly' | 'annual' + +type Tier = { + name: string + tagline: string + monthly: number | null // null = custom / contact + annual: number | null // total per year (per seat for Team); 0 = free + perSeat?: boolean + cta: { href: string; label: string; external?: boolean } + features: string[] + highlight?: boolean +} + +// ponytail: prices live here, single source. Mirrors HANDOFF "Business decisions". +const TIERS: Tier[] = [ + { + name: 'Free', + tagline: 'Self-host, or try the cloud with capped usage.', + monthly: 0, + annual: 0, + cta: { href: '/login', label: 'Start free' }, + features: [ + 'Self-host the full open-source toolkit', + 'Capped cloud usage, no card required', + 'All core developer tools', + 'Community support', + ], + }, + { + name: 'Pro', + tagline: 'For individual developers who live in the cloud.', + monthly: 9, + annual: 84, + highlight: true, + cta: { href: '/login', label: 'Start Pro' }, + features: [ + 'Unlimited cloud usage', + 'Cross-device sync', + 'Private history & saved snippets', + 'Priority support', + ], + }, + { + name: 'Team', + tagline: 'Shared workspaces for small teams.', + monthly: 15, + annual: 144, + perSeat: true, + cta: { href: '/login', label: 'Start Team' }, + features: [ + 'Everything in Pro, per seat', + 'Shared team workspace', + 'Roles & access controls', + 'Centralized billing', + ], + }, + { + name: 'Enterprise', + tagline: 'Compliance, SSO, and support at scale.', + monthly: null, + annual: null, + cta: { href: 'mailto:support@mydevtools.tech', label: 'Contact sales', external: true }, + features: [ + 'SSO / SAML & audit logs', + 'Self-host with a support SLA', + 'Security review & DPA', + 'Dedicated onboarding', + ], + }, +] + +function priceLabel(tier: Tier, billing: Billing): { big: string; sub: string } { + if (tier.monthly === null) return { big: 'Custom', sub: "Let's scope it together" } + if (tier.monthly === 0) return { big: '$0', sub: 'Free forever' } + + const seat = tier.perSeat ? '/user' : '' + if (billing === 'monthly') { + return { big: `$${tier.monthly}`, sub: `per month${seat}` } + } + // annual: show effective monthly, note the yearly total + const perMonth = Math.round((tier.annual as number) / 12) + return { + big: `$${perMonth}`, + sub: `per month${seat}, billed $${tier.annual}/yr`, + } +} + +export function PricingTiers() { + const [billing, setBilling] = useState('monthly') + + return ( +
+
+
+

+ Simple, honest pricing +

+

+ Self-host for free, forever. Upgrade to cloud when you want the convenience. +

+ +
+ {(['monthly', 'annual'] as const).map((option) => ( + + ))} +
+

+ {billing === 'annual' ? ( + Annual saves ~2 months + ) : ( + 'Switch to annual for ~2 months free.' + )} +

+
+ +
+ {TIERS.map((tier) => { + const { big, sub } = priceLabel(tier, billing) + return ( +
+ {tier.highlight ? ( + <> +
+ Most popular + + ) : null} + +

{tier.name}

+

+ {tier.tagline} +

+ +
+
{big}
+
{sub}
+
+ + + {tier.cta.label} + + + +
    + {tier.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+
+ ) + })} +
+
+
+ ) +} diff --git a/apps/web/src/lib/seo/platform-pages.ts b/apps/web/src/lib/seo/platform-pages.ts index 2714a767..aa7aff7f 100644 --- a/apps/web/src/lib/seo/platform-pages.ts +++ b/apps/web/src/lib/seo/platform-pages.ts @@ -295,33 +295,33 @@ export const platformSeoPages: PlatformSeoPage[] = [ }, sections: [ { - title: 'Self-hosted', + title: 'Free', body: - 'Use the GPL-3.0 source code on your own infrastructure with no license fee from MyDevTools.', + 'Self-host the GPL-3.0 source on your own infrastructure, or try the cloud with capped usage — no card required.', bullets: [ - '$0 license fee from MyDevTools.', - 'All included tools and features remain available.', - 'You own hosting, operations, backups, and access controls.', + '$0 forever — self-host with no license fee.', + 'All core developer tools included.', + 'Capped cloud usage to try before you buy.', ], }, { - title: 'MyDevTools Cloud', + title: 'Pro & Team', body: - 'Managed hosting for developers who want the toolkit available without deployment work.', + 'Managed cloud for developers who would rather not run infrastructure. Pro is $9/mo per developer; Team is $15/mo per user.', bullets: [ - 'Hosted dashboard and account sync.', - 'Secure sync for sensitive data using browser-side encryption where supported.', - 'Subscription details will be shown clearly before paid billing is required.', + 'Pro: unlimited cloud usage, cross-device sync, private history.', + 'Team: shared workspace, roles, and centralized billing.', + 'Annual billing saves roughly two months.', ], }, { - title: 'Premium add-ons', + title: 'Enterprise', body: - 'Future paid capabilities will focus on power-user and team workflows without removing the core self-hosted toolkit.', + 'Custom plans for organizations that need SSO, compliance, and support at scale.', bullets: [ - 'Advanced workflows for heavy users.', - 'Team-oriented collaboration features.', - 'The open-source self-hosted path remains a first-class option.', + 'SSO / SAML, audit logs, and a security review.', + 'Self-host with a support SLA.', + 'Dedicated onboarding — contact support@mydevtools.tech.', ], }, ], diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index ffc79fc0..86b1630c 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,18 +1,31 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -/** - * Forward pathname to server components (e.g. JSON-LD on /app/*) without exposing - * it as a public response header. - */ export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl + + // Maintenance gate: when MAINTENANCE_MODE=true, serve /maintenance for every + // page request (URL preserved via rewrite). Off by default — zero overhead. + // ponytail: env flag, no admin UI; flip the var and redeploy to toggle. + if (process.env.MAINTENANCE_MODE === 'true' && !pathname.startsWith('/maintenance')) { + const url = request.nextUrl.clone() + url.pathname = '/maintenance' + return NextResponse.rewrite(url, { + status: 503, + headers: { 'Retry-After': '3600' }, + }) + } + + // Forward pathname to server components (e.g. JSON-LD on /app/*) without + // exposing it as a public response header. const requestHeaders = new Headers(request.headers) - requestHeaders.set('x-mdt-pathname', request.nextUrl.pathname) + requestHeaders.set('x-mdt-pathname', pathname) return NextResponse.next({ request: { headers: requestHeaders }, }) } +// Run on all page routes (skip Next internals, static assets, files with an extension). export const config = { - matcher: ['/app/:path*'], + matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.[\\w]+$).*)'], }