.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
unstable_retry,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
unstable_retry: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main className="site-shell">
|
||||
<section className="hero" aria-labelledby="error-heading">
|
||||
<div className="container">
|
||||
<div className="hero-copy">
|
||||
<p className="eyebrow" aria-hidden="true">
|
||||
Error
|
||||
</p>
|
||||
<h1 id="error-heading">Something went wrong</h1>
|
||||
<p className="hero-text">
|
||||
An unexpected error occurred. Please try again, or contact us at{" "}
|
||||
<a href="mailto:contact@novarixnet.com" style={{ color: "var(--accent)" }}>
|
||||
contact@novarixnet.com
|
||||
</a>{" "}
|
||||
if the problem persists.
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<button className="button button-primary" onClick={unstable_retry}>
|
||||
Try again
|
||||
</button>
|
||||
<a href="/" className="button button-secondary">
|
||||
Go home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
+1088
File diff suppressed because it is too large
Load Diff
+162
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* layout.tsx — Root layout for Novarix Networks
|
||||
*
|
||||
* Responsible for:
|
||||
* • Loading "DM Sans" via next/font/google (self-hosted, zero CLS).
|
||||
* • Applying global CSS (globals.css).
|
||||
* • Exporting page-level metadata for <head> injection, Open Graph, and
|
||||
* Twitter card meta.
|
||||
* • Setting viewport and theme-colour meta tags.
|
||||
* • Injecting JSON-LD Organisation structured data for rich search results.
|
||||
*
|
||||
* ─── Adding pages ────────────────────────────────────────────────────────
|
||||
*
|
||||
* New pages created under /app inherit this layout automatically.
|
||||
* To create a nested layout (e.g. for a /services sub-section), add a
|
||||
* layout.tsx inside /app/services/ — Next.js will compose them.
|
||||
*/
|
||||
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { DM_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
/* ── Font ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* DM Sans loaded via next/font/google.
|
||||
* The font is downloaded at build time and served from /_next/static/media/
|
||||
* — no third-party request at runtime, eliminating the CLS caused by
|
||||
* loading from fonts.googleapis.com.
|
||||
*
|
||||
* `variable` exposes the font as a CSS custom property (--font-dm-sans)
|
||||
* so globals.css can reference it without importing from JS.
|
||||
*/
|
||||
const dmSans = DM_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-dm-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
/* ── JSON-LD structured data ───────────────────────────────────────────── */
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "Novarix Networks",
|
||||
url: "https://novarixnet.com",
|
||||
email: "contact@novarixnet.com",
|
||||
description:
|
||||
"Novarix Networks provides business connectivity, managed network services, and transit-focused infrastructure support for organisations that need dependable engineering and clear technical ownership.",
|
||||
knowsAbout: [
|
||||
"Business Connectivity",
|
||||
"Managed Network Services",
|
||||
"IP Transit",
|
||||
"Peering",
|
||||
"BGP",
|
||||
"Network Engineering",
|
||||
],
|
||||
};
|
||||
|
||||
/* ── Site metadata ─────────────────────────────────────────────────────── */
|
||||
|
||||
export const metadata: Metadata = {
|
||||
/**
|
||||
* metadataBase resolves relative URLs in metadata fields (e.g. OG images).
|
||||
* Pull from an environment variable per-environment if needed:
|
||||
*
|
||||
* metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://novarixnet.com"),
|
||||
*/
|
||||
metadataBase: new URL("https://novarixnet.com"),
|
||||
|
||||
title: {
|
||||
default: "Novarix Networks",
|
||||
template: "%s | Novarix Networks",
|
||||
},
|
||||
|
||||
description:
|
||||
"Novarix Networks provides business connectivity, managed network services, and transit-focused infrastructure support for organisations that need dependable engineering and clear technical ownership.",
|
||||
|
||||
applicationName: "Novarix Networks",
|
||||
|
||||
keywords: [
|
||||
"Novarix Networks",
|
||||
"ISP",
|
||||
"Managed Service Provider",
|
||||
"MSP",
|
||||
"IP Transit",
|
||||
"Business Connectivity",
|
||||
"Peering",
|
||||
"BGP",
|
||||
"IXP",
|
||||
"CDN Edge",
|
||||
"Network Engineering",
|
||||
],
|
||||
|
||||
authors: [{ name: "Novarix Networks" }],
|
||||
creator: "Novarix Networks",
|
||||
publisher: "Novarix Networks",
|
||||
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
},
|
||||
|
||||
openGraph: {
|
||||
type: "website",
|
||||
url: "https://novarixnet.com",
|
||||
siteName: "Novarix Networks",
|
||||
title: "Novarix Networks",
|
||||
description:
|
||||
"Business connectivity, managed network services, transit, and engineering-led infrastructure support.",
|
||||
locale: "en_GB",
|
||||
/*
|
||||
* Add a 1200 × 630 px image to /public/og-image.png to enable rich
|
||||
* link previews on LinkedIn, Slack, and Facebook:
|
||||
*
|
||||
* images: [{ url: "/og-image.png", width: 1200, height: 630 }],
|
||||
*/
|
||||
},
|
||||
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Novarix Networks",
|
||||
description:
|
||||
"Business connectivity, managed network services, transit, and engineering-led infrastructure support.",
|
||||
/*
|
||||
* images: ["/og-image.png"],
|
||||
*/
|
||||
},
|
||||
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
|
||||
/* ── Viewport & theme colour ───────────────────────────────────────────── */
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#020617" },
|
||||
],
|
||||
};
|
||||
|
||||
/* ── Root layout component ─────────────────────────────────────────────── */
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en-GB" suppressHydrationWarning className={dmSans.variable}>
|
||||
<head>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Page Not Found",
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="site-shell">
|
||||
<section className="hero" aria-labelledby="notfound-heading">
|
||||
<div className="container">
|
||||
<div className="hero-copy">
|
||||
<p className="eyebrow" aria-hidden="true">
|
||||
404
|
||||
</p>
|
||||
<h1 id="notfound-heading">Page not found</h1>
|
||||
<p className="hero-text">
|
||||
The page you’re looking for doesn’t exist or has been
|
||||
moved.
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<Link href="/" className="button button-primary">
|
||||
Go home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+540
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* page.tsx — Novarix Networks home page
|
||||
*
|
||||
* This is the only route in the site ("/"). It renders a single-page layout
|
||||
* with the following sections:
|
||||
*
|
||||
* 1. Intro overlay — first-visit only splash (Lottie + wordmark)
|
||||
* 2. Header — sticky nav with brand wordmark
|
||||
* 3. Hero — headline, sub-text, CTA buttons
|
||||
* 4. Services — three service cards
|
||||
* 5. Direction — three-phase platform roadmap
|
||||
* 6. Contact — mailto link
|
||||
* 7. Footer — copyright line
|
||||
*
|
||||
* ─── State overview ──────────────────────────────────────────────────────
|
||||
*
|
||||
* showIntro Controls whether the splash overlay is rendered. Starts
|
||||
* false (SSR safe), is set to true on first mount if the
|
||||
* "novarix-intro-seen" sessionStorage key is absent, then
|
||||
* reverts to false after the intro duration elapses.
|
||||
*
|
||||
* mounted Prevents the intro overlay from rendering during SSR / the
|
||||
* hydration pass, which would cause a server/client mismatch.
|
||||
*
|
||||
* pointer Tracks normalised cursor position (0–100 %) for the ambient
|
||||
* radial-gradient background effect.
|
||||
*
|
||||
* ─── Adding / editing content ────────────────────────────────────────────
|
||||
*
|
||||
* • Update the `services` array to change service cards.
|
||||
* • Update the direction items inline in the JSX.
|
||||
* • The intro animation JSON lives at /public/branding/animated_logo_intro.json
|
||||
* • Wordmark images live at /public/branding/novarix-wordmark-{colour,white}.png
|
||||
*
|
||||
* ─── Dependencies ────────────────────────────────────────────────────────
|
||||
*
|
||||
* next/image — optimised image component (lazy-loading, AVIF/WebP)
|
||||
* lottie-react — renders the Bodymovin / Lottie JSON animation
|
||||
*
|
||||
* Install lottie-react if not already present:
|
||||
* npm install lottie-react
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Lottie is dynamically imported so neither the library (~150 KB) nor the
|
||||
* animation JSON (261 KB) are included in the initial page bundle. They are
|
||||
* fetched only on first visit when the intro overlay is needed.
|
||||
*/
|
||||
const Lottie = dynamic(() => import("lottie-react"), { ssr: false });
|
||||
|
||||
/* ── Service card data ─────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Each entry renders one card in the Services section.
|
||||
* Add, remove, or reorder entries here without touching the JSX.
|
||||
*
|
||||
* Fields:
|
||||
* title — card heading
|
||||
* description — body copy (2–3 sentences recommended)
|
||||
* icon — Unicode emoji or inline SVG string used as the card icon
|
||||
*/
|
||||
const services = [
|
||||
{
|
||||
eyebrow: "ISP",
|
||||
title: "Business Connectivity",
|
||||
description:
|
||||
"Routed internet access for organisations that need clear ownership, static addressing, and a provider willing to talk through the real topology.",
|
||||
points: ["Business internet access", "Static IP addressing", "Routed handoff options"],
|
||||
},
|
||||
{
|
||||
eyebrow: "MSP",
|
||||
title: "Managed Network Services",
|
||||
description:
|
||||
"Managed edge, routing, firewall, and switching support for teams that want stronger control without carrying every operational detail alone.",
|
||||
points: ["Managed edge infrastructure", "Change and migration support", "Operational visibility"],
|
||||
},
|
||||
{
|
||||
eyebrow: "Transit",
|
||||
title: "Transit and Interconnect",
|
||||
description:
|
||||
"Transit and interconnection planning for operators, platforms, and technical environments where routing posture matters.",
|
||||
points: ["IP transit readiness", "Peering strategy", "Carrier engagement"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const networkSignals = [
|
||||
{ label: "Operating focus", value: "ISP / MSP / Transit" },
|
||||
{ label: "Designed for", value: "Business-critical networks" },
|
||||
{ label: "Built around", value: "Practical engineering" },
|
||||
] as const;
|
||||
|
||||
const platformPhases = [
|
||||
{
|
||||
phase: "Phase 1",
|
||||
title: "Connectivity and managed support",
|
||||
description:
|
||||
"Lead with services that can be delivered credibly from day one: internet access, managed network support, and engineering advisory work.",
|
||||
},
|
||||
{
|
||||
phase: "Phase 2",
|
||||
title: "Transit, peering, and interconnect",
|
||||
description:
|
||||
"Grow toward exchange participation, partner interconnection, and clearer transit propositions as the network footprint matures.",
|
||||
},
|
||||
{
|
||||
phase: "Phase 3",
|
||||
title: "Selective edge infrastructure",
|
||||
description:
|
||||
"Introduce regional edge or platform capability only where real demand, operational control, and commercial return justify it.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
/* ── Types ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
type PointerState = {
|
||||
x: number; // cursor X as percentage of viewport width (0–100)
|
||||
y: number; // cursor Y as percentage of viewport height (0–100)
|
||||
};
|
||||
|
||||
/* ── Constants ─────────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Total duration the intro overlay is visible (milliseconds).
|
||||
* Must be long enough to cover:
|
||||
* • Lottie animation length
|
||||
* • Wordmark slide-in (350 ms delay + ~1 000 ms animation = ~1 350 ms)
|
||||
* • CSS fade-out at 2 450 ms (620 ms duration)
|
||||
*
|
||||
* The JS timer uses 3 200 ms so it matches the full CSS sequence before
|
||||
* React removes the overlay from the DOM.
|
||||
*/
|
||||
const INTRO_DURATION_MS = 3200;
|
||||
|
||||
/** sessionStorage key used to gate the intro to one play per browser tab */
|
||||
const INTRO_STORAGE_KEY = "novarix-intro-seen";
|
||||
|
||||
/* ── Component ─────────────────────────────────────────────────────────── */
|
||||
|
||||
export default function HomePage() {
|
||||
// Prevents intro from rendering during SSR / hydration mismatch
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Whether to show the intro overlay
|
||||
const [showIntro, setShowIntro] = useState(false);
|
||||
|
||||
// Lazily-loaded Lottie animation data (null until fetched)
|
||||
const [introAnimation, setIntroAnimation] = useState<Record<string, unknown> | null>(null);
|
||||
|
||||
// Normalised cursor position for the ambient gradient
|
||||
const [pointer, setPointer] = useState<PointerState>({ x: 50, y: 22 });
|
||||
|
||||
// Ref to the Lottie instance — can be used to imperatively control playback
|
||||
const lottieRef = useRef(null);
|
||||
|
||||
/* ── Side effects ── */
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
|
||||
let timer: number | undefined;
|
||||
|
||||
try {
|
||||
const introSeen = window.sessionStorage.getItem(INTRO_STORAGE_KEY);
|
||||
|
||||
if (!introSeen) {
|
||||
window.sessionStorage.setItem(INTRO_STORAGE_KEY, "true");
|
||||
|
||||
/*
|
||||
* Dynamically import the 261 KB animation JSON only on first visit.
|
||||
* This keeps it out of the initial page bundle entirely.
|
||||
*/
|
||||
import("@/public/branding/animated_logo_intro.json").then((mod) => {
|
||||
setIntroAnimation(mod.default as Record<string, unknown>);
|
||||
setShowIntro(true);
|
||||
timer = window.setTimeout(() => setShowIntro(false), INTRO_DURATION_MS);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/*
|
||||
* sessionStorage may be unavailable (private browsing restrictions,
|
||||
* storage quota exceeded, or cross-origin iframes). Silently skip
|
||||
* the intro rather than crashing.
|
||||
*/
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) window.clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/* ── Derived values ── */
|
||||
|
||||
/**
|
||||
* Memoised inline style object for the ambient background.
|
||||
* Only recalculated when pointer.x or pointer.y change, preventing
|
||||
* object identity churn on every render.
|
||||
*/
|
||||
const backgroundStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
"--mx": `${pointer.x}%`,
|
||||
"--my": `${pointer.y}%`,
|
||||
} as React.CSSProperties),
|
||||
[pointer.x, pointer.y]
|
||||
);
|
||||
|
||||
/* ── Handlers ── */
|
||||
|
||||
/**
|
||||
* Updates the pointer state on mouse movement.
|
||||
* Coordinates are normalised to the bounding rect of the main element
|
||||
* (rather than the window) to avoid edge cases near fixed headers.
|
||||
*/
|
||||
function handleMouseMove(event: React.MouseEvent<HTMLElement>) {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
setPointer({
|
||||
x: ((event.clientX - rect.left) / rect.width) * 100,
|
||||
y: ((event.clientY - rect.top) / rect.height) * 100,
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Render ── */
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Intro overlay ───────────────────────────────────────────────
|
||||
Rendered only:
|
||||
(a) after hydration (`mounted`)
|
||||
(b) on the first visit within the session (`showIntro`)
|
||||
|
||||
aria-hidden="true" hides it from screen readers; the main content
|
||||
is focusable without waiting for the intro to finish.
|
||||
─────────────────────────────────────────────────────────────────── */}
|
||||
{mounted && showIntro && (
|
||||
<div className="intro-overlay" aria-hidden="true">
|
||||
<div className="intro-backdrop" />
|
||||
|
||||
<div className="intro-shell">
|
||||
{/* Lottie logo animation — renders once animationData has loaded */}
|
||||
<div className="intro-lottie-wrap">
|
||||
{introAnimation && (
|
||||
<Lottie
|
||||
lottieRef={lottieRef}
|
||||
animationData={introAnimation}
|
||||
loop={false}
|
||||
autoplay
|
||||
className="intro-lottie"
|
||||
rendererSettings={{
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Wordmark — colour in light mode, white in dark mode */}
|
||||
<div className="intro-wordmark-wrap">
|
||||
<Image
|
||||
src="/branding/novarix-wordmark-colour.png"
|
||||
alt="Novarix Networks"
|
||||
width={2048}
|
||||
height={430}
|
||||
sizes="(max-width: 720px) 86vw, (max-width: 980px) 72vw, 864px"
|
||||
className="intro-wordmark intro-wordmark-light"
|
||||
/>
|
||||
<Image
|
||||
src="/branding/novarix-wordmark-white.png"
|
||||
alt="Novarix Networks"
|
||||
width={2048}
|
||||
height={430}
|
||||
sizes="(max-width: 720px) 86vw, (max-width: 980px) 72vw, 864px"
|
||||
className="intro-wordmark intro-wordmark-dark"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Page shell ──────────────────────────────────────────────────
|
||||
`intro-running` class hides section content while the intro
|
||||
overlay is active, preventing a flash of content beneath it.
|
||||
─────────────────────────────────────────────────────────────────── */}
|
||||
<main
|
||||
id="top"
|
||||
className={`site-shell${showIntro ? " intro-running" : ""}`}
|
||||
style={backgroundStyle}
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
{/* ── Ambient decorative layer (aria-hidden) ─────────────────── */}
|
||||
<div className="ambient-layer" aria-hidden="true">
|
||||
<div className="ambient-orb ambient-orb-a" />
|
||||
<div className="ambient-orb ambient-orb-b" />
|
||||
<div className="ambient-grid" />
|
||||
</div>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════
|
||||
HEADER
|
||||
════════════════════════════════════════════════════════════ */}
|
||||
<header className="site-header">
|
||||
<div className="container header-inner">
|
||||
{/* Brand wordmark — links back to the top of the page */}
|
||||
<a href="#top" className="brand-link" aria-label="Novarix Networks — go to top">
|
||||
<Image
|
||||
src="/branding/novarix-wordmark-colour.png"
|
||||
alt="Novarix Networks"
|
||||
width={2048}
|
||||
height={430}
|
||||
priority
|
||||
sizes="(max-width: 560px) 50vw, (max-width: 720px) 54vw, (max-width: 980px) 45vw, 250px"
|
||||
className="brand-image brand-image-light"
|
||||
/>
|
||||
<Image
|
||||
src="/branding/novarix-wordmark-white.png"
|
||||
alt="Novarix Networks"
|
||||
width={2048}
|
||||
height={430}
|
||||
priority
|
||||
sizes="(max-width: 560px) 50vw, (max-width: 720px) 54vw, (max-width: 980px) 45vw, 250px"
|
||||
className="brand-image brand-image-dark"
|
||||
/>
|
||||
</a>
|
||||
|
||||
{/* Primary navigation */}
|
||||
<nav className="nav" aria-label="Primary navigation">
|
||||
<a href="#services">Services</a>
|
||||
<a href="#network">Network</a>
|
||||
<a href="#direction">Direction</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════
|
||||
HERO
|
||||
════════════════════════════════════════════════════════════ */}
|
||||
<section className="hero" aria-labelledby="hero-heading">
|
||||
<div className="hero-mark" aria-hidden="true" />
|
||||
<div className="container">
|
||||
<div className="hero-copy">
|
||||
<p className="eyebrow" aria-hidden="true">
|
||||
Independent ISP · Managed Services · Transit
|
||||
</p>
|
||||
|
||||
<h1 id="hero-heading">
|
||||
Agile network infrastructure for organisations that cannot wait on slow providers.
|
||||
</h1>
|
||||
|
||||
<p className="hero-text">
|
||||
Novarix Networks is being built as an engineering-led ISP, MSP,
|
||||
and transit provider for businesses, operators, and demanding
|
||||
projects that need direct answers, dependable routing, and room
|
||||
to grow.
|
||||
</p>
|
||||
|
||||
<div className="hero-actions">
|
||||
<a href="#contact" className="button button-primary">
|
||||
Discuss a requirement
|
||||
</a>
|
||||
<a href="#services" className="button button-secondary">
|
||||
Explore services
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="signal-strip" aria-label="Novarix Networks positioning">
|
||||
{networkSignals.map((signal) => (
|
||||
<div key={signal.label} className="signal-item">
|
||||
<span>{signal.label}</span>
|
||||
<strong>{signal.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="statement-section section-border" aria-label="Company statement">
|
||||
<div className="container">
|
||||
<p>
|
||||
Novarix exists for the moments where connectivity is not a commodity purchase:
|
||||
new sites, awkward migrations, routed environments, managed edge,
|
||||
transit conversations, and the technical work that sits between them.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════
|
||||
SERVICES
|
||||
To add a service, append an entry to the `services` array
|
||||
at the top of this file.
|
||||
════════════════════════════════════════════════════════════ */}
|
||||
<section
|
||||
id="services"
|
||||
className="section section-border"
|
||||
aria-labelledby="services-heading"
|
||||
>
|
||||
<div className="container">
|
||||
<div className="section-heading">
|
||||
<h2 id="services-heading">Services</h2>
|
||||
<p>
|
||||
A focused offer for customers who need practical delivery now,
|
||||
with a network strategy that can deepen over time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="card-grid">
|
||||
{services.map((service) => (
|
||||
<article key={service.title} className="card">
|
||||
<span className="card-eyebrow">{service.eyebrow}</span>
|
||||
<h3>{service.title}</h3>
|
||||
<p>{service.description}</p>
|
||||
<ul className="card-points">
|
||||
{service.points.map((point) => (
|
||||
<li key={point}>{point}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="network"
|
||||
className="network-section section-border"
|
||||
aria-labelledby="network-heading"
|
||||
>
|
||||
<div className="container network-layout">
|
||||
<div className="section-heading">
|
||||
<h2 id="network-heading">Built for the current climate of networking.</h2>
|
||||
<p>
|
||||
The site should signal that Novarix is technical, independent,
|
||||
and pragmatic: capable of working across access, managed
|
||||
services, transit, and the messy edges where real networks live.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="network-stack" aria-label="Network operating principles">
|
||||
<div className="network-stack-item">
|
||||
<span>01</span>
|
||||
<strong>Direct technical conversation</strong>
|
||||
<p>Requirements are shaped with the people who understand routing, handoff, risk, and operational reality.</p>
|
||||
</div>
|
||||
<div className="network-stack-item">
|
||||
<span>02</span>
|
||||
<strong>Lean delivery model</strong>
|
||||
<p>Keep the path between problem, decision, and change short enough to stay useful.</p>
|
||||
</div>
|
||||
<div className="network-stack-item">
|
||||
<span>03</span>
|
||||
<strong>Expansion without theatre</strong>
|
||||
<p>Grow into peering, transit, and edge services when the demand and network maturity support it.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════
|
||||
PLATFORM DIRECTION
|
||||
Three-phase roadmap. Edit phases inline below.
|
||||
════════════════════════════════════════════════════════════ */}
|
||||
<section
|
||||
id="direction"
|
||||
className="section section-border"
|
||||
aria-labelledby="direction-heading"
|
||||
>
|
||||
<div className="container narrow">
|
||||
<div className="section-heading">
|
||||
<h2 id="direction-heading">Platform Direction</h2>
|
||||
<p>
|
||||
The public message stays credible today while leaving a clear
|
||||
route toward deeper interconnection and operator-grade services.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="direction-list">
|
||||
{platformPhases.map((item) => (
|
||||
<div className="direction-item" key={item.phase}>
|
||||
<span className="direction-phase">{item.phase}</span>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════
|
||||
CONTACT
|
||||
Update the mailto address and any supporting copy here.
|
||||
════════════════════════════════════════════════════════════ */}
|
||||
<section
|
||||
id="contact"
|
||||
className="section section-border"
|
||||
aria-labelledby="contact-heading"
|
||||
>
|
||||
<div className="container narrow">
|
||||
<div className="section-heading">
|
||||
<h2 id="contact-heading">Contact</h2>
|
||||
<p>
|
||||
For connectivity, managed networking, transit, or early project
|
||||
discussions, start with a direct email. Keep it technical if
|
||||
that is what the requirement needs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="contact-link"
|
||||
href="mailto:contact@novarixnet.com"
|
||||
aria-label="Send an email to contact@novarixnet.com"
|
||||
>
|
||||
contact@novarixnet.com
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════
|
||||
FOOTER
|
||||
════════════════════════════════════════════════════════════ */}
|
||||
<footer className="footer section-border">
|
||||
<div className="container footer-inner">
|
||||
<span>
|
||||
© {new Date().getFullYear()} Novarix Networks Limited
|
||||
</span>
|
||||
|
||||
{/*
|
||||
* Optional: add secondary footer links here, e.g.:
|
||||
* <nav aria-label="Footer navigation">
|
||||
* <a href="/privacy">Privacy</a>
|
||||
* </nav>
|
||||
*/}
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* robots.ts — Robots.txt generation for Novarix Networks
|
||||
*
|
||||
* Next.js App Router generates /robots.txt automatically from the object
|
||||
* returned by this default export.
|
||||
*
|
||||
* ─── Notes ───────────────────────────────────────────────────────────────
|
||||
*
|
||||
* • The `sitemap` field should match the canonical domain. If the domain
|
||||
* ever changes, update both this file and sitemap.ts.
|
||||
*
|
||||
* • To block specific paths (e.g. staging pages or admin routes), add
|
||||
* `disallow` entries to the rules array:
|
||||
*
|
||||
* rules: [
|
||||
* { userAgent: "*", allow: "/", disallow: ["/admin/", "/staging/"] },
|
||||
* ],
|
||||
*
|
||||
* • To block a specific crawler entirely:
|
||||
*
|
||||
* rules: [
|
||||
* { userAgent: "*", allow: "/" },
|
||||
* { userAgent: "GPTBot", disallow: "/" },
|
||||
* ],
|
||||
*
|
||||
* ─── Output ──────────────────────────────────────────────────────────────
|
||||
*
|
||||
* User-agent: *
|
||||
* Allow: /
|
||||
* Sitemap: https://novarixnet.com/sitemap.xml
|
||||
*/
|
||||
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
sitemap: "https://novarixnet.com/sitemap.xml",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* sitemap.ts — XML sitemap generation for Novarix Networks
|
||||
*
|
||||
* Next.js App Router generates /sitemap.xml automatically from the array
|
||||
* returned by this default export.
|
||||
*
|
||||
* ─── Adding pages ────────────────────────────────────────────────────────
|
||||
*
|
||||
* Append an entry for each publicly reachable page:
|
||||
*
|
||||
* {
|
||||
* url: "https://novarixnet.com/services",
|
||||
* lastModified: new Date("2025-06-01"),
|
||||
* changeFrequency: "monthly",
|
||||
* priority: 0.8,
|
||||
* },
|
||||
*
|
||||
* `lastModified` can be a Date object or an ISO 8601 string.
|
||||
* `priority` is a hint (0.0–1.0) for search engine crawl budgeting.
|
||||
*
|
||||
* ─── Dynamic routes ──────────────────────────────────────────────────────
|
||||
*
|
||||
* For blog posts or product pages generated from a CMS, fetch slugs inside
|
||||
* this function and map them to sitemap entries:
|
||||
*
|
||||
* const posts = await fetchPosts();
|
||||
* return posts.map((p) => ({
|
||||
* url: `https://novarixnet.com/blog/${p.slug}`,
|
||||
* lastModified: p.updatedAt,
|
||||
* changeFrequency: "weekly",
|
||||
* priority: 0.6,
|
||||
* }));
|
||||
*
|
||||
* ─── Output ──────────────────────────────────────────────────────────────
|
||||
*
|
||||
* <?xml version="1.0" encoding="UTF-8"?>
|
||||
* <urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
* <url>
|
||||
* <loc>https://novarixnet.com</loc>
|
||||
* <lastmod>…</lastmod>
|
||||
* <changefreq>monthly</changefreq>
|
||||
* <priority>1</priority>
|
||||
* </url>
|
||||
* </urlset>
|
||||
*/
|
||||
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: "https://novarixnet.com",
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user