diff --git a/.gitignore b/.gitignore
index 53eaa2196..e389f766c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
/target
**/*.rs.bk
+.something/
+node_modules/
+frontend/node_modules/
+*.lock
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 000000000..23b588a4b
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "url-shortener-frontend",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.18.0",
+ "react-helmet-async": "^2.0.1",
+ "@tanstack/react-query": "^5.8.0",
+ "zustand": "^4.4.6",
+ "axios": "^1.6.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.37",
+ "@types/react-dom": "^18.2.15",
+ "@vitejs/plugin-react": "^4.2.0",
+ "autoprefixer": "^10.4.16",
+ "daisyui": "^4.0.0",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.5",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.0",
+ "vitest": "^1.0.0",
+ "@testing-library/react": "^14.1.0",
+ "@testing-library/jest-dom": "^6.1.5",
+ "@testing-library/user-event": "^14.5.1",
+ "jsdom": "^22.1.0",
+ "msw": "^2.0.0"
+ }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 000000000..5a3561d33
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,14 @@
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import Home from './pages/Home';
+
+function App() {
+ return (
+
+
+ } />
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 000000000..5950beb17
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { HelmetProvider } from 'react-helmet-async';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+);
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
new file mode 100644
index 000000000..33ea60f3d
--- /dev/null
+++ b/frontend/src/pages/Home.tsx
@@ -0,0 +1,175 @@
+import { Helmet } from 'react-helmet-async';
+import type { SEOConfig } from '../types/home';
+
+const SEO_CONFIG: SEOConfig = {
+ title: 'Shorten Links, Track Clicks | URL Shortener',
+ description: 'Free URL shortener with analytics. Create short links, track clicks, and measure performance. Get started in seconds.',
+ canonicalUrl: 'https://urlshortener.example.com',
+ ogImage: 'https://urlshortener.example.com/og-image.png',
+};
+
+const STRUCTURED_DATA = {
+ '@context': 'https://schema.org',
+ '@type': 'WebApplication',
+ name: 'URL Shortener',
+ description: SEO_CONFIG.description,
+ url: SEO_CONFIG.canonicalUrl,
+ applicationCategory: 'UtilityApplication',
+ operatingSystem: 'Web',
+ offers: {
+ '@type': 'Offer',
+ price: '0',
+ priceCurrency: 'USD',
+ },
+};
+
+function Home() {
+ return (
+ <>
+
+ {SEO_CONFIG.title}
+
+
+
+ {/* Open Graph tags */}
+
+
+
+
+
+
+ {/* Twitter Card tags */}
+
+
+
+
+
+ {/* Structured data */}
+
+
+
+
+
+
+
+
+
+
+
+ Shorten Links, Track Clicks
+
+
+ Create short, memorable links and track their performance with powerful analytics.
+
+
+
+
+
+
+
+
+
+ Why Choose Us?
+
+
+
+
+
URL Shortening
+
Create short, shareable links instantly.
+
+
+
+
+
Analytics
+
Track clicks, locations, and devices.
+
+
+
+
+
Dashboard
+
Manage all your links in one place.
+
+
+
+
+
Security
+
Enterprise-grade security for your links.
+
+
+
+
+
+
+
+
+
+ How It Works
+
+
+
+
1
+
Paste
+
Enter your long URL
+
+
+
2
+
Shorten
+
Get a short link instantly
+
+
+
3
+
Share
+
Share and track clicks
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default Home;
+
+export { SEO_CONFIG, STRUCTURED_DATA };
diff --git a/frontend/src/types/custom.d.ts b/frontend/src/types/custom.d.ts
new file mode 100644
index 000000000..166b68480
--- /dev/null
+++ b/frontend/src/types/custom.d.ts
@@ -0,0 +1,11 @@
+///
+
+declare module '*.svg' {
+ const content: string;
+ export default content;
+}
+
+declare module '*.png' {
+ const content: string;
+ export default content;
+}
diff --git a/frontend/src/types/home.ts b/frontend/src/types/home.ts
new file mode 100644
index 000000000..a91e97322
--- /dev/null
+++ b/frontend/src/types/home.ts
@@ -0,0 +1,45 @@
+/**
+ * Homepage-specific type definitions.
+ * Owner: First scenario builder
+ */
+
+export interface Feature {
+ id: string;
+ icon: string;
+ title: string;
+ description: string;
+}
+
+export interface Testimonial {
+ id: string;
+ quote: string;
+ author: string;
+ role: string;
+ company?: string;
+ avatar?: string;
+}
+
+export interface FAQItem {
+ id: string;
+ question: string;
+ answer: string;
+}
+
+export interface PublicStats {
+ total_urls: number;
+ total_clicks: number;
+ active_users?: number;
+}
+
+export interface ShortenResponse {
+ short_code: string;
+ short_url: string;
+ original_url: string;
+}
+
+export interface SEOConfig {
+ title: string;
+ description: string;
+ canonicalUrl: string;
+ ogImage?: string;
+}
diff --git a/frontend/tests/pages/Home.test.tsx b/frontend/tests/pages/Home.test.tsx
new file mode 100644
index 000000000..8b5293dba
--- /dev/null
+++ b/frontend/tests/pages/Home.test.tsx
@@ -0,0 +1,251 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import { HelmetProvider } from 'react-helmet-async';
+import { BrowserRouter } from 'react-router-dom';
+import Home, { SEO_CONFIG, STRUCTURED_DATA } from '../../src/pages/Home';
+
+const renderWithProviders = (component: React.ReactNode) => {
+ const helmetContext = {};
+ return render(
+
+
+ {component}
+
+
+ );
+};
+
+describe('Home Page SEO', () => {
+ beforeEach(() => {
+ document.head.innerHTML = '';
+ });
+
+ describe('Test Case 1: Page Title Tag', () => {
+ it('should have a title tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const title = document.querySelector('title');
+ expect(title).not.toBeNull();
+ });
+ });
+
+ it('should have a descriptive title', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const title = document.querySelector('title');
+ expect(title?.textContent).toContain('URL Shortener');
+ });
+ });
+
+ it('should have a title under 60 characters', async () => {
+ renderWithProviders();
+
+ expect(SEO_CONFIG.title.length).toBeLessThanOrEqual(60);
+ });
+
+ it('should include primary keywords in title', async () => {
+ expect(SEO_CONFIG.title.toLowerCase()).toContain('shorten');
+ expect(SEO_CONFIG.title.toLowerCase()).toContain('link');
+ });
+ });
+
+ describe('Test Case 2: Meta Description', () => {
+ it('should have a meta description tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const metaDesc = document.querySelector('meta[name="description"]');
+ expect(metaDesc).not.toBeNull();
+ });
+ });
+
+ it('should have a meta description under 160 characters', async () => {
+ expect(SEO_CONFIG.description.length).toBeLessThanOrEqual(160);
+ });
+
+ it('should have a descriptive meta description content', async () => {
+ expect(SEO_CONFIG.description.toLowerCase()).toContain('url shortener');
+ });
+ });
+
+ describe('Test Case 3: Open Graph Tags', () => {
+ it('should have og:title tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const ogTitle = document.querySelector('meta[property="og:title"]');
+ expect(ogTitle).not.toBeNull();
+ });
+ });
+
+ it('should have og:description tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const ogDesc = document.querySelector('meta[property="og:description"]');
+ expect(ogDesc).not.toBeNull();
+ });
+ });
+
+ it('should have og:image tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const ogImage = document.querySelector('meta[property="og:image"]');
+ expect(ogImage).not.toBeNull();
+ });
+ });
+
+ it('should have og:url tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const ogUrl = document.querySelector('meta[property="og:url"]');
+ expect(ogUrl).not.toBeNull();
+ });
+ });
+
+ it('should have og:type tag present', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const ogType = document.querySelector('meta[property="og:type"]');
+ expect(ogType).not.toBeNull();
+ expect(ogType?.getAttribute('content')).toBe('website');
+ });
+ });
+ });
+
+ describe('Test Case 4: Semantic HTML Structure', () => {
+ it('should have a header element', () => {
+ renderWithProviders();
+
+ const header = document.querySelector('header');
+ expect(header).not.toBeNull();
+ expect(header?.getAttribute('role')).toBe('banner');
+ });
+
+ it('should have a nav element within header', () => {
+ renderWithProviders();
+
+ const nav = document.querySelector('header nav');
+ expect(nav).not.toBeNull();
+ expect(nav?.getAttribute('role')).toBe('navigation');
+ });
+
+ it('should have a main element', () => {
+ renderWithProviders();
+
+ const main = document.querySelector('main');
+ expect(main).not.toBeNull();
+ expect(main?.getAttribute('role')).toBe('main');
+ expect(main?.getAttribute('id')).toBe('main-content');
+ });
+
+ it('should have section elements with proper labels', () => {
+ renderWithProviders();
+
+ const sections = document.querySelectorAll('section');
+ expect(sections.length).toBeGreaterThanOrEqual(2);
+
+ // Check that sections have aria-labelledby
+ sections.forEach(section => {
+ const labelledBy = section.getAttribute('aria-labelledby');
+ expect(labelledBy).not.toBeNull();
+ });
+ });
+
+ it('should have a footer element', () => {
+ renderWithProviders();
+
+ const footer = document.querySelector('footer');
+ expect(footer).not.toBeNull();
+ expect(footer?.getAttribute('role')).toBe('contentinfo');
+ });
+
+ it('should have proper heading hierarchy with h1', () => {
+ renderWithProviders();
+
+ const h1 = screen.getByRole('heading', { level: 1 });
+ expect(h1).toBeInTheDocument();
+ });
+
+ it('should have h2 headings for sections', () => {
+ renderWithProviders();
+
+ const h2s = screen.getAllByRole('heading', { level: 2 });
+ expect(h2s.length).toBeGreaterThanOrEqual(2);
+ });
+ });
+
+ describe('Test Case 5: Canonical URL', () => {
+ it('should have a canonical link tag', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const canonical = document.querySelector('link[rel="canonical"]');
+ expect(canonical).not.toBeNull();
+ });
+ });
+
+ it('should point canonical to the homepage URL', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const canonical = document.querySelector('link[rel="canonical"]');
+ expect(canonical?.getAttribute('href')).toBe(SEO_CONFIG.canonicalUrl);
+ });
+ });
+
+ it('should have a valid canonical URL format', () => {
+ const urlPattern = /^https?:\/\/.+/;
+ expect(SEO_CONFIG.canonicalUrl).toMatch(urlPattern);
+ });
+ });
+
+ describe('Structured Data (JSON-LD)', () => {
+ it('should have JSON-LD structured data', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const jsonLd = document.querySelector('script[type="application/ld+json"]');
+ expect(jsonLd).not.toBeNull();
+ });
+ });
+
+ it('should have valid Schema.org context', () => {
+ expect(STRUCTURED_DATA['@context']).toBe('https://schema.org');
+ });
+
+ it('should have WebApplication type', () => {
+ expect(STRUCTURED_DATA['@type']).toBe('WebApplication');
+ });
+
+ it('should include application name', () => {
+ expect(STRUCTURED_DATA.name).toBe('URL Shortener');
+ });
+ });
+
+ describe('Twitter Card Meta Tags', () => {
+ it('should have twitter:card meta tag', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const twitterCard = document.querySelector('meta[name="twitter:card"]');
+ expect(twitterCard).not.toBeNull();
+ expect(twitterCard?.getAttribute('content')).toBe('summary_large_image');
+ });
+ });
+
+ it('should have twitter:title meta tag', async () => {
+ renderWithProviders();
+
+ await waitFor(() => {
+ const twitterTitle = document.querySelector('meta[name="twitter:title"]');
+ expect(twitterTitle).not.toBeNull();
+ });
+ });
+ });
+});
diff --git a/frontend/tests/setup.ts b/frontend/tests/setup.ts
new file mode 100644
index 000000000..9347c17d2
--- /dev/null
+++ b/frontend/tests/setup.ts
@@ -0,0 +1,7 @@
+import '@testing-library/jest-dom';
+import { cleanup } from '@testing-library/react';
+import { afterEach } from 'vitest';
+
+afterEach(() => {
+ cleanup();
+});
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 000000000..ca002b7b9
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "tests"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 000000000..8e3fc387a
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts", "vitest.config.ts"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 000000000..e45e78984
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ },
+ },
+ },
+});
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 000000000..ebdd33654
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['./tests/setup.ts'],
+ include: ['tests/**/*.{test,spec}.{ts,tsx}'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ },
+ },
+});