diff --git a/client/App.tsx b/client/App.tsx
index a233c209..44386c20 100644
--- a/client/App.tsx
+++ b/client/App.tsx
@@ -1,9 +1,10 @@
import "./global.css";
+import { useEffect } from "react";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { BrowserRouter, Routes, Route, useNavigate } from "react-router-dom";
import { useDiscordActivity } from "./contexts/DiscordActivityContext";
import { AuthProvider } from "./contexts/AuthContext";
import { Web3Provider } from "./contexts/Web3Context";
@@ -161,14 +162,18 @@ import DeveloperPlatform from "./pages/dev-platform/DeveloperPlatform";
const queryClient = new QueryClient();
-// When the app is accessed via staff.aethex.tech, auto-redirect to /staff
-// so the subdomain works as a proper alias for the staff section.
+// Detects staff.aethex.tech and navigates to /staff inside the SPA.
+// Must be inside BrowserRouter so useNavigate works.
const StaffSubdomainRedirect = ({ children }: { children: React.ReactNode }) => {
- const hostname = typeof window !== "undefined" ? window.location.hostname : "";
- if (hostname === "staff.aethex.tech" && !window.location.pathname.startsWith("/staff")) {
- window.location.replace("/staff" + window.location.pathname + window.location.search);
- return null;
- }
+ const navigate = useNavigate();
+ useEffect(() => {
+ if (
+ window.location.hostname === "staff.aethex.tech" &&
+ !window.location.pathname.startsWith("/staff")
+ ) {
+ navigate("/staff", { replace: true });
+ }
+ }, [navigate]);
return <>{children}>;
};
diff --git a/index.html b/index.html
index 50d62c1c..8bb827e9 100644
--- a/index.html
+++ b/index.html
@@ -77,6 +77,7 @@
+
{
- // Don't serve index.html for API routes
+// ── SSR Meta Injection ────────────────────────────────────────────────────────
+
+const BASE_URL = "https://aethex.dev";
+const DEFAULT_OG_IMAGE =
+ "https://docs.aethex.tech/~gitbook/image?url=https%3A%2F%2F1143808467-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Forganizations%252FDhUg3jal6kdpG645FzIl%252Fsites%252Fsite_HeOmR%252Flogo%252FqxDYz8Oj2SnwUTa8t3UB%252FAeThex%2520Origin%2520logo.png%3Falt%3Dmedia%26token%3D200e8ea2-0129-4cbe-b516-4a53f60c512b&width=1200&dpr=1&quality=100&sign=6c7576ce&sv=2";
+
+type RouteMeta = {
+ title: string;
+ description: string;
+ image?: string;
+ type?: string;
+};
+
+/** Static route overrides — matched in order, first match wins. */
+const STATIC_META: Array<{ pattern: RegExp; meta: RouteMeta }> = [
+ {
+ pattern: /^\/$/,
+ meta: {
+ title: "AeThex | Developer Platform for Builders, Creators & Innovation",
+ description:
+ "AeThex: an advanced development platform and community for builders. Collaborate on projects, learn, and ship innovation.",
+ },
+ },
+ {
+ pattern: /^\/projects\/?$/,
+ meta: {
+ title: "AeThex | Projects",
+ description:
+ "Explore open-source and community projects built on the AeThex platform.",
+ },
+ },
+ {
+ pattern: /^\/projects\/new/,
+ meta: {
+ title: "AeThex | New Project",
+ description: "Start a new project on AeThex.",
+ },
+ },
+ {
+ pattern: /^\/gameforge/,
+ meta: {
+ title: "AeThex | GameForge Studio",
+ description:
+ "GameForge — AeThex's game development studio for indie game creators. Build, manage, and ship games.",
+ },
+ },
+ {
+ pattern: /^\/ethos/,
+ meta: {
+ title: "AeThex | Ethos Guild",
+ description:
+ "Ethos Guild — collaborative music creation, licensing, and creative work on AeThex.",
+ },
+ },
+ {
+ pattern: /^\/dev-platform/,
+ meta: {
+ title: "AeThex | Developer Platform",
+ description:
+ "Build with AeThex APIs. Documentation, SDKs, examples, and developer tools for modern builders.",
+ },
+ },
+ {
+ pattern: /^\/feed/,
+ meta: {
+ title: "AeThex | Community Feed",
+ description: "What's happening in the AeThex builder community.",
+ },
+ },
+ {
+ pattern: /^\/login/,
+ meta: {
+ title: "AeThex | Sign In",
+ description:
+ "Sign in to your AeThex account to access projects, community, and more.",
+ },
+ },
+ {
+ pattern: /^\/register/,
+ meta: {
+ title: "AeThex | Create Account",
+ description:
+ "Join AeThex and start building, learning, and collaborating with the community.",
+ },
+ },
+ {
+ pattern: /^\/dashboard/,
+ meta: {
+ title: "AeThex | Dashboard",
+ description:
+ "Your personal AeThex dashboard — manage projects, track progress, and connect.",
+ },
+ },
+ {
+ pattern: /^\/passport\/me$/,
+ meta: {
+ title: "AeThex | My Passport",
+ description: "View your AeThex developer passport and profile.",
+ },
+ },
+ {
+ pattern: /^\/docs/,
+ meta: {
+ title: "AeThex | Documentation",
+ description: "Guides, API references, and developer resources for AeThex.",
+ },
+ },
+ {
+ pattern: /^\/pricing/,
+ meta: {
+ title: "AeThex | Pricing",
+ description:
+ "Simple, transparent pricing for AeThex — individuals, teams, and enterprises.",
+ },
+ },
+ {
+ pattern: /^\/about/,
+ meta: {
+ title: "AeThex | About",
+ description:
+ "Learn about AeThex — our mission, team, and the technology we build.",
+ },
+ },
+ {
+ pattern: /^\/blog/,
+ meta: {
+ title: "AeThex | Blog",
+ description:
+ "News, tutorials, and announcements from the AeThex team and community.",
+ },
+ },
+];
+
+// UUID pattern
+const PROJECT_UUID_RE =
+ /^\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
+// Passport: /passport/ (not "me", handled above)
+const PASSPORT_USER_RE = /^\/passport\/([^/]+)$/;
+
+function escHtml(s: string): string {
+ return s
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(//g, ">");
+}
+
+/**
+ * Replaces meta tag content values in the HTML template.
+ * Handles both inline and multi-line attribute formats (Vite preserves them).
+ */
+function injectMeta(
+ html: string,
+ meta: RouteMeta & { url: string }
+): string {
+ const {
+ title,
+ description,
+ url,
+ image = DEFAULT_OG_IMAGE,
+ type = "website",
+ } = meta;
+
+ const t = escHtml(title);
+ const d = escHtml(description);
+ const u = escHtml(url);
+ const img = escHtml(image);
+
+ return (
+ html
+ //
+ .replace(/[^<]*<\/title>/, `${t}`)
+ // meta name="description"
+ .replace(
+ /( {
+ const url = `${BASE_URL}${pathname}`;
+
+ // Dynamic: project detail page
+ const projectMatch = PROJECT_UUID_RE.exec(pathname);
+ if (projectMatch && adminSupabase) {
+ const projectId = projectMatch[1];
+ try {
+ const { data: project } = await (adminSupabase as any)
+ .from("projects")
+ .select("title, description, image_url, status")
+ .eq("id", projectId)
+ .maybeSingle();
+ if (project) {
+ const desc = project.description
+ ? String(project.description).slice(0, 160)
+ : `View the ${project.title} project on AeThex.`;
+ return {
+ title: `${project.title} — AeThex Project`,
+ description: desc,
+ image: project.image_url || DEFAULT_OG_IMAGE,
+ type: "article",
+ url,
+ };
+ }
+ } catch {
+ // fall through to default
+ }
+ }
+
+ // Dynamic: public passport / profile page
+ const passportMatch = PASSPORT_USER_RE.exec(pathname);
+ if (passportMatch && adminSupabase) {
+ const username = passportMatch[1];
+ try {
+ const { data: profile } = await adminSupabase
+ .from("user_profiles")
+ .select("username, full_name, avatar_url, bio")
+ .eq("username", username)
+ .maybeSingle();
+ if (profile) {
+ const displayName =
+ (profile as any).full_name || profile.username || username;
+ const bio = (profile as any).bio;
+ const desc = bio
+ ? String(bio).slice(0, 160)
+ : `View ${displayName}'s developer passport and projects on AeThex.`;
+ return {
+ title: `${displayName} — AeThex Passport`,
+ description: desc,
+ url,
+ };
+ }
+ } catch {
+ // fall through
+ }
+ }
+
+ // Static route map
+ for (const { pattern, meta } of STATIC_META) {
+ if (pattern.test(pathname)) {
+ return { ...meta, url };
+ }
+ }
+
+ // Default fallback
+ return {
+ title: "AeThex | Developer Platform for Builders, Creators & Innovation",
+ description:
+ "AeThex: an advanced development platform and community for builders. Collaborate on projects, learn, and ship innovation.",
+ url,
+ };
+}
+
+// Cache the HTML template (reset on read error so a rebuild is picked up on restart)
+let htmlTemplate: string | null = null;
+
+function getTemplate(): string {
+ if (!htmlTemplate) {
+ htmlTemplate = readFileSync(path.join(distPath, "index.html"), "utf-8");
+ }
+ return htmlTemplate;
+}
+
+// ── Route Handler ─────────────────────────────────────────────────────────────
+
+app.get("*", async (req, res) => {
if (req.path.startsWith("/api/") || req.path.startsWith("/health")) {
return res.status(404).json({ error: "API endpoint not found" });
}
- res.sendFile(path.join(distPath, "index.html"));
+ try {
+ const template = getTemplate();
+ const meta = await resolveRouteMeta(req.path);
+ const html = injectMeta(template, meta);
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
+ // Crawlers should not cache — browsers can
+ res.setHeader(
+ "Cache-Control",
+ "public, max-age=60, stale-while-revalidate=300"
+ );
+ res.send(html);
+ } catch (err) {
+ console.error("[SSR Meta] Error:", err);
+ // Fallback: send unmodified file
+ res.sendFile(path.join(distPath, "index.html"));
+ }
});
+// ── Server ────────────────────────────────────────────────────────────────────
+
app.listen(Number(port), host, () => {
console.log(`🚀 AeThex server running on ${host}:${port}`);
console.log(`📱 Frontend: http://${host}:${port}`);
console.log(`🔧 API: http://${host}:${port}/api`);
});
-// Graceful shutdown
process.on("SIGTERM", () => {
console.log("🛑 Received SIGTERM, shutting down gracefully");
process.exit(0);