Skip to content

Commit 7820e85

Browse files
ChengaDevclaude
andcommitted
SEO improvements: schemas, 404, internal links, bug fixes
- Optimize home page title/H1/description for 'online' keyword - Add subtle body text below clock for Google indexing - Add WebSite schema with author sameAs (LinkedIn, GitHub, personal site) - Add BreadcrumbList schema on all inner pages - Fix Helmet Fragment error causing TypeError in console - Fix isCurrentlyTicking prop leaking to DOM (transient prop) - Add custom 404 page; fix LangLayout redirecting invalid paths to home - Add SeeAlso internal links component on FAQ/Instructions/FIBA pages - Add og:image:alt and twitter:image:alt - Add aria-label to language flag buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ae0bc99 commit 7820e85

10 files changed

Lines changed: 355 additions & 19 deletions

File tree

public/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
<meta property="og:title" content="ShotClock Pro - Basketball Shot Clock Training" />
1919
<meta property="og:description" content="Professional basketball shot clock training application. Practice your 24-second shot clock operation skills with real-time simulation." />
2020
<meta property="og:image" content="%PUBLIC_URL%/og-image.png" />
21+
<meta property="og:image:alt" content="ShotClock Pro — free online basketball shot clock" />
2122

2223
<!-- Twitter -->
2324
<meta property="twitter:card" content="summary_large_image" />
2425
<meta property="twitter:url" content="https://www.24shotclock.com/" />
2526
<meta property="twitter:title" content="ShotClock Pro - Basketball Shot Clock Training" />
2627
<meta property="twitter:description" content="Professional basketball shot clock training application. Practice your 24-second shot clock operation skills with real-time simulation." />
2728
<meta property="twitter:image" content="%PUBLIC_URL%/og-image.png" />
29+
<meta property="twitter:image:alt" content="ShotClock Pro — free online basketball shot clock" />
2830

2931
<script
3032
data-ad-client="ca-pub-1951283706310480"

src/App.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, lazy, Suspense } from 'react'
2-
import { BrowserRouter as Router, Routes, Route, useLocation, Navigate, useParams, Outlet, useNavigate } from 'react-router-dom'
2+
import { BrowserRouter as Router, Routes, Route, useLocation, useParams, Outlet, useNavigate } from 'react-router-dom'
33
import styled, { ThemeProvider } from 'styled-components'
44
import LanguageProvider, { useLocalization } from './contexts/Language/LanguageProvider'
55
import Navigation from './components/Navigation'
@@ -15,6 +15,8 @@ const ShotClockPage = lazy(() => import('./components/ShotClockPage'))
1515
const AboutUs = lazy(() => import('./components/AboutUs'))
1616
const Instructions = lazy(() => import('./components/Instructions'))
1717
const FIBAResources = lazy(() => import('./components/FIBAResources'))
18+
const FAQ = lazy(() => import('./components/FAQ'))
19+
const NotFound = lazy(() => import('./components/NotFound'))
1820

1921
const NON_ENGLISH_LANGS = ['it', 'es', 'fr']
2022

@@ -56,18 +58,21 @@ const LangLayout = () => {
5658
const location = useLocation()
5759
const fadeIn = useSpring({ from: { opacity: 0 }, to: { opacity: 1 }, config: { duration: 800 } })
5860

61+
const isValidLang = lang === 'en' || (!!lang && NON_ENGLISH_LANGS.includes(lang))
62+
5963
useEffect(() => {
64+
if (!isValidLang) return
6065
if (lang === 'en') {
6166
// Redirect /en/* → /* to keep English at canonical URLs
6267
const pagePath = location.pathname.slice('/en'.length) || '/'
6368
navigate(pagePath, { replace: true })
6469
} else if (lang && NON_ENGLISH_LANGS.includes(lang)) {
6570
changeLocale(lang)
66-
} else {
67-
navigate('/', { replace: true })
6871
}
6972
}, [lang])
7073

74+
if (!isValidLang) return <NotFound />
75+
7176
return <animated.div style={fadeIn}><Outlet /></animated.div>
7277
}
7378

@@ -98,6 +103,11 @@ const pageRoutes = (
98103
<FIBAResources />
99104
</PageContent>
100105
} />
106+
<Route path="faq" element={
107+
<PageContent title="FAQ - Basketball Shot Clock App">
108+
<FAQ />
109+
</PageContent>
110+
} />
101111
</>
102112
)
103113

@@ -126,7 +136,7 @@ const App = () => {
126136
{pageRoutes}
127137
</Route>
128138

129-
<Route path="*" element={<Navigate to="/" replace />} />
139+
<Route path="*" element={<NotFound />} />
130140
</Routes>
131141
</Suspense>
132142
</MainContent>

src/components/Controls.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function Buttons(props: ControlsProps) {
1616

1717
return (
1818
<Container>
19-
<TimeToggleButton id='btnStart' onClick={props.onTickToggle} isCurrentlyTicking={props.isTicking}>
19+
<TimeToggleButton id='btnStart' onClick={props.onTickToggle} $isCurrentlyTicking={props.isTicking}>
2020
{props.isTicking ? locals.stopLabel : locals.startLabel}
2121
</TimeToggleButton>
2222
<ResetButton id='btnReset14' onClick={props.on14SecondsClick}>
@@ -77,12 +77,12 @@ const ClockButton = styled.button`
7777
`;
7878

7979
type TimeToggleButtonProps = {
80-
isCurrentlyTicking: boolean;
80+
$isCurrentlyTicking: boolean;
8181
};
8282

8383
const TimeToggleButton = styled(ClockButton)<TimeToggleButtonProps>`
8484
color: ${(props) => props.theme.colors.white};
85-
background-color: ${(props) => (props.isCurrentlyTicking ? props.theme.colors.red : 'green')};
85+
background-color: ${(props) => (props.$isCurrentlyTicking ? props.theme.colors.red : 'green')};
8686
`;
8787

8888
const ResetButton = styled(ClockButton)`

src/components/FAQ.tsx

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import React, { useState } from 'react'
2+
import styled, { keyframes } from 'styled-components'
3+
import { Helmet } from 'react-helmet'
4+
import { useLocalization } from '../contexts/Language/LanguageProvider'
5+
import SEO from './SEO'
6+
import SeeAlso from './SeeAlso'
7+
8+
const fadeInUp = keyframes`
9+
from { opacity: 0; transform: translateY(20px); }
10+
to { opacity: 1; transform: translateY(0); }
11+
`
12+
13+
const FAQ = () => {
14+
const { locals } = useLocalization()
15+
const [openIndex, setOpenIndex] = useState<number | null>(null)
16+
17+
const toggle = (index: number) => {
18+
setOpenIndex(openIndex === index ? null : index)
19+
}
20+
21+
const faqSchema = {
22+
'@context': 'https://schema.org',
23+
'@type': 'FAQPage',
24+
mainEntity: locals.faqItems.map(item => ({
25+
'@type': 'Question',
26+
name: item.question,
27+
acceptedAnswer: {
28+
'@type': 'Answer',
29+
text: item.answer,
30+
},
31+
})),
32+
}
33+
34+
return (
35+
<Container>
36+
<SEO
37+
title="FAQ | ShotClock Pro"
38+
description="Frequently asked questions about basketball shot clock rules, the 14-second reset, and how to use the ShotClock Pro training simulator."
39+
/>
40+
<Helmet>
41+
<script type="application/ld+json">{JSON.stringify(faqSchema)}</script>
42+
</Helmet>
43+
44+
<AnimatedTitle>{locals.faqTitle}</AnimatedTitle>
45+
<Description>{locals.faqDescription}</Description>
46+
47+
<FaqList>
48+
{locals.faqItems.map((item, index) => (
49+
<FaqItem key={index} style={{ animationDelay: `${index * 0.05}s` }}>
50+
<Question onClick={() => toggle(index)} isOpen={openIndex === index}>
51+
<QuestionText>{item.question}</QuestionText>
52+
<Chevron isOpen={openIndex === index}></Chevron>
53+
</Question>
54+
{openIndex === index && (
55+
<Answer>{item.answer}</Answer>
56+
)}
57+
</FaqItem>
58+
))}
59+
</FaqList>
60+
<SeeAlso exclude={['faq']} />
61+
</Container>
62+
)
63+
}
64+
65+
const Container = styled.div`
66+
max-width: 860px;
67+
margin: 0 auto;
68+
@media (min-width: 768px) {
69+
padding: 2rem;
70+
}
71+
`
72+
73+
const AnimatedTitle = styled.h1`
74+
font-size: 2.5rem;
75+
color: ${props => props.theme.titleColor};
76+
margin-bottom: 1rem;
77+
text-align: center;
78+
font-weight: 700;
79+
font-family: 'Poppins', sans-serif;
80+
letter-spacing: -0.5px;
81+
animation: ${fadeInUp} 0.6s ease-out forwards;
82+
83+
@media (max-width: 768px) {
84+
font-size: 2rem;
85+
}
86+
`
87+
88+
const Description = styled.p`
89+
text-align: center;
90+
color: ${props => props.theme.text};
91+
font-size: 1.1rem;
92+
line-height: 1.6;
93+
margin-bottom: 3rem;
94+
animation: ${fadeInUp} 0.6s ease-out 0.1s forwards;
95+
opacity: 0;
96+
animation-fill-mode: forwards;
97+
`
98+
99+
const FaqList = styled.div`
100+
display: flex;
101+
flex-direction: column;
102+
gap: 1rem;
103+
`
104+
105+
const FaqItem = styled.div`
106+
background: rgba(0, 0, 0, 0.5);
107+
border-radius: 12px;
108+
border: 1px solid rgba(255, 255, 255, 0.1);
109+
overflow: hidden;
110+
animation: ${fadeInUp} 0.5s ease-out forwards;
111+
opacity: 0;
112+
transition: border-color 0.2s ease;
113+
114+
&:hover {
115+
border-color: rgba(255, 215, 0, 0.3);
116+
}
117+
`
118+
119+
const Question = styled.button<{ isOpen: boolean }>`
120+
width: 100%;
121+
display: flex;
122+
justify-content: space-between;
123+
align-items: center;
124+
padding: 1.25rem 1.5rem;
125+
background: none;
126+
border: none;
127+
cursor: pointer;
128+
text-align: left;
129+
gap: 1rem;
130+
background: ${props => props.isOpen ? 'rgba(255, 215, 0, 0.08)' : 'transparent'};
131+
transition: background 0.2s ease;
132+
133+
&:focus {
134+
outline: none;
135+
}
136+
`
137+
138+
const QuestionText = styled.span`
139+
font-size: 1.05rem;
140+
font-weight: 600;
141+
color: ${props => props.theme.mainTextColor};
142+
line-height: 1.4;
143+
`
144+
145+
const Chevron = styled.span<{ isOpen: boolean }>`
146+
color: #ffd700;
147+
font-size: 1.3rem;
148+
flex-shrink: 0;
149+
transform: ${props => props.isOpen ? 'rotate(180deg)' : 'rotate(0deg)'};
150+
transition: transform 0.25s ease;
151+
display: inline-block;
152+
`
153+
154+
const Answer = styled.p`
155+
padding: 0 1.5rem 1.25rem;
156+
margin: 0;
157+
color: rgba(255, 255, 255, 0.85);
158+
font-size: 1rem;
159+
line-height: 1.7;
160+
border-top: 1px solid rgba(255, 255, 255, 0.07);
161+
padding-top: 1rem;
162+
`
163+
164+
export default FAQ

src/components/FIBAResources.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react'
22
import styled, { keyframes } from 'styled-components'
33
import { useLocalization } from '../contexts/Language/LanguageProvider'
44
import SEO from './SEO'
5+
import SeeAlso from './SeeAlso'
56

67
const fadeInUp = keyframes`
78
from {
@@ -88,6 +89,7 @@ const FIBAResources = () => {
8889
</InfoCard>
8990
</InfoGrid>
9091
</AdditionalInfo>
92+
<SeeAlso exclude={['fiba-resources']} />
9193
</Container>
9294
)
9395
}

src/components/Instructions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet'
33
import styled, { keyframes } from 'styled-components'
44
import { useLocalization } from '../contexts/Language/LanguageProvider'
55
import SEO from './SEO'
6+
import SeeAlso from './SeeAlso'
67

78
const fadeInUp = keyframes`
89
from {
@@ -79,6 +80,7 @@ const Instructions = () => {
7980
))}
8081
</TipsGrid>
8182
</TipsSection>
83+
<SeeAlso exclude={['instructions']} />
8284
</Container>
8385
)
8486
}

src/components/LanguageSelector.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ const LanguageSelector = () => {
2525

2626
return (
2727
<Container>
28-
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.Italian)}>
28+
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.Italian)} aria-label="Switch to Italian">
2929
<Flag height="15" code="ITA" />
3030
</FlagContainer>
31-
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.Spanish)}>
31+
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.Spanish)} aria-label="Switch to Spanish">
3232
<Flag height="15" code="ES" />
3333
</FlagContainer>
34-
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.French)}>
34+
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.French)} aria-label="Switch to French">
3535
<Flag height="15" code="FR" />
3636
</FlagContainer>
37-
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.English)}>
37+
<FlagContainer onClick={() => handleLanguageChange(LanguageCodes.English)} aria-label="Switch to English">
3838
<Flag height="15" code="US" />
3939
</FlagContainer>
4040
</Container>

0 commit comments

Comments
 (0)