Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,41 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>Local Competitor Analyzer | Business Intelligence Tool</title>
<meta name="description" content="Analyze your local business competition with AI-powered insights. Discover nearby competitors, get strategic recommendations, and make data-driven decisions." />
<meta property="og:title" content="Local Competitor Analyzer" />
<meta property="og:description" content="AI-powered local business competition analysis tool" />
<title>Competitor Analysis Tool for Local Businesses | Competitor Watcher</title>
<meta name="description" content="Competitor Watcher is a free competitor analysis tool for local businesses. Run competitive analysis, benchmark nearby competitors, and get AI-powered market insights." />
<meta name="keywords" content="competitor analysis, competitive analysis tool, competitor analysis software, competitive intelligence, local market analysis, local SEO competitor analysis, market intelligence software" />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<link rel="canonical" href="https://competitorwatcher.pt/" />

<meta property="og:title" content="Competitor Analysis Tool for Local Businesses | Competitor Watcher" />
<meta property="og:description" content="Run competitor analysis for any location, compare local competitors, and get actionable AI insights." />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Competitor Watcher" />
<meta property="og:url" content="https://competitorwatcher.pt/" />
<meta property="og:image" content="https://competitorwatcher.pt/og-image.png" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Competitor Analysis Tool for Local Businesses | Competitor Watcher" />
<meta name="twitter:description" content="Free competitor analysis software for local businesses with AI market insights." />
<meta name="twitter:image" content="https://competitorwatcher.pt/og-image.png" />

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Competitor Watcher",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"url": "https://competitorwatcher.pt/",
"description": "Competitor Watcher is a free competitor analysis tool for local businesses that need competitive intelligence and local market analysis.",
"keywords": "competitor analysis, competitive analysis tool, competitor analysis software, competitive intelligence, local market analysis",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR"
}
}
</script>
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Expand All @@ -17,4 +47,4 @@
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>
21 changes: 21 additions & 0 deletions client/public/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Competitor Watcher
> Competitor Watcher is a free competitor analysis platform for local businesses.

## Summary
- Product: AI-powered competitor analysis and competitive intelligence software.
- Primary users: Local businesses, agencies, and operators who need fast market visibility.
- Core use cases: competitor research, local market analysis, review intelligence, and tactical recommendations.

## Public pages
- Homepage: https://competitorwatcher.pt/
- Support: https://competitorwatcher.pt/support
- Privacy policy: https://competitorwatcher.pt/privacy-policy
- Cookie policy: https://competitorwatcher.pt/cookie-policy
- Sitemap: https://competitorwatcher.pt/sitemap.xml

