Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to the NodeByte Hosting website will be documented in this f
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.5.4] - 2026-05-30

### Added
- **Google Ads Integration** — Google tag (`AW-16740819749`) installed via `next/script` with `strategy="afterInteractive"` in the root layout; fires after hydration without blocking render
- `packages/core/lib/gtag.ts` — central utility exporting `GOOGLE_ADS_ID`, `CONVERSION_IDS` map, and ready-to-call `conversions.*()` helpers for all 10 goals (begin checkout, subscribe, purchase, submit lead form, page view, sign-up, get directions, request quote, outbound click, contact)
- `packages/ui/components/google-ads-pageview.tsx` — client component that re-fires the page-view conversion on every App Router route change via `usePathname`

### Changed
- **`About`, `Features`, `Services` converted to Server Components** — `"use client"` directive removed from all three home page sections; none use client-only APIs, state, or effects, so they now render as HTML on the server, reducing the client JS bundle and improving TTI
- **Currency rates — localStorage TTL cache** — `CurrencyProvider` now checks `localStorage` for a cached rates response before fetching `/api/currency/rates`; cache TTL is 1 hour (matching the API's `s-maxage`), so repeat page loads within the hour skip the network request entirely

### Fixed
- **Canvas globe — per-frame string allocation** — the dot-grid draw loop in `hero-graphic.tsx` was creating a new `` `rgba(150,175,215,${a})` `` string for every dot on every 60 fps frame (~500+ allocations/frame); replaced with a precomputed 32-entry `DOT_ALPHA_TABLE` lookup built once at module load time

---

## [3.5.3] - 2026-04-17

### Added
Expand Down
17 changes: 17 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import type React from "react"
import type { Metadata } from "next"
import { cookies } from "next/headers"
import { Geist, Geist_Mono } from "next/font/google"
import Script from "next/script"
import { Analytics } from "@vercel/analytics/next"
import { NextIntlClientProvider } from "next-intl"
import { GoogleAdsPageView } from "@/packages/ui/components/google-ads-pageview"
import { GOOGLE_ADS_ID } from "@/packages/core/lib/gtag"
import { getMessages, getLocale } from "next-intl/server"
import "./globals.css"
import { Toaster } from "@/packages/ui/components/ui/toaster"
Expand Down Expand Up @@ -154,6 +157,20 @@ export default async function RootLayout({
</NextIntlClientProvider>
<Toaster />
<Analytics />
<GoogleAdsPageView />
{/* Google Ads tag */}
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GOOGLE_ADS_ID}`}
strategy="afterInteractive"
/>
<Script id="gtag-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GOOGLE_ADS_ID}');
`}
</Script>
</body>
</html>
)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@nodebyte/hosting-site",
"description": "The official website for NodeByte Hosting.",
"license": "AGPL-3.0-only",
"version": "3.3.0",
"version": "3.5.4",
"scripts": {
"build": "next build",
"dev": "next dev",
Expand Down
29 changes: 28 additions & 1 deletion packages/core/hooks/use-currency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,38 @@ export function CurrencyProvider({ children }: { children: ReactNode }) {
setCurrencyState(getDefaultCurrency())
}

// Fetch live exchange rates — falls back to static if unavailable
// Fetch live exchange rates — falls back to cached or static if unavailable
const RATES_CACHE_KEY = "nb_currency_rates"
const RATES_CACHE_TTL = 3600_000 // 1 hour in ms

try {
const cached = localStorage.getItem(RATES_CACHE_KEY)
if (cached) {
const { rates, ts } = JSON.parse(cached) as { rates: Record<string, number>; ts: number }
if (Date.now() - ts < RATES_CACHE_TTL) {
setLiveRates((prev) => {
const updated = { ...prev }
for (const [code, rate] of Object.entries(rates)) {
if (code in updated) updated[code as CurrencyCode] = rate
}
return updated
})
return // skip network fetch — cache is fresh
}
}
} catch {
// corrupted cache entry — ignore and fall through to fetch
}

fetch("/api/currency/rates")
.then((r) => r.json())
.then((data: { rates?: Record<string, number> }) => {
if (data.rates) {
try {
localStorage.setItem(RATES_CACHE_KEY, JSON.stringify({ rates: data.rates, ts: Date.now() }))
} catch {
// localStorage quota exceeded — ignore
}
setLiveRates((prev) => {
const updated = { ...prev }
for (const [code, rate] of Object.entries(data.rates!)) {
Expand Down
46 changes: 46 additions & 0 deletions packages/core/lib/gtag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export const GOOGLE_ADS_ID = "AW-16740819749"

/** Send-to labels for each conversion goal */
export const CONVERSION_IDS = {
beginCheckout: `${GOOGLE_ADS_ID}/qUthCNieibYcEKXG0q4-`,
subscribe: `${GOOGLE_ADS_ID}/2l-5CNueibYcEKXG0q4-`,
purchase: `${GOOGLE_ADS_ID}/eDjjCN6eibYcEKXG0q4-`,
submitLeadForm: `${GOOGLE_ADS_ID}/CY9iCOGeibYcEKXG0q4-`,
pageView: `${GOOGLE_ADS_ID}/li9bCOSeibYcEKXG0q4-`,
signUp: `${GOOGLE_ADS_ID}/ElPhCOeeibYcEKXG0q4-`,
getDirections: `${GOOGLE_ADS_ID}/E4xrCOqeibYcEKXG0q4-`,
requestQuote: `${GOOGLE_ADS_ID}/WWVMCO2eibYcEKXG0q4-`,
outboundClick: `${GOOGLE_ADS_ID}/Y6xZCPCeibYcEKXG0q4-`,
contact: `${GOOGLE_ADS_ID}/Z1z3CJGcoLYcEKXG0q4-`,
} as const

declare global {
interface Window {
gtag: (...args: unknown[]) => void
dataLayer: unknown[]
}
}

function fireConversion(
sendTo: string,
extra?: Record<string, unknown>,
): void {
if (typeof window === "undefined" || typeof window.gtag !== "function") return
window.gtag("event", "conversion", { send_to: sendTo, ...extra })
}

/** Ready-to-call helpers for each conversion goal */
export const conversions = {
beginCheckout: () => fireConversion(CONVERSION_IDS.beginCheckout),
subscribe: () => fireConversion(CONVERSION_IDS.subscribe),
/** @param transactionId Optional order / invoice ID */
purchase: (transactionId = "") =>
fireConversion(CONVERSION_IDS.purchase, { transaction_id: transactionId }),
submitLeadForm: () => fireConversion(CONVERSION_IDS.submitLeadForm),
pageView: () => fireConversion(CONVERSION_IDS.pageView),
signUp: () => fireConversion(CONVERSION_IDS.signUp),
getDirections: () => fireConversion(CONVERSION_IDS.getDirections),
requestQuote: () => fireConversion(CONVERSION_IDS.requestQuote),
outboundClick: () => fireConversion(CONVERSION_IDS.outboundClick),
contact: () => fireConversion(CONVERSION_IDS.contact),
}
2 changes: 0 additions & 2 deletions packages/ui/components/Layouts/Home/about.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client"

import type React from "react"
import { Card } from "@/packages/ui/components/ui/card"
import { Heart, Code, Gamepad2, Server, Sparkles, ArrowRight, Globe, Shield, Zap } from "lucide-react"
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/components/Layouts/Home/features.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client"

import React from "react"
import { Shield, Zap, Globe, Lock, Eye, Server, ArrowRight, CheckCircle2 } from "lucide-react"
import { Card } from "@/packages/ui/components/ui/card"
Expand Down
12 changes: 10 additions & 2 deletions packages/ui/components/Layouts/Home/hero-graphic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const CY = SIZE / 2
const cT = Math.cos(TILT)
const sT = Math.sin(TILT)

// Precomputed alpha lookup — avoids template-string allocation on every dot per frame.
// alpha = 0.025 + 0.135 * (z / R), z ∈ (0, R] → 32 discrete levels.
const DOT_ALPHA_LEVELS = 32
const DOT_ALPHA_TABLE: string[] = Array.from({ length: DOT_ALPHA_LEVELS }, (_, i) => {
const a = 0.025 + 0.135 * ((i + 1) / DOT_ALPHA_LEVELS)
return `rgba(150,175,215,${a.toFixed(3)})`
})

// ─── Locations ────────────────────────────────────────────────────────────────

type Region = "eu" | "am" | "ap"
Expand Down Expand Up @@ -159,8 +167,8 @@ export default function HeroGraphic() {
for (let lon = 0; lon < 360; lon += step) {
const p = proj(lat, lon, rot)
if (p.z <= 0) continue
const a = 0.025 + 0.135 * (p.z / R)
ctx.fillStyle = `rgba(150,175,215,${a})`
const idx = Math.min(DOT_ALPHA_LEVELS - 1, Math.floor((p.z / R) * DOT_ALPHA_LEVELS))
ctx.fillStyle = DOT_ALPHA_TABLE[idx]
ctx.fillRect(p.x - DOT_PX, p.y - DOT_PX, DOT_PX * 2, DOT_PX * 2)
}
}
Expand Down
2 changes: 0 additions & 2 deletions packages/ui/components/Layouts/Home/services.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client"

import { Button } from "@/packages/ui/components/ui/button"
import { Card } from "@/packages/ui/components/ui/card"
import { Layers, ArrowRight, Check } from "lucide-react"
Expand Down
19 changes: 19 additions & 0 deletions packages/ui/components/google-ads-pageview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client"

import { useEffect } from "react"
import { usePathname } from "next/navigation"
import { conversions } from "@/packages/core/lib/gtag"

/**
* Fires the Google Ads page-view conversion on every client-side route change.
* Must be rendered inside the root layout (client boundary).
*/
export function GoogleAdsPageView() {
const pathname = usePathname()

useEffect(() => {
conversions.pageView()
}, [pathname])

return null
}