diff --git a/app/(portfolio)/[username]/page.tsx b/app/(portfolio)/[username]/page.tsx index ed76c30..aa27c03 100644 --- a/app/(portfolio)/[username]/page.tsx +++ b/app/(portfolio)/[username]/page.tsx @@ -8,6 +8,8 @@ import type { PRByOrg } from "@/components/portfolio/prs-by-org-section" import { createAPIClient } from "@/lib/utils/api-client" import { verifyUsername } from "@/lib/utils/user" import { getGithubUsernameByCustomSlug } from "@/lib/utils/custom-url" +import { extractSubdomainFromHostname } from "@/lib/utils/domain" +import { headers } from "next/headers" interface PageProps { params: Promise<{ username: string }> @@ -95,8 +97,14 @@ export async function generateMetadata({ params }: PageProps): Promise export default async function PortfolioPage({ params, searchParams }: PageProps) { const { username: rawUsername } = await params + const hostname = (await headers()).get("host") || "" + const subdomain = extractSubdomainFromHostname(hostname) + if(!subdomain){ + return + } const { layout } = await searchParams - const username = await resolveUsername(rawUsername) + + const username = await resolveUsername(rawUsername || subdomain) if (!username) { notFound() diff --git a/app/page.tsx b/app/page.tsx index e8d644e..89673a9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -176,7 +176,9 @@ export default function LandingPage() { }) setIsLoading(true) - router.push(`/${username.trim()}`) + const targetUrl = `${window.location.protocol}//${username}.${window.location.host}/` + + window.location.href = targetUrl } return ( @@ -200,18 +202,18 @@ export default function LandingPage() { rel="noreferrer" className="hidden md:flex items-center justify-center gap-1.5 outline-none transition-colors border border-transparent text-white px-2.5 py-1.5 rounded-full bg-gray-900 hover:bg-gray-700 active:bg-gray-600 text-xs md:text-sm lg:px-4 lg:py-2.5 lg:text-base tracking-normal whitespace-nowrap cursor-pointer relative group overflow-visible" > - - GitHub - {displayedStars > 0 && ( - - - {displayedStars.toLocaleString()} - - )} - - - - + + GitHub + {displayedStars > 0 && ( + + + {displayedStars.toLocaleString()} + + )} + + + + - - {displayedStars > 0 && ( - - - {displayedStars > 999 ? `${(displayedStars / 1000).toFixed(1)}k` : displayedStars} - - )} - - - - + + {displayedStars > 0 && ( + + + {displayedStars > 999 ? `${(displayedStars / 1000).toFixed(1)}k` : displayedStars} + + )} + + + + @@ -253,19 +255,19 @@ export default function LandingPage() {
{/* Logo and Description */} -
+
+
-
-

- folio - x -

-
-

- Turn your GitHub into a stunning portfolio. Powered by AI, zero coding required. -

+

+ folio + x +

+

+ Turn your GitHub into a stunning portfolio. Powered by AI, zero coding required. +

+
{/* Form */}
diff --git a/components/portfolio/share-button.tsx b/components/portfolio/share-button.tsx index 9574a99..0b136ab 100644 --- a/components/portfolio/share-button.tsx +++ b/components/portfolio/share-button.tsx @@ -31,21 +31,39 @@ export function ShareButton({ username }: ShareButtonProps) { const [availabilityStatus, setAvailabilityStatus] = useState<"idle" | "available" | "taken" | "checking">("idle") const [registeredSlug, setRegisteredSlug] = useState(null) + function getRootHost() { + if (typeof window === "undefined") return ""; + + const hostParts = window.location.hostname.split("."); + + if (hostParts[hostParts.length - 1] === "localhost") { + return "localhost" + (window.location.port ? `:${window.location.port}` : ""); + } + + return hostParts.slice(-2).join(".") + (window.location.port ? `:${window.location.port}` : ""); + } + + useEffect(() => { if (typeof window !== 'undefined') { const baseUrl = window.location.origin const layout = searchParams.get("layout") const params = new URLSearchParams() - + if (layout) { params.set("layout", layout) } - + const queryString = params.toString() - const urlPath = registeredSlug ? `/${registeredSlug}` : `/${username}` - const fullUrl = queryString ? `${baseUrl}${urlPath}?${queryString}` : `${baseUrl}${urlPath}` - + const subdomain = registeredSlug ?? username + const protocol = window.location.protocol + + const rootDomain = process.env.NEXT_PUBLIC_SITE_URL?.replace(/^https?:\/\//, "") || "localhost:3000"; + const fullUrl = `${protocol}//${subdomain}.${rootDomain}${queryString ? `/?${queryString}` : "/"}`; + + setPortfolioUrl(fullUrl) + setCanShare(typeof navigator !== 'undefined' && 'share' in navigator) } }, [username, registeredSlug, searchParams]) @@ -190,7 +208,7 @@ export function ShareButton({ username }: ShareButtonProps) {
- {typeof window !== 'undefined' ? new URL(window.location.href).origin : ''}/ + {typeof window !== 'undefined' ? new URL(window.location.href).protocol : ''}// + + { + getRootHost() + }/ +
diff --git a/lib/utils/domain.ts b/lib/utils/domain.ts new file mode 100644 index 0000000..896336e --- /dev/null +++ b/lib/utils/domain.ts @@ -0,0 +1,56 @@ +import { Settings } from "../config/settings"; +import { type NextRequest, NextResponse } from 'next/server'; + +const URLMATCH_REGEX = /http:\/\/([^.]+)\.localhost/ + +export const protocol = Settings.NODE_ENV === "production" ? "https" : "http" +export const rootDomain = Settings.NEXT_PUBLIC_SITE_URL || "localhost:3000" + +export function extractSubdomainFromRequest(request: NextRequest): string | null { + const url = request.url; + const host = request.headers.get('host') || ''; + const hostname = host.split(':')[0]; + + if (url.includes('localhost') || url.includes('127.0.0.1')) { + const fullUrlMatch = url.match(URLMATCH_REGEX); + if (fullUrlMatch && fullUrlMatch[1]) { + return fullUrlMatch[1]; + } + + if (hostname.includes('.localhost')) { + return hostname.split('.')[0]; + } + + return null; + } + + const rootDomainFormatted = rootDomain.split(':')[0]; + + if (hostname.includes('---') && hostname.endsWith('.foliox.site')) { + const parts = hostname.split('---'); + return parts.length > 0 ? parts[0] : null; + } + + const isSubdomain = + hostname !== rootDomainFormatted && + hostname !== `www.${rootDomainFormatted}` && + hostname.endsWith(`.${rootDomainFormatted}`); + + return isSubdomain ? hostname.replace(`.${rootDomainFormatted}`, '') : null; +} + +export function extractSubdomainFromHostname(host: string): string { + if (!host) return null + + if (host.includes(".localhost")) { + const parts = host.split(".") + return parts[0] || null + } + + const parts = host.split(".") + if (parts.length > 2) { + return parts[0] + } + + return null +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb78822..44a9394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,7 +154,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -368,7 +367,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.1.tgz", "integrity": "sha512-N4kyRdA472WGLoCjsJpUeYdZZvpoBDgP65hUeQQxTQYwBTqD9O17Tokax9CdNbkb4g34sTfxaJCfcncE3Hy4SA==", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" @@ -398,14 +396,12 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@better-fetch/fetch": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", - "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==", - "peer": true + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" }, "node_modules/@chevrotain/cst-dts-gen": { "version": "10.5.0", @@ -444,8 +440,7 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.6", @@ -2889,7 +2884,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2900,7 +2894,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2951,7 +2944,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3491,7 +3483,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3880,7 +3871,6 @@ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.1.0.tgz", "integrity": "sha512-7CecYG+yN8J1uBJni/Mpjryp8bW/YySYsrGEWgFe048ORASjq17keGjbKI2kHEOSc6u8pi11UxzkJ7jIovQw6w==", "license": "MIT", - "peer": true, "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", @@ -3932,7 +3922,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4677,7 +4666,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4863,7 +4851,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5670,7 +5657,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.10.tgz", "integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -6215,7 +6201,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.2.tgz", "integrity": "sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -6324,7 +6309,6 @@ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.8.tgz", "integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" } @@ -6859,7 +6843,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -7262,7 +7245,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -7493,7 +7475,6 @@ "integrity": "sha512-VZObZ1pQV/OScarYg68RYUx61GpFLH2mJGf9fUX4XxQxTst/6ZK7nkY86CSZ3zBW6U9lKRTsBrZWVz20X5G/KQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "7.0.0", "@prisma/dev": "0.13.0", @@ -7612,7 +7593,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7635,7 +7615,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8533,7 +8512,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8718,7 +8696,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9073,7 +9050,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/proxy.ts b/proxy.ts index 188622f..48b6a43 100644 --- a/proxy.ts +++ b/proxy.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { Settings } from '@/lib/config/settings'; +import { extractSubdomainFromRequest } from './lib/utils/domain'; const ALLOWED_ORIGINS = [ 'http://localhost:3000', @@ -55,10 +56,19 @@ export function proxy(request: NextRequest) { return response; } + // Subdomain routing logic -> + const subdomain = extractSubdomainFromRequest(request) + if (subdomain && pathname === '/') { + const url = request.nextUrl.clone(); + url.pathname = `/${subdomain}`; + return NextResponse.rewrite(url); + } + + return NextResponse.next(); } export const config = { - matcher: ['/api/:path*'], + matcher: ['/((?!api|_next|[\\w-]+\\.\\w+).*)'], };