diff --git a/apps/web/client/src/app/llms-full.txt/route.ts b/apps/web/client/src/app/llms-full.txt/route.ts new file mode 100644 index 0000000000..f1ccb90d06 --- /dev/null +++ b/apps/web/client/src/app/llms-full.txt/route.ts @@ -0,0 +1,179 @@ +async function getFullDocumentation(docsUrl: string): Promise { + const baseContent = `# Onlook - Complete Documentation + +> Open-source visual editor for React apps. Design directly in your live React app and generate clean code. + +## Project Overview + +Onlook is a "Cursor for Designers" that enables designers to make live edits to React and TailwindCSS projects directly within the browser DOM. It provides a seamless integration between design and development. + +### Key Features + +- **Visual Editing**: Edit React components directly in the browser +- **Code Generation**: Automatically generates clean, production-ready code +- **TailwindCSS Integration**: Full support for Tailwind styling +- **AI Assistance**: Built-in AI chat for design and development help +- **Real-time Preview**: See changes instantly as you design +- **Component Library**: Reusable components and design systems + +### Architecture + +Onlook is structured as a monorepo with several interconnected apps and packages: + +- **Web App**: Next.js application with visual editor interface +- **Documentation**: Comprehensive guides and API references +- **Packages**: Shared utilities, UI components, and core functionality +- **Backend**: Supabase integration for user management and data storage + +### Technology Stack + +- **Frontend**: Next.js, React, TailwindCSS +- **Backend**: Supabase, tRPC, Drizzle ORM +- **AI Integration**: Anthropic Claude, OpenRouter +- **Development**: TypeScript, Bun, Docker +- **Deployment**: Vercel, CodeSandbox containers + +## Getting Started + +### Installation + +1. Clone the repository: + \`\`\`bash + git clone https://github.com/onlook-dev/onlook.git + cd onlook + \`\`\` + +2. Install dependencies: + \`\`\`bash + bun install + \`\`\` + +3. Set up environment variables: + \`\`\`bash + cp .env.example .env.local + \`\`\` + +4. Start the development server: + \`\`\`bash + bun dev + \`\`\` + +### First Project + +1. **Create a New Project**: Use the project creation wizard +2. **Import Existing Project**: Connect your React + TailwindCSS project +3. **Start Designing**: Use the visual editor to modify components +4. **Generate Code**: Export clean code changes to your project + +### Core Concepts + +- **Visual Editor**: The main interface for designing components +- **Style Editor**: Modify TailwindCSS classes through a visual interface +- **Component Tree**: Navigate and select elements in your React app +- **AI Chat**: Get help with design decisions and code generation +- **Code Export**: Generate and apply code changes to your project + +## API Reference + +### Core APIs + +- **Project Management**: Create, update, and manage projects +- **Component Editing**: Modify React components and their properties +- **Style Management**: Apply and manage TailwindCSS classes +- **AI Integration**: Chat with AI for design assistance +- **Code Generation**: Generate and export code changes + +### Authentication + +Onlook uses Supabase for authentication and user management: + +- **Sign Up/Sign In**: Email-based authentication +- **User Profiles**: Manage user settings and preferences +- **Project Access**: Control access to projects and collaboration + +### Data Models + +- **Projects**: Container for your React applications +- **Components**: Individual React components within projects +- **Styles**: TailwindCSS classes and custom styles +- **Conversations**: AI chat history and context + +## Contributing + +### Development Setup + +1. **Prerequisites**: Node.js 18+, Bun, Docker (optional) +2. **Environment**: Set up Supabase, AI providers, and other services +3. **Local Development**: Run the development server and containers +4. **Testing**: Run tests and ensure code quality + +### Code Standards + +- **TypeScript**: Strict type checking enabled +- **ESLint**: Code linting and formatting +- **Prettier**: Code formatting +- **Husky**: Pre-commit hooks for quality assurance + +### Pull Request Process + +1. Fork the repository and create a feature branch +2. Make your changes with appropriate tests +3. Ensure all tests pass and code is properly formatted +4. Submit a pull request with detailed description +5. Address review feedback and get approval + +## Deployment + +### Production Deployment + +- **Web App**: Deployed on Vercel with automatic CI/CD +- **Documentation**: Static site generation and deployment +- **Backend**: Supabase managed services +- **Containers**: CodeSandbox for development environments + +### Environment Configuration + +- **Production**: Optimized builds with caching +- **Staging**: Testing environment for new features +- **Development**: Local development with hot reloading + +## Community and Support + +### Getting Help + +- **Documentation**: Comprehensive guides and tutorials +- **Discord**: Active community for questions and discussions +- **GitHub Issues**: Bug reports and feature requests +- **Email**: Direct contact for business inquiries + +### Contributing + +- **Code Contributions**: Bug fixes, features, and improvements +- **Documentation**: Help improve guides and examples +- **Community**: Answer questions and help other users +- **Testing**: Report bugs and test new features + +--- + +For the most up-to-date information, visit our documentation at ${docsUrl} or join our Discord community at https://discord.gg/hERDfFZCsH. +`; + + return baseContent; +} + +export async function GET() { + try { + const docsUrl = process.env.DOCS_URL ?? 'https://docs.onlook.com'; + const content = await getFullDocumentation(docsUrl); + + return new Response(content, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Robots-Tag': 'llms-txt', + }, + }); + } catch (error) { + console.error('Error generating llms-full.txt:', error); + return new Response('Error generating documentation', { status: 500 }); + } +} diff --git a/apps/web/client/src/app/llms.txt/route.ts b/apps/web/client/src/app/llms.txt/route.ts new file mode 100644 index 0000000000..11a96c55cc --- /dev/null +++ b/apps/web/client/src/app/llms.txt/route.ts @@ -0,0 +1,77 @@ +interface LLMSSection { + title: string; + links: Array<[string, string]>; +} + +interface LLMSData { + title: string; + description: string; + sections: LLMSSection[]; +} + +function renderMarkdown(data: LLMSData): string { + let output = `# ${data.title}\n\n> ${data.description}\n\n`; + + for (const section of data.sections) { + output += `## ${section.title}\n\n`; + for (const [text, url] of section.links) { + output += `- [${text}](${url})\n`; + } + output += `\n`; + } + + return output; +} + +export function GET() { + const docsUrl = process.env.DOCS_URL ?? 'https://docs.onlook.com'; + + const llmsData: LLMSData = { + title: 'Onlook', + description: + 'Open-source visual editor for React apps. Design directly in your live React app and generate clean code.', + sections: [ + { + title: 'Getting Started', + links: [ + ['Documentation', docsUrl], + ['First Project', `${docsUrl}/getting-started/first-project`], + ['UI Overview', `${docsUrl}/getting-started/ui-overview`], + ['Core Features', `${docsUrl}/getting-started/core-features`], + ], + }, + { + title: 'Tutorials', + links: [ + ['Importing Templates', `${docsUrl}/tutorials/importing-templates`], + ['Figma to Onlook', `${docsUrl}/tutorials/figma-to-onlook`], + ], + }, + { + title: 'Contributing', + links: [ + ['Developer Guide', `${docsUrl}/contributing/developers`], + ['Running Locally', `${docsUrl}/contributing/developers/running-locally`], + ['Architecture', `${docsUrl}/contributing/developers/architecture`], + ], + }, + { + title: 'Resources', + links: [ + ['GitHub Repository', 'https://github.com/onlook-dev/onlook'], + ['FAQ', `${docsUrl}/faq`], + ['Discord Community', 'https://discord.gg/hERDfFZCsH'], + ], + }, + ], + }; + + const content = renderMarkdown(llmsData); + + return new Response(content, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Robots-Tag': 'llms-txt', + }, + }); +} diff --git a/apps/web/client/src/app/robots.ts b/apps/web/client/src/app/robots.ts new file mode 100644 index 0000000000..c0f78c90c8 --- /dev/null +++ b/apps/web/client/src/app/robots.ts @@ -0,0 +1,27 @@ +import type { MetadataRoute } from 'next'; + +const BASE_URL = process.env.APP_URL ?? 'https://onlook.com'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + disallow: [ + '/api/', + '/auth/', + '/callback/', + '/webhook/', + '/projects/', + '/project/', + '/invitation/', + '/_next/', + '/_vercel/', + '/private/', + ], + crawlDelay: 1, + }, + sitemap: `${BASE_URL}/sitemap.xml`, + host: BASE_URL, + }; +} diff --git a/apps/web/client/src/app/sitemap.ts b/apps/web/client/src/app/sitemap.ts new file mode 100644 index 0000000000..0122d56073 --- /dev/null +++ b/apps/web/client/src/app/sitemap.ts @@ -0,0 +1,7 @@ +import type { MetadataRoute } from 'next'; +import { getWebRoutes } from '@/lib/sitemap-utils'; + +export default async function sitemap(): Promise { + const routes = await getWebRoutes(); + return routes; +} diff --git a/apps/web/client/src/lib/sitemap-utils.ts b/apps/web/client/src/lib/sitemap-utils.ts new file mode 100644 index 0000000000..3263052ad7 --- /dev/null +++ b/apps/web/client/src/lib/sitemap-utils.ts @@ -0,0 +1,100 @@ +import { readdir } from 'fs/promises'; +import { join } from 'path'; +import type { MetadataRoute } from 'next'; + +const BASE_URL = process.env.APP_URL ?? 'https://onlook.com'; +const EXCLUDED_PATTERNS = [ + '/api/', + '/auth/', + '/callback/', + '/webhook/', + '/projects/', + '/project/', + '/invitation/', + '/_', +]; + +async function scanAppDirectory( + dir: string, + basePath = '', + excludedPatterns: string[], +): Promise { + const routes: string[] = []; + + try { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + const routePath = join(basePath, entry.name); + + if (entry.isDirectory()) { + if ( + entry.name.startsWith('_') || + entry.name.startsWith('(') || + entry.name.startsWith('[') + ) { + continue; + } + + const subRoutes = await scanAppDirectory(fullPath, routePath, excludedPatterns); + routes.push(...subRoutes); + } else if (entry.name === 'page.tsx' || entry.name === 'page.ts') { + let route = basePath === '' ? '/' : basePath.replace(/\\/g, '/'); + + if (!route.startsWith('/')) { + route = '/' + route; + } + + const shouldExclude = excludedPatterns.some((pattern) => route.startsWith(pattern)); + + if (!shouldExclude) { + routes.push(route); + } + } + } + } catch (error) { + console.warn(`Failed to scan directory ${dir}:`, error); + } + + return routes; +} + +function getRouteMetadata(route: string) { + const routeConfig = { + '/': { priority: 1.0, changeFrequency: 'daily' as const }, + '/pricing': { priority: 0.9, changeFrequency: 'weekly' as const }, + '/about': { priority: 0.9, changeFrequency: 'weekly' as const }, + '/faq': { priority: 0.7, changeFrequency: 'weekly' as const }, + '/login': { priority: 0.6, changeFrequency: 'monthly' as const }, + '/terms-of-service': { priority: 0.5, changeFrequency: 'monthly' as const }, + '/sitemap': { priority: 0.3, changeFrequency: 'monthly' as const }, + } as const; + + return ( + routeConfig[route as keyof typeof routeConfig] ?? { + priority: 0.5, + changeFrequency: 'monthly' as const, + } + ); +} + +export async function getWebRoutes(): Promise { + const now = new Date(); + + const appDir = join(process.cwd(), 'src', 'app'); + const discoveredRoutes = await scanAppDirectory(appDir, '', EXCLUDED_PATTERNS); + + const sitemapRoutes = discoveredRoutes.map((route) => { + const { priority, changeFrequency } = getRouteMetadata(route); + + return { + url: `${BASE_URL}${route}`, + lastModified: now, + changeFrequency, + priority, + }; + }); + + return sitemapRoutes; +} diff --git a/apps/web/client/src/utils/constants/index.ts b/apps/web/client/src/utils/constants/index.ts index 5119e9cbea..376b170b20 100644 --- a/apps/web/client/src/utils/constants/index.ts +++ b/apps/web/client/src/utils/constants/index.ts @@ -34,4 +34,4 @@ export const ExternalRoutes = { export const Git = { MAX_COMMIT_MESSAGE_LENGTH: 72, MAX_COMMIT_MESSAGE_BODY_LENGTH: 500, -} as const; \ No newline at end of file +} as const; diff --git a/docs/next-sitemap.config.js b/docs/next-sitemap.config.js deleted file mode 100644 index eab84e9388..0000000000 --- a/docs/next-sitemap.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('next-sitemap').IConfig} */ -module.exports = { - siteUrl: 'https://docs.onlook.dev', - generateRobotsTxt: false, // handled by route handler - generateIndexSitemap: true, -}; diff --git a/docs/package.json b/docs/package.json index 3f5c81c720..15b1c63ec1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,7 +7,6 @@ "dev": "next dev --turbo", "start": "next start", "postinstall": "fumadocs-mdx", - "postbuild": "next-sitemap", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -16,7 +15,6 @@ "fumadocs-mdx": "^11.7.4", "fumadocs-ui": "^15.6.9", "next": "15.3.1", - "next-sitemap": "^4.2.3", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/docs/src/app/llms-full.txt/route.ts b/docs/src/app/llms-full.txt/route.ts new file mode 100644 index 0000000000..48e1bc9b01 --- /dev/null +++ b/docs/src/app/llms-full.txt/route.ts @@ -0,0 +1,201 @@ +import { readFile, readdir } from 'fs/promises'; +import { join } from 'path'; + +// Enable ISR (Incremental Static Regeneration) with 1-hour revalidation +export const revalidate = 3600; + +interface DocFile { + path: string; + title: string; + content: string; +} + +async function scanDocsDirectory(dirPath: string, basePath: string = ''): Promise { + const files: DocFile[] = []; + + try { + const entries = await readdir(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dirPath, entry.name); + const relativePath = join(basePath, entry.name); + + if (entry.isDirectory()) { + const subFiles = await scanDocsDirectory(fullPath, relativePath); + files.push(...subFiles); + } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) { + try { + const content = await readFile(fullPath, 'utf-8'); + const title = extractTitle(content, entry.name); + + files.push({ + path: relativePath, + title, + content: cleanMarkdownContent(content), + }); + } catch (error) { + console.warn(`Failed to read file ${fullPath}:`, error); + } + } + } + } catch (error) { + console.warn(`Failed to scan directory ${dirPath}:`, error); + } + + return files; +} + +function extractTitle(content: string, filename: string): string { + // Try to extract title from frontmatter or first heading + const titleMatch = + content.match(/^title:\s*["']?([^"'\n]+)["']?/m) || content.match(/^#\s+(.+)$/m); + + if (titleMatch) { + return titleMatch[1].trim(); + } + + // Fallback to filename without extension + return filename.replace(/\.(mdx?|md)$/, '').replace(/-/g, ' '); +} + +function cleanMarkdownContent(content: string): string { + // Remove frontmatter + content = content.replace(/^---[\s\S]*?---\n/, ''); + + // Remove JSX components and imports + content = content.replace(/^import\s+.*$/gm, ''); + content = content.replace(/<[^>]+>/g, ''); + + // Clean up extra whitespace + content = content.replace(/\n{3,}/g, '\n\n'); + + return content.trim(); +} + +async function getFullDocumentation(baseUrl: string, webUrl?: string): Promise { + const docsPath = join(process.cwd(), 'content', 'docs'); + const docFiles = await scanDocsDirectory(docsPath); + + let fullContent = `# Onlook Documentation - Complete Reference + +> Comprehensive documentation for Onlook - the open-source visual editor for React apps. Design directly in your live React app and generate clean code. + +## Table of Contents + +`; + + // Generate table of contents + for (const file of docFiles) { + const anchor = file.title.toLowerCase().replace(/[^a-z0-9]+/g, '-'); + fullContent += `- [${file.title}](#${anchor})\n`; + } + + fullContent += '\n---\n\n'; + + // Add all documentation content + for (const file of docFiles) { + const anchor = file.title.toLowerCase().replace(/[^a-z0-9]+/g, '-'); + fullContent += `## ${file.title} {#${anchor}}\n\n`; + fullContent += `*Source: ${file.path}*\n\n`; + fullContent += file.content; + fullContent += '\n\n---\n\n'; + } + + // Add additional project information + fullContent += `## Project Information + +### Repository Structure + +Onlook is structured as a monorepo with the following key directories: + +- **apps/web/**: Main web application (Next.js) +- **docs/**: Documentation site (Next.js with Fumadocs) +- **packages/**: Shared packages and utilities + - **ai/**: AI integration and chat functionality + - **ui/**: Reusable UI components + - **models/**: Data models and types + - **parser/**: Code parsing and manipulation + - **db/**: Database schema and utilities + +### Technology Stack + +- **Frontend**: Next.js, React, TailwindCSS, TypeScript +- **Backend**: Supabase, tRPC, Drizzle ORM +- **AI**: Anthropic Claude, OpenRouter integration +- **Development**: Bun, Docker, CodeSandbox containers +- **Deployment**: Vercel + +### Key Features + +- Visual editing of React components in the browser +- Real-time code generation and export +- TailwindCSS integration with visual style editor +- AI-powered design assistance +- Component library and design system support +- Collaborative editing capabilities + +### Community + +- **GitHub**: https://github.com/onlook-dev/onlook +- **Discord**: https://discord.gg/hERDfFZCsH +- **Website**: ${webUrl || baseUrl.replace('docs.', '')} +- **Documentation**: ${baseUrl} + +--- + +*This documentation was automatically generated from the Onlook documentation source files.* +`; + + return fullContent; +} + +export async function GET() { + const docsUrl = process.env.DOCS_URL ?? 'https://docs.onlook.com'; + const webUrl = process.env.APP_URL ?? 'https://onlook.com'; + + try { + const content = await getFullDocumentation(docsUrl, webUrl); + + return new Response(content, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Robots-Tag': 'llms-txt', + }, + }); + } catch (error) { + console.error('Error generating llms-full.txt:', error); + + // Fallback content if file reading fails + const fallbackContent = `# Onlook Documentation - Complete Reference + +> Comprehensive documentation for Onlook - the open-source visual editor for React apps. + +## Error + +Unable to generate complete documentation. Please visit ${docsUrl} for the latest documentation. + +## Basic Information + +Onlook is an open-source visual editor for React applications that allows designers to make live edits directly in the browser and generate clean code. + +### Key Features +- Visual editing of React components +- TailwindCSS integration +- AI-powered assistance +- Real-time code generation +- Component library support + +### Links +- Documentation: ${docsUrl} +- GitHub: https://github.com/onlook-dev/onlook +- Discord: https://discord.gg/hERDfFZCsH +`; + + return new Response(fallbackContent, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Robots-Tag': 'llms-txt', + }, + }); + } +} diff --git a/docs/src/app/llms.txt/route.ts b/docs/src/app/llms.txt/route.ts new file mode 100644 index 0000000000..0cbd2b08b5 --- /dev/null +++ b/docs/src/app/llms.txt/route.ts @@ -0,0 +1,88 @@ +interface LLMSSection { + title: string; + links: Array<[string, string]>; +} + +interface LLMSData { + title: string; + description: string; + sections: LLMSSection[]; +} + +function renderMarkdown(data: LLMSData): string { + let output = `# ${data.title}\n\n> ${data.description}\n\n`; + + for (const section of data.sections) { + output += `## ${section.title}\n\n`; + for (const [text, url] of section.links) { + output += `- [${text}](${url})\n`; + } + output += `\n`; + } + + return output; +} + +export function GET() { + const docsUrl = process.env.DOCS_URL ?? 'https://docs.onlook.com'; + + const llmsData: LLMSData = { + title: 'Onlook Documentation', + description: + 'Comprehensive documentation for Onlook - the open-source visual editor for React apps. Learn how to design directly in your live React app and generate clean code.', + sections: [ + { + title: 'Getting Started', + links: [ + ['Introduction', `${docsUrl}`], + ['First Project', `${docsUrl}/getting-started/first-project`], + ['UI Overview', `${docsUrl}/getting-started/ui-overview`], + ['Core Features', `${docsUrl}/getting-started/core-features`], + ], + }, + { + title: 'Tutorials', + links: [ + ['Importing Templates', `${docsUrl}/tutorials/importing-templates`], + ['Figma to Onlook', `${docsUrl}/tutorials/figma-to-onlook`], + ], + }, + { + title: 'Contributing', + links: [ + ['Developer Guide', `${docsUrl}/contributing/developers`], + ['Running Locally', `${docsUrl}/contributing/developers/running-locally`], + ['Architecture Overview', `${docsUrl}/contributing/developers/architecture`], + ['Development Appendix', `${docsUrl}/contributing/developers/appendix`], + ], + }, + { + title: 'Migrations', + links: [ + [ + 'Electron to Web Migration', + `${docsUrl}/migrations/electron-to-web-migration`, + ], + ], + }, + { + title: 'Support', + links: [ + ['FAQ', `${docsUrl}/faq`], + ['GitHub Repository', 'https://github.com/onlook-dev/onlook'], + ['Discord Community', 'https://discord.gg/hERDfFZCsH'], + ['Contact', 'mailto:contact@onlook.com'], + ], + }, + ], + }; + + const content = renderMarkdown(llmsData); + + return new Response(content, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'X-Robots-Tag': 'llms-txt', + }, + }); +} diff --git a/docs/src/app/robots.ts b/docs/src/app/robots.ts new file mode 100644 index 0000000000..bae6face53 --- /dev/null +++ b/docs/src/app/robots.ts @@ -0,0 +1,16 @@ +import type { MetadataRoute } from 'next'; + +const BASE_URL = process.env.APP_URL ?? 'https://docs.onlook.com'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + disallow: ['/api/', '/_next/', '/_vercel/'], + crawlDelay: 1, + }, + sitemap: `${BASE_URL}/sitemap.xml`, + host: BASE_URL, + }; +} diff --git a/docs/src/app/robots.txt/route.ts b/docs/src/app/robots.txt/route.ts deleted file mode 100644 index 3eaac7c17d..0000000000 --- a/docs/src/app/robots.txt/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Dynamic robots.txt using Next.js route handler -import { NextResponse } from 'next/server'; - -export function GET() { - const body = `User-agent: *\nAllow: /\nSitemap: https://docs.onlook.dev/sitemap.xml`; - return new NextResponse(body, { - headers: { - 'Content-Type': 'text/plain', - }, - }); -} diff --git a/docs/src/app/sitemap.ts b/docs/src/app/sitemap.ts new file mode 100644 index 0000000000..592846b79e --- /dev/null +++ b/docs/src/app/sitemap.ts @@ -0,0 +1,15 @@ +import type { MetadataRoute } from 'next'; + +export default function sitemap(): MetadataRoute.Sitemap { + const now = new Date(); + const BASE_URL = process.env.APP_URL ?? 'https://docs.onlook.com'; + + return [ + { + url: BASE_URL, + lastModified: now, + changeFrequency: 'daily', + priority: 1.0, + }, + ]; +}