## Access notes
- Main product workflows require authentication in /dashboard.
- Public API routes live under /api/* and are rate-limited.

## Keywords
competitor analysis, competitive analysis tool, competitor analysis software, competitive intelligence, local market analysis, competitor tracking
12 changes: 12 additions & 0 deletions client/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
User-agent: *
Allow: /
Allow: /login
Allow: /register
Allow: /support
Allow: /privacy-policy
Allow: /cookie-policy
Allow: /llms.txt
Disallow: /dashboard
Disallow: /settings
Disallow: /admin
Disallow: /admin/
Disallow: /api/
Disallow: /r/

Sitemap: https://competitorwatcher.pt/sitemap.xml
18 changes: 9 additions & 9 deletions client/public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,37 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://competitorwatcher.pt/</loc>
<lastmod>2024-03-20</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://competitorwatcher.pt/login</loc>
<lastmod>2024-03-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<priority>0.6</priority>
</url>
<url>
<loc>https://competitorwatcher.pt/register</loc>
<lastmod>2024-03-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<priority>0.6</priority>
</url>
<url>
<loc>https://competitorwatcher.pt/support</loc>
<lastmod>2024-03-20</lastmod>
<changefreq>monthly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://competitorwatcher.pt/privacy-policy</loc>
<lastmod>2024-03-20</lastmod>
<changefreq>yearly</changefreq>
<priority>0.5</priority>
<priority>0.4</priority>
</url>
<url>
<loc>https://competitorwatcher.pt/cookie-policy</loc>
<lastmod>2024-03-20</lastmod>
<changefreq>yearly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://competitorwatcher.pt/llms.txt</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>
6 changes: 3 additions & 3 deletions client/src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,8 @@
"perMonth": "/month"
},
"quickSearch": {
"title": "Discover Your Local Competition",
"subtitle": "Analyze competitors in any location instantly",
"title": "Competitor Analysis for Local Businesses",
"subtitle": "Run instant competitor research and competitive analysis in any location",
"addressPlaceholder": "Enter business address",
"selectType": "Business type",
"selectRadius": "Search radius",
Expand Down Expand Up @@ -900,4 +900,4 @@
"generationStarted": "Report Generation Started",
"generationStartedDesc": "We are generating your report in the background. You will be notified when it is ready."
}
}
}
6 changes: 3 additions & 3 deletions client/src/i18n/locales/pt/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,8 @@
"perMonth": "/mês"
},
"quickSearch": {
"title": "Descubra a Sua Concorrência Local",
"subtitle": "Analise concorrentes em qualquer localização instantaneamente",
"title": "Análise de Concorrência para Negócios Locais",
"subtitle": "Faça análise competitiva em qualquer localização em segundos",
"addressPlaceholder": "Introduza a morada do negócio",
"selectType": "Tipo de negócio",
"selectRadius": "Raio de pesquisa",
Expand Down Expand Up @@ -899,4 +899,4 @@
"generationStarted": "Geração de Relatório Iniciada",
"generationStartedDesc": "Estamos a gerar o seu relatório em segundo plano. Será notificado quando estiver pronto."
}
}
}
8 changes: 8 additions & 0 deletions client/src/pages/LandingPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@
line-height: 1.6;
}

.hero-intent-text {
max-width: 860px;
margin: 0 auto 2rem;
font-size: 0.975rem;
line-height: 1.6;
color: rgba(255, 255, 255, 0.92);
}

.hero-features {
display: flex;
flex-direction: column;
Expand Down
71 changes: 58 additions & 13 deletions client/src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,24 @@ import { usePricingModal } from "@/context/PricingModalContext";

export default function LandingPage() {
const { isAuthenticated, isLoading } = useAuth();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const { openPricing } = usePricingModal();
const language = i18n?.language ?? "en";
const siteUrl = "https://competitorwatcher.pt";
const canonicalUrl = `${siteUrl}/`;
const seoTitle = "Competitor Analysis Tool for Local Businesses | Competitor Watcher";
const seoDescription = "Competitor Watcher is a free competitor analysis platform for local businesses. Discover competitors, benchmark your market, and get actionable AI insights.";
const seoKeywords = "competitor analysis, competitive analysis tool, competitor analysis software, competitive intelligence, local market analysis, competitor tracking, market intelligence software";
const ogLocale =
language === "pt"
? "pt_PT"
: language === "es"
? "es_ES"
: language === "fr"
? "fr_FR"
: language === "de"
? "de_DE"
: "en_US";

// Quick search state
const [isSearching, setIsSearching] = useState(false);
Expand Down Expand Up @@ -212,23 +228,27 @@ export default function LandingPage() {
return (
<div className="landing-page">
<Helmet>
<title>Competitor Watcher - Análise de Concorrência Local com IA</title>
<meta name="description" content="Analise a concorrência do seu negócio local com inteligência artificial. Descubra concorrentes, obtenha insights estratégicos e tome decisões baseadas em dados." />
<meta name="keywords" content="análise de concorrência, inteligência artificial, negócios locais, estratégia de mercado, competidores" />
<title>{seoTitle}</title>
<meta name="description" content={seoDescription} />
<meta name="keywords" content={seoKeywords} />
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<link rel="canonical" href={canonicalUrl} />

{/* Open Graph / Facebook */}
<meta property="og:type" content="website" />
<meta property="og:url" content="https://competitorwatcher.pt/" />
<meta property="og:title" content="Competitor Watcher - Análise de Concorrência Local" />
<meta property="og:description" content="Descubra e analise os seus concorrentes locais com o poder da IA." />
<meta property="og:image" content="https://competitorwatcher.pt/og-image.png" />
<meta property="og:site_name" content="Competitor Watcher" />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:title" content={seoTitle} />
<meta property="og:description" content={seoDescription} />
<meta property="og:image" content={`${siteUrl}/og-image.png`} />
<meta property="og:locale" content={ogLocale} />

{/* Twitter */}
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://competitorwatcher.pt/" />
<meta property="twitter:title" content="Competitor Watcher - Análise de Concorrência Local" />
<meta property="twitter:description" content="Descubra e analise os seus concorrentes locais com o poder da IA." />
<meta property="twitter:image" content="https://competitorwatcher.pt/og-image.png" />
<meta property="twitter:url" content={canonicalUrl} />
<meta property="twitter:title" content={seoTitle} />
<meta property="twitter:description" content={seoDescription} />
<meta property="twitter:image" content={`${siteUrl}/og-image.png`} />

{/* Structured Data */}
<script type="application/ld+json">
Expand All @@ -238,12 +258,31 @@ export default function LandingPage() {
"name": "Competitor Watcher",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"url": canonicalUrl,
"keywords": seoKeywords,
"featureList": [
"Competitor analysis for local businesses",
"Competitive intelligence dashboard",
"Local market trend detection",
"Review and sentiment analysis",
"AI-generated action recommendations"
],
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR"
},
"description": "AI-powered local business competition analysis tool."
"description": seoDescription
})}
</script>
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Competitor Watcher",
"url": canonicalUrl,
"description": seoDescription,
"inLanguage": ["en", "pt", "es", "fr", "de"]
})}
</script>
</Helmet>
Expand Down Expand Up @@ -290,6 +329,12 @@ export default function LandingPage() {
<p className="hero-subheadline" data-testid="hero-subheadline">
{t('quickSearch.subtitle')}
</p>
<p className="hero-intent-text" data-testid="hero-intent-text">
{t("quickSearch.seoIntent", {
defaultValue:
"Competitor Watcher is a free competitor analysis tool and competitive intelligence platform for local businesses. Compare nearby competitors, track market shifts, and get actionable recommendations.",
})}
</p>

{/* Quick Search Form */}
<Form {...form}>
Expand Down
20 changes: 18 additions & 2 deletions e2e/dashboard-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,18 +170,34 @@ test.describe('Dashboard Business Management Flow', () => {

// Now address is "verified" (pending status), select type
await page.getByTestId('select-business-type').click();
await page.getByTestId('option-type-restaurant').click();
const restaurantOption = page.getByTestId('option-type-restaurant').first();
await expect(restaurantOption).toBeVisible({ timeout: 10000 });
await restaurantOption.click({ force: true });
await expect(page.getByTestId('select-business-type')).toContainText(/Restaurant|Restaurante/i);

// Submit
// We need to update the GET mock to include the new business for the UI to update optimistically or on refetch?
// Actually, TanStack Query usually invalidates key 'businesses'.
// Let's update the GET mock dynamically for the next fetch.


const createPromise = page.waitForResponse((response) =>
response.url().includes('/api/businesses') &&
response.request().method() === 'POST' &&
response.status() === 200
);
const refetchPromise = page.waitForResponse((response) =>
/\/api\/businesses(\?|$)/.test(response.url()) &&
response.request().method() === 'GET' &&
response.status() === 200
);

await page.getByTestId('button-submit-business').click({ force: true });
await createPromise;
await refetchPromise;

// Verify it appears in the list
await expect(page.getByText('New Pizza Place')).toBeVisible();
await expect(page.getByRole('heading', { name: 'New Pizza Place' })).toBeVisible({ timeout: 10000 });
});

test('can edit an existing business', async ({ page }) => {
Expand Down
8 changes: 6 additions & 2 deletions e2e/i18n-thorough.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,16 @@ test.describe('Internationalization Thoroughness', () => {
await langSelector.click();
await page.getByTestId('button-lang-en').click();

await expect(page.getByRole('heading', { name: /Discover Your Local Competition/i })).toBeVisible();
await expect(
page.getByRole('heading', { name: /Competitor Analysis for Local Businesses|Discover Your Local Competition/i })
).toBeVisible();

// Switch to PT
await langSelector.click();
await page.getByTestId('button-lang-pt').click();

await expect(page.getByRole('heading', { name: /Descubra a sua Concorrência Local/i })).toBeVisible();
await expect(
page.getByRole('heading', { name: /Análise de Concorrência para Negócios Locais|Descubra a sua Concorrência Local/i })
).toBeVisible();
});
});
6 changes: 5 additions & 1 deletion e2e/seo-accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ test.describe('SEO & Accessibility', () => {

// Meta description
const description = page.locator('meta[name="description"]').first();
await expect(description).toHaveAttribute('content', /Analyze|concorrência/i, { timeout: 10000 });
await expect(description).toHaveAttribute(
'content',
/competitor analysis|Analyze|concorr[eê]ncia/i,
{ timeout: 10000 }
);

// OpenGraph tags
const ogTitle = page.locator('meta[property="og:title"]').first();
Expand Down
5 changes: 3 additions & 2 deletions e2e/settings-support.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ test.describe('Support & Settings Flows', () => {

// Verify toast or updated state
// When switching to PT, the toast should be in PT
await expect(page.getByText('Idioma atualizado')).toBeVisible({ timeout: 5000 });
await expect(
page.getByRole('status').filter({ hasText: /Idioma atualizado|Language updated/i }).first()
).toBeVisible({ timeout: 5000 });
});

test('authenticated user can update profile', async ({ page }) => {
Expand Down Expand Up @@ -123,4 +125,3 @@ test.describe('Support & Settings Flows', () => {
});

});

Loading