Skip to content

Commit 32d190c

Browse files
committed
feat: implement canonical URL handling and middleware for production deployment
1 parent ee2f2d1 commit 32d190c

File tree

6 files changed

+101
-12
lines changed

6 files changed

+101
-12
lines changed

app/new/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import type { Metadata } from "next"
22

33
import { StackWizardShell } from "@/components/stack-wizard-shell"
44
import { StackWizardClient } from "@/app/new/stack/stack-wizard-client"
5+
import { absoluteUrl } from "@/lib/site-metadata"
56

67
const title = "Launch the DevContext Wizard"
78
const description =
89
"Start a guided flow to assemble AI-ready coding instruction files. Pick your stack, customize conventions, and export Copilot, Cursor, or agents guidelines in minutes."
10+
const canonicalUrl = absoluteUrl("/new")
911

1012
export const metadata: Metadata = {
1113
title,
1214
description,
1315
alternates: {
14-
canonical: "/new",
16+
canonical: canonicalUrl,
1517
},
1618
openGraph: {
1719
title,
1820
description,
19-
url: "/new",
21+
url: canonicalUrl,
2022
type: "website",
2123
siteName: "DevContext",
2224
images: [

app/new/stack/[[...stackSegments]]/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import stacksData from "@/data/stacks.json"
55
import type { DataQuestionSource, WizardStep } from "@/types/wizard"
66
import { StackWizardShell } from "@/components/stack-wizard-shell"
77
import { loadStackWizardStep } from "@/lib/wizard-config"
8+
import { absoluteUrl } from "@/lib/site-metadata"
89
import { StackWizardClient } from "../stack-wizard-client"
910
import { StackSummaryPage } from "../stack-summary-page"
1011

@@ -58,19 +59,20 @@ export async function generateMetadata({ params }: MetadataProps): Promise<Metad
5859
}
5960

6061
const canonicalPath = `/new/stack${segments.length > 0 ? `/${segments.join("/")}` : ""}`
62+
const canonicalUrl = absoluteUrl(canonicalPath)
6163
const ogImage = "/og-image.png"
6264
const imageAlt = stackLabel ? `${stackLabel} DevContext wizard preview` : "DevContext wizard interface preview"
6365

6466
return {
6567
title,
6668
description,
6769
alternates: {
68-
canonical: canonicalPath,
70+
canonical: canonicalUrl,
6971
},
7072
openGraph: {
7173
title,
7274
description,
73-
url: canonicalPath,
75+
url: canonicalUrl,
7476
type: "website",
7577
siteName: "DevContext",
7678
images: [

app/page.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,51 @@
11
import type { Metadata } from "next"
22
import Link from "next/link"
33

4+
import { Github } from "lucide-react"
5+
46
import { AnimatedBackground } from "@/components/AnimatedBackground"
57
import { Hero } from "@/components/Hero"
68
import { Button } from "@/components/ui/button"
7-
import { Github } from "lucide-react"
89
import { absoluteUrl } from "@/lib/site-metadata"
910

11+
const title = "DevContext | Repo-aware AI Coding Guidelines Assistant"
12+
const description =
13+
"Generate AI-ready Copilot instructions, Cursor rules, and developer onboarding docs with a GitHub-aware coding guidelines workflow."
14+
1015
export const metadata: Metadata = {
16+
title,
17+
description,
18+
keywords: [
19+
"AI coding guidelines",
20+
"Copilot instructions generator",
21+
"Cursor rules",
22+
"GitHub repo analyzer",
23+
"developer onboarding docs",
24+
],
1125
alternates: {
1226
canonical: absoluteUrl("/"),
1327
},
28+
openGraph: {
29+
title,
30+
description,
31+
url: absoluteUrl("/"),
32+
type: "website",
33+
siteName: "DevContext",
34+
images: [
35+
{
36+
url: absoluteUrl("/og-image.png"),
37+
width: 1200,
38+
height: 630,
39+
alt: "DevContext AI coding guidelines assistant preview",
40+
},
41+
],
42+
},
43+
twitter: {
44+
card: "summary_large_image",
45+
title,
46+
description,
47+
images: [absoluteUrl("/og-image.png")],
48+
},
1449
}
1550

1651
export default function LandingPage() {

components/Hero.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,21 @@ export function Hero() {
8484
variants={itemVariants}
8585
>
8686
<span className="inline-flex h-1.5 w-1.5 rounded-full bg-primary" />
87-
Less setup. Sharper AI outputs.
87+
Guided setup for AI coding guidelines.
8888
</motion.span>
8989

9090
<motion.h1
9191
className="mx-auto max-w-4xl text-4xl font-semibold tracking-tight text-foreground md:text-6xl md:leading-tight"
9292
variants={itemVariants}
9393
>
94-
Build concise AI coding guardrails your team will actually use
94+
Repo-aware AI coding guidelines assistant for Copilot &amp; Cursor
9595
</motion.h1>
9696

9797
<motion.p
9898
className="mx-auto max-w-2xl text-sm text-muted-foreground md:text-lg"
9999
variants={itemVariants}
100100
>
101-
Jump in with a ready-made stack template, explore the full wizard, or drop a GitHub repo for an automatic scan.
101+
Generate AI-ready Copilot instructions, Cursor rules, and developer onboarding docs in minutes—start from curated stacks or drop a repo into the GitHub analyzer.
102102
</motion.p>
103103

104104

@@ -110,7 +110,7 @@ export function Hero() {
110110
Start fast with popular stacks
111111
</p>
112112
<p className="text-sm text-muted-foreground">
113-
Pick a curated quickstart and we&apos;ll pre-fill the wizard with stack defaults.
113+
Pick a curated quickstart and we&apos;ll pre-fill the wizard with stack defaults and suggested AI guardrails.
114114
</p>
115115
<div className="flex flex-wrap gap-3">
116116
{popularStacks.map((stack) => {
@@ -190,7 +190,7 @@ export function Hero() {
190190
Scan a GitHub repository
191191
</p>
192192
<p className="text-sm text-muted-foreground">
193-
Paste an owner/repo or URL and we&apos;ll prefill the wizard with detected tech and tooling.
193+
Paste an owner/repo or URL and we&apos;ll prefill the wizard with detected tech, tooling, and guardrail suggestions.
194194
</p>
195195
</div>
196196
<div className="flex w-full flex-col gap-2 sm:flex-row">

lib/site-metadata.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@ const normalizeSiteUrl = (input: string): string => {
66
return DEFAULT_SITE_URL;
77
}
88

9-
const withoutTrailingSlash = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
10-
return withoutTrailingSlash || DEFAULT_SITE_URL;
9+
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
10+
11+
try {
12+
const url = new URL(withProtocol);
13+
return url.origin;
14+
} catch {
15+
return DEFAULT_SITE_URL;
16+
}
1117
};
1218

1319
export const SITE_URL = normalizeSiteUrl(
1420
process.env.NEXT_PUBLIC_SITE_URL ?? process.env.SITE_URL ?? DEFAULT_SITE_URL,
1521
);
1622

23+
export const CANONICAL_HOST = new URL(SITE_URL).hostname;
24+
1725
export const absoluteUrl = (path = ""): string => {
1826
if (!path || path === "/") {
1927
return SITE_URL;

middleware.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { NextRequest } from "next/server";
2+
import { NextResponse } from "next/server";
3+
4+
import { CANONICAL_HOST } from "@/lib/site-metadata";
5+
6+
const LOCALHOST_ALLOWLIST = new Set(["localhost", "127.0.0.1", "[::1]"]);
7+
8+
const isProductionDeployment = (): boolean => {
9+
const vercelEnv = process.env.VERCEL_ENV;
10+
if (vercelEnv) {
11+
return vercelEnv === "production";
12+
}
13+
14+
return process.env.NODE_ENV === "production";
15+
};
16+
17+
export function middleware(request: NextRequest) {
18+
if (!isProductionDeployment()) {
19+
return NextResponse.next();
20+
}
21+
22+
const canonicalHost = CANONICAL_HOST.toLowerCase();
23+
const requestHostname = request.nextUrl.hostname.toLowerCase();
24+
25+
if (requestHostname === canonicalHost || LOCALHOST_ALLOWLIST.has(requestHostname)) {
26+
return NextResponse.next();
27+
}
28+
29+
const redirectUrl = request.nextUrl.clone();
30+
redirectUrl.hostname = canonicalHost;
31+
redirectUrl.protocol = "https";
32+
redirectUrl.port = "";
33+
34+
return NextResponse.redirect(redirectUrl, 308);
35+
}
36+
37+
export const config = {
38+
matcher: [
39+
"/((?!_next/static|_next/image|favicon.ico|robots.txt|site.webmanifest|sitemap.xml|manifest.json|og-image.png).*)",
40+
],
41+
};
42+

0 commit comments

Comments
 (0)