Skip to content

Commit 1f63d11

Browse files
committed
fix: add canonical URLs and correct site host metadata
1 parent cc894ad commit 1f63d11

8 files changed

Lines changed: 254 additions & 178 deletions

File tree

app/blog/page.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
import type { Metadata } from 'next'
12
import Link from 'next/link'
23
import { PostCard } from 'nextra-theme-blog'
34
import { getPosts, getTags } from './get-posts'
4-
import type { BlogMetadata, TagCount, PostCardData } from '../../types/blog'
5+
import type { TagCount, PostCardData } from '../../types/blog'
56

6-
export const metadata: BlogMetadata = {
7-
title: 'Blog'
7+
const BLOG_TITLE = 'Blog'
8+
9+
export const metadata: Metadata = {
10+
title: BLOG_TITLE,
11+
description: 'Articles about Happy, Claude Code workflows, distribution, and practical engineering.',
12+
alternates: {
13+
canonical: '/blog/',
14+
},
15+
openGraph: {
16+
url: '/blog/',
17+
},
818
}
919

1020
export default async function BlogIndexPage() {
@@ -18,7 +28,7 @@ export default async function BlogIndexPage() {
1828

1929
return (
2030
<div data-pagefind-ignore="all">
21-
<h1>{metadata.title}</h1>
31+
<h1>{BLOG_TITLE}</h1>
2232
{Object.keys(allTags).length > 0 && (
2333
<div
2434
className="not-prose"

app/docs/[[...mdxPath]]/page.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@ export async function generateMetadata(props: {
99
}): Promise<Metadata> {
1010
const params = await props.params
1111
const { metadata } = await importPage(params.mdxPath)
12-
return metadata
12+
const canonicalPath = params.mdxPath.length > 0
13+
? `/docs/${params.mdxPath.join('/')}/`
14+
: '/docs/'
15+
16+
return {
17+
...metadata,
18+
alternates: {
19+
...metadata?.alternates,
20+
canonical: canonicalPath,
21+
},
22+
openGraph: {
23+
...metadata?.openGraph,
24+
url: canonicalPath,
25+
},
26+
}
1327
}
1428

1529
const Wrapper = getMDXComponents().wrapper
@@ -25,4 +39,4 @@ export default async function Page(props: {
2539
<MDXContent {...props} params={params} />
2640
</Wrapper>
2741
);
28-
}
42+
}

app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const ibmPlexMono = IBM_Plex_Mono({
1414
})
1515

1616
export const metadata: Metadata = {
17-
metadataBase: new URL('https://slopus.github.io'),
17+
metadataBase: new URL('https://happy.engineering'),
1818
title: 'Happy - Claude Code Mobile Client',
1919
description: 'Free, open-source mobile app for Claude Code. Control Claude AI from your phone with end-to-end encryption and seamless workflow. Get started with npm install -g happy-coder.',
2020
keywords: ['Claude', 'AI', 'mobile', 'coding', 'voice-to-code', 'open source', 'npm', 'development', 'programming'],
@@ -25,7 +25,7 @@ export const metadata: Metadata = {
2525
openGraph: {
2626
title: 'Happy - Claude Code Mobile Client',
2727
description: 'Free, open-source mobile app for Claude Code. Control Claude AI from your phone with end-to-end encryption and seamless workflow.',
28-
url: 'https://slopus.github.io',
28+
url: 'https://happy.engineering/',
2929
siteName: 'Happy',
3030
images: [
3131
{

app/page.tsx

Lines changed: 12 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,15 @@
1-
'use client'
2-
3-
import AppStoreButton from '@/components/AppStoreButton'
4-
import GooglePlayButton from '@/components/GooglePlayButton'
5-
import { useState, useEffect } from 'react'
6-
import { TextBasedHowItWorks, YoutubeDemoSection } from '@/components/marketing'
7-
import TextBasedFeatures from '@/components/marketing/TextBasedFeatures'
8-
import AdaptiveTerminal from '@/components/AdaptiveTerminal'
9-
import LaunchWebAppButton from '@/components/WebAppButton'
10-
import StarOnGithubButton from '@/components/GithubButton'
11-
import VideoComposite from '@/components/VideoComposite'
12-
import Testimonials from '@/components/Testimonials'
13-
14-
const GOOGLE_PLAY_LINK = 'https://play.google.com/store/apps/details?id=com.ex3ndr.happy'
15-
const APP_STORE_LINK = 'https://apps.apple.com/us/app/happy-claude-code-client/id6748571505'
16-
17-
18-
function DesktopHeroSection() {
19-
// Device detection state
20-
const [deviceType, setDeviceType] = useState<'ios' | 'android' | 'desktop'>('desktop')
21-
22-
useEffect(() => {
23-
// Client-side device detection
24-
const userAgent = navigator.userAgent.toLowerCase()
25-
26-
if (/iphone|ipad|ipod/.test(userAgent)) {
27-
setDeviceType('ios')
28-
} else if (/android/.test(userAgent)) {
29-
setDeviceType('android')
30-
} else {
31-
setDeviceType('desktop')
32-
}
33-
}, [])
34-
35-
// Render store buttons based on device type
36-
const renderStoreButtons = () => {
37-
switch (deviceType) {
38-
case 'ios':
39-
return (
40-
<>
41-
<AppStoreButton href={APP_STORE_LINK} />
42-
</>
43-
)
44-
case 'android':
45-
return (
46-
<>
47-
<GooglePlayButton href={GOOGLE_PLAY_LINK} />
48-
</>
49-
)
50-
case 'desktop':
51-
default:
52-
return (
53-
<>
54-
<GooglePlayButton href={GOOGLE_PLAY_LINK} />
55-
<AppStoreButton href={APP_STORE_LINK} />
56-
</>
57-
)
58-
}
59-
}
60-
61-
return (
62-
<section className="py-8 pb-24 md:py-24 xl:py-32 hidden sm:block font-mono">
63-
<div className="max-w-[72ch] mx-auto px-5">
64-
<div className="">
65-
<div>
66-
<h1 className="text-3xl sm:text-3xl font-bold mb-5 leading-tight">
67-
Claude Code Anywhere
68-
</h1>
69-
70-
<YoutubeDemoSection
71-
youtubeId="GCS0OG9QMSE"
72-
posterImage="https://img.youtube.com/vi/GCS0OG9QMSE/maxresdefault.jpg"
73-
/>
74-
75-
<div className="mb-6 text-base text-gray-700 dark:text-gray-300">
76-
<div className="flex items-start mb-4 ">
77-
<span>Works seamlessly with your existing tools and workflow</span>
78-
</div>
79-
<div className="flex items-start mb-4">
80-
<span>Open source (MIT licensed)</span>
81-
</div>
82-
<div className="flex items-start mb-4">
83-
<span>Secure with end-to-end encryption</span>
84-
</div>
85-
<div className="flex items-start mb-4">
86-
<span>Multiple active sessions across multiple machines</span>
87-
</div>
88-
<AdaptiveTerminal command="npm i -g happy-coder && happy" />
89-
</div>
90-
91-
<div className="grid grid-cols-2 gap-4 sm:flex sm:gap-4 sm:flex-wrap">
92-
<GooglePlayButton href={GOOGLE_PLAY_LINK} />
93-
<AppStoreButton href={APP_STORE_LINK} />
94-
<LaunchWebAppButton href="https://app.happy.engineering" />
95-
<StarOnGithubButton href="https://github.com/slopus/happy" />
96-
</div>
97-
98-
</div>
99-
</div>
100-
</div>
101-
</section>
102-
)
1+
import type { Metadata } from 'next'
2+
import HomePage from '@/components/marketing/HomePage'
3+
4+
export const metadata: Metadata = {
5+
alternates: {
6+
canonical: '/',
7+
},
8+
openGraph: {
9+
url: '/',
10+
},
10311
}
10412

10513
export default function Home() {
106-
107-
108-
return (
109-
<>
110-
<div className="max-w-[74ch] mx-auto px-2.5 sm:px-[2ch] pt-8">
111-
<h1 className="text-3xl sm:text-3xl font-bold leading-tight mb-2">
112-
Claude Code Anywhere
113-
</h1>
114-
<p className="text-base text-gray-700 dark:text-gray-300 mb-2">
115-
Spawn and control multiple Claude Codes in parallel. Happy Coder runs on
116-
your hardware, works from your phone and desktop, and costs nothing.
117-
Open source.
118-
</p>
119-
</div>
120-
121-
<VideoComposite />
122-
123-
<section className="max-w-[72ch] mx-auto px-2.5 md:px-0">
124-
<div className="mb-6">
125-
<AdaptiveTerminal command="npm i -g happy-coder && happy" />
126-
</div>
127-
128-
<div className="grid grid-cols-2 gap-4 sm:flex sm:gap-4 sm:flex-wrap">
129-
<GooglePlayButton href={GOOGLE_PLAY_LINK} />
130-
<AppStoreButton href={APP_STORE_LINK} />
131-
<LaunchWebAppButton href="https://app.happy.engineering" />
132-
<StarOnGithubButton href="https://github.com/slopus/happy" />
133-
</div>
134-
<div className="mt-6 text-base text-gray-700 dark:text-gray-300 font-mono">
135-
<div className="flex items-start mb-4 ">
136-
<span>Hands-free control with voice agent—not just dictation</span>
137-
</div>
138-
<div className="flex items-start mb-4">
139-
<span>Multiple active sessions across multiple machines</span>
140-
</div>
141-
<div className="flex items-start mb-4">
142-
<span>Works seamlessly with your existing tools and workflow</span>
143-
</div>
144-
<div className="flex items-start mb-4">
145-
<span>Secure with end-to-end encryption</span>
146-
</div>
147-
<div className="flex items-start mb-10">
148-
<span>Open source (MIT licensed)</span>
149-
</div>
150-
</div>
151-
</section>
152-
153-
154-
<section className="max-w-[72ch] mx-auto py-12 px-2.5 md:px-0">
155-
<TextBasedFeatures />
156-
</section>
157-
<section className="max-w-5xl mx-auto">
158-
<Testimonials layout="masonry" />
159-
</section>
160-
<section className="max-w-[72ch] mx-auto py-12 px-2.5 md:px-0">
161-
<TextBasedHowItWorks />
162-
</section>
163-
{/*
164-
<YoutubeDemoSection
165-
youtubeId="GCS0OG9QMSE"
166-
posterImage="https://img.youtube.com/vi/GCS0OG9QMSE/maxresdefault.jpg"
167-
/>
168-
*/}
169-
</>
170-
)
171-
}
14+
return <HomePage />
15+
}

app/privacy/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Metadata } from 'next'
12
import { Bricolage_Grotesque } from 'next/font/google'
23
import Link from 'next/link'
34

@@ -6,6 +7,17 @@ const bricolageGrotesque = Bricolage_Grotesque({
67
subsets: ['latin'],
78
})
89

10+
export const metadata: Metadata = {
11+
title: 'Privacy Policy',
12+
description: 'Privacy policy for Happy.',
13+
alternates: {
14+
canonical: '/privacy/',
15+
},
16+
openGraph: {
17+
url: '/privacy/',
18+
},
19+
}
20+
921
export default function PrivacyPolicy() {
1022
return (
1123
<main className="min-h-screen bg-white text-black p-8">
@@ -140,4 +152,4 @@ export default function PrivacyPolicy() {
140152
</div>
141153
</main>
142154
)
143-
}
155+
}

app/terms/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Metadata } from 'next'
12
import { Bricolage_Grotesque } from 'next/font/google'
23
import Link from 'next/link'
34

@@ -6,6 +7,17 @@ const bricolageGrotesque = Bricolage_Grotesque({
67
subsets: ['latin'],
78
})
89

10+
export const metadata: Metadata = {
11+
title: 'Terms of Use',
12+
description: 'Terms of use for Happy.',
13+
alternates: {
14+
canonical: '/terms/',
15+
},
16+
openGraph: {
17+
url: '/terms/',
18+
},
19+
}
20+
921
export default function TermsOfUse() {
1022
return (
1123
<main className="min-h-screen bg-white text-black p-8">
@@ -183,4 +195,4 @@ export default function TermsOfUse() {
183195
</div>
184196
</main>
185197
)
186-
}
198+
}

app/tools/[[...kind]]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,19 @@ export async function generateMetadata(props: {
3838
}): Promise<Metadata> {
3939
const params = await props.params;
4040
const kind = params.kind?.[0];
41+
const canonicalPath = kind ? `/tools/${kind}/` : '/tools/';
4142

4243
if (!kind) {
4344
return {
4445
title: "Claude Code Tools & Resources",
4546
description:
4647
"Discover tools, agents, MCP servers, and resources for Claude Code",
48+
alternates: {
49+
canonical: canonicalPath,
50+
},
51+
openGraph: {
52+
url: canonicalPath,
53+
},
4754
};
4855
}
4956

@@ -61,6 +68,12 @@ export async function generateMetadata(props: {
6168
return {
6269
title: `${title} | Claude Code Tools`,
6370
description: `Browse ${title.toLowerCase()} for Claude Code`,
71+
alternates: {
72+
canonical: canonicalPath,
73+
},
74+
openGraph: {
75+
url: canonicalPath,
76+
},
6477
};
6578
}
6679

0 commit comments

Comments
 (0)