diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ed6173..4b50b76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,10 +3,13 @@ ## Development Setup ```bash -# Clone the repo -git clone https://github.com/codercops/chatcops.git +# Fork the repo on GitHub, then clone your fork +git clone https://github.com//chatcops.git cd chatcops +# Add upstream remote +git remote add upstream https://github.com/codercops/chatcops.git + # Install dependencies pnpm install @@ -32,12 +35,44 @@ website/ - Marketing site + Starlight docs (Astro) ## Development Workflow -1. Create a branch: `git checkout -b feature/my-feature` -2. Make changes +1. Sync your fork's `dev` branch with upstream: + ```bash + git fetch upstream + git checkout dev + git merge upstream/dev + ``` +2. Work directly on your fork's `dev` branch — commit and push as you go 3. Run tests: `pnpm test` 4. Run typecheck: `pnpm -r typecheck` -5. Add a changeset: `pnpm changeset` -6. Open a PR +5. Add a changeset if your changes affect published packages: `pnpm changeset` +6. When ready, open a PR from your fork's `dev` → upstream `dev` + +### Branch Strategy + +- **`dev`** — default branch, all development happens here +- **`production`** — release branch, merged from `dev` when cutting a release +- PRs should target **`dev`** unless you're doing a production release + +### Releasing to Production + +When `dev` is ready to ship: + +1. Ensure all changes that affect published packages have changesets: + ```bash + pnpm changeset + ``` + This creates a changeset file describing the version bump (patch/minor/major) and a summary. Commit it to `dev`. + +2. Create a PR from `dev` → `production` and merge it. + +3. The **Release** workflow runs automatically on `production` push: + - Builds, typechecks, and tests everything + - If unreleased changesets exist, Changesets action opens a **"Version Packages"** PR on `production` that bumps versions and updates changelogs + - Merging that PR triggers the workflow again, which **publishes to npm** (via OIDC trusted publishing) + +4. The website is auto-deployed by the Vercel GitHub integration — no manual step needed. + +> **Note:** Only maintainers can merge into `production`. If you're a contributor, just make sure your PR to `dev` includes a changeset when needed. ## Widget Development diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..c4881cb --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,129 @@ +# @chatcops/core + +Core AI provider abstraction, tools, knowledge base, conversation management, analytics, and i18n for ChatCops. + +## Install + +```bash +npm install @chatcops/core +``` + +## Providers + +Unified interface for Claude, OpenAI, and Gemini. Supports streaming (AsyncGenerator) and sync modes. + +```typescript +import { createProvider } from '@chatcops/core'; + +const provider = await createProvider({ + type: 'claude', // 'claude' | 'openai' | 'gemini' + apiKey: process.env.ANTHROPIC_API_KEY, + model: 'claude-haiku-4-5-20251001', // optional +}); + +// Streaming +for await (const chunk of provider.chat({ + messages: [{ id: '1', role: 'user', content: 'Hello', timestamp: Date.now() }], + systemPrompt: 'You are a helpful assistant.', +})) { + process.stdout.write(chunk); +} + +// Sync +const response = await provider.chatSync({ + messages: [{ id: '1', role: 'user', content: 'Hello', timestamp: Date.now() }], + systemPrompt: 'You are a helpful assistant.', +}); +``` + +Default models: `claude-haiku-4-5-20251001` (Claude), `gpt-4o-mini` (OpenAI), `gemini-2.0-flash` (Gemini). + +## Tools + +Define tools the AI can call. Built-in `LeadCaptureTool` for collecting contact info. + +```typescript +import { LeadCaptureTool } from '@chatcops/core'; + +const leadTool = new LeadCaptureTool((lead) => { + console.log('Lead captured:', lead.name, lead.email); +}); +``` + +Custom tools implement the `ChatTool` interface: + +```typescript +import type { ChatTool, ToolResult } from '@chatcops/core'; + +const myTool: ChatTool = { + name: 'lookup_order', + description: 'Look up an order by ID', + parameters: { + orderId: { type: 'string', description: 'The order ID' }, + }, + required: ['orderId'], + async execute(input): Promise { + return { success: true, data: { status: 'shipped' } }; + }, +}; +``` + +## Knowledge Base + +Feed context to the AI with text chunks or FAQ pairs. + +```typescript +import { TextKnowledgeSource, FAQKnowledgeSource } from '@chatcops/core'; + +const text = new TextKnowledgeSource([ + 'ChatCops supports Claude, OpenAI, and Gemini.', + 'The widget is zero-dependency and uses Shadow DOM.', +]); + +const faq = new FAQKnowledgeSource([ + { question: 'What is ChatCops?', answer: 'An embeddable AI chat widget.' }, + { question: 'Is it free?', answer: 'Yes, MIT licensed.' }, +]); + +const context = await faq.getContext('is chatcops free'); +``` + +## Conversation Management + +```typescript +import { ConversationManager } from '@chatcops/core'; + +const manager = new ConversationManager({ maxMessages: 100 }); +const conversation = await manager.getOrCreate('conv-123'); +await manager.addMessage('conv-123', { + id: '1', role: 'user', content: 'Hello', timestamp: Date.now(), +}); +``` + +Plug in your own store by implementing `ConversationStore`. + +## Analytics + +```typescript +import { AnalyticsCollector } from '@chatcops/core'; + +const analytics = new AnalyticsCollector(); +analytics.track('message:sent', { conversationId: 'conv-123' }); +const stats = analytics.getStats(); +``` + +## i18n + +8 built-in locales: `en`, `es`, `hi`, `fr`, `de`, `ja`, `zh`, `ar`. + +```typescript +import { getLocale, getAvailableLocales } from '@chatcops/core'; + +const strings = getLocale('es'); +console.log(strings.welcomeMessage); // "Hola! Como puedo ayudarte hoy?" +console.log(getAvailableLocales()); // ['en', 'es', 'hi', 'fr', 'de', 'ja', 'zh', 'ar'] +``` + +## License + +MIT diff --git a/packages/server/README.md b/packages/server/README.md new file mode 100644 index 0000000..d218614 --- /dev/null +++ b/packages/server/README.md @@ -0,0 +1,164 @@ +# @chatcops/server + +Server-side handler and platform adapters for ChatCops. Handles streaming responses, rate limiting, webhooks, and analytics. + +## Install + +```bash +npm install @chatcops/server @chatcops/core +``` + +## Quick Start + +### Express + +```typescript +import express from 'express'; +import { chatcopsMiddleware } from '@chatcops/server'; + +const app = express(); +app.use(express.json()); + +app.post('/chat', chatcopsMiddleware({ + provider: { + type: 'claude', + apiKey: process.env.ANTHROPIC_API_KEY, + }, + systemPrompt: 'You are a helpful assistant for our website.', + cors: '*', +})); + +app.listen(3001); +``` + +### Vercel Edge + +```typescript +import { chatcopsVercelHandler } from '@chatcops/server'; + +const handler = chatcopsVercelHandler({ + provider: { type: 'openai', apiKey: process.env.OPENAI_API_KEY }, + systemPrompt: 'You are a helpful assistant.', + cors: '*', +}); + +export const POST = (req: Request) => handler(req); +export const OPTIONS = (req: Request) => handler(req); +export const config = { runtime: 'edge' }; +``` + +### Cloudflare Workers + +```typescript +import { chatcopsCloudflareHandler } from '@chatcops/server'; + +export default { + async fetch(request: Request, env: Env) { + const handler = chatcopsCloudflareHandler({ + provider: { type: 'claude', apiKey: env.ANTHROPIC_API_KEY }, + systemPrompt: 'You are a helpful assistant.', + cors: '*', + }); + return handler(request); + }, +}; +``` + +## Configuration + +```typescript +import { createChatHandler } from '@chatcops/server'; +import { LeadCaptureTool, FAQKnowledgeSource } from '@chatcops/core'; + +const handler = createChatHandler({ + // AI provider (required) + provider: { + type: 'claude', // 'claude' | 'openai' | 'gemini' + apiKey: 'sk-...', + model: 'claude-haiku-4-5-20251001', + }, + + // System prompt (required) + systemPrompt: 'You are a helpful support assistant.', + + // Tools (optional) + tools: [ + new LeadCaptureTool((lead) => { + console.log('New lead:', lead.email); + }), + ], + + // Knowledge base (optional) + knowledge: [ + new FAQKnowledgeSource([ + { question: 'What do you do?', answer: 'We build software.' }, + ]), + ], + + // Rate limiting (optional, default: 30 req/60s) + rateLimit: { maxRequests: 10, windowMs: 60_000 }, + + // Webhooks (optional) + webhooks: [ + { url: 'https://hooks.example.com/leads', events: ['lead:captured'] }, + ], + + // Analytics (optional) + analytics: true, + + // CORS origin (required) + cors: '*', +}); +``` + +## SSE Stream Format + +All adapters produce identical Server-Sent Events: + +``` +data: {"content":"Hello"} +data: {"content":" there!"} +data: {"done":true} +data: [DONE] +``` + +On error: + +``` +data: {"error":"rate_limit","retryAfter":42} +data: [DONE] +``` + +Error codes: `invalid_request`, `rate_limit`, `provider_error`, `internal_error`. + +## Rate Limiting + +In-memory, per-IP. Suitable for single-instance deployments. + +```typescript +import { RateLimiter } from '@chatcops/server'; + +const limiter = new RateLimiter({ maxRequests: 30, windowMs: 60_000 }); +const result = limiter.check('127.0.0.1'); +// { allowed: true } or { allowed: false, retryAfter: 42 } +``` + +## Webhooks + +HMAC-SHA256 signed, with exponential backoff retries (up to 3 attempts). + +```typescript +import { WebhookDispatcher } from '@chatcops/server'; + +const dispatcher = new WebhookDispatcher([ + { url: 'https://hooks.example.com/chat', events: ['*'], secret: 'my-secret' }, +]); + +await dispatcher.dispatch('lead:captured', { email: 'user@example.com' }); +``` + +Signature sent via `X-ChatCops-Signature` header. + +## License + +MIT diff --git a/packages/widget/README.md b/packages/widget/README.md new file mode 100644 index 0000000..f076ccb --- /dev/null +++ b/packages/widget/README.md @@ -0,0 +1,165 @@ +# @chatcops/widget + +Embeddable AI chatbot widget. Zero dependencies, Shadow DOM isolated, ~50KB gzipped. + +## Install + +### Script Tag (easiest) + +```html + +``` + +The widget auto-initializes from `data-*` attributes on `DOMContentLoaded`. + +### npm + +```bash +npm install @chatcops/widget +``` + +```typescript +import ChatCops from '@chatcops/widget'; + +ChatCops.init({ + apiUrl: 'https://your-api.com/chat', + theme: { accent: '#6366f1' }, +}); +``` + +## Modes + +### Popup (default) + +Floating action button (FAB) in the corner. Clicking it opens a chat panel. + +```typescript +ChatCops.init({ + apiUrl: '/chat', + mode: 'popup', // default + theme: { position: 'bottom-right' }, // or 'bottom-left' +}); +``` + +### Inline + +Renders directly inside a container element. No FAB, no welcome bubble. + +```typescript +ChatCops.init({ + apiUrl: '/chat', + mode: 'inline', + container: '#chat-container', // CSS selector or HTMLElement +}); +``` + +## Configuration + +```typescript +ChatCops.init({ + // Required + apiUrl: 'https://your-api.com/chat', + + // Display mode + mode: 'popup', // 'popup' | 'inline' + container: '#chat', // required for inline mode + + // Theme + theme: { + accent: '#6366f1', // primary color + textColor: '#ffffff', + bgColor: '#0a0a0a', + fontFamily: 'system-ui, sans-serif', + borderRadius: '12px', + position: 'bottom-right', // 'bottom-right' | 'bottom-left' + }, + + // Branding + branding: { + name: 'Support Bot', + avatar: 'https://example.com/avatar.png', + subtitle: 'Online', + }, + + // Messages + welcomeMessage: 'Hi! How can I help?', + welcomeBubble: { + text: 'Need help? Chat with us!', + delay: 3000, // ms before showing + showOnce: true, // per session + }, + placeholder: 'Type a message...', + + // Behavior + persistHistory: true, // localStorage, default: true + maxMessages: 50, // max persisted messages + pageContext: true, // send page URL/title to API + autoOpen: 5000, // auto-open after ms (popup only) + locale: 'en', + + // Callbacks + onOpen: () => {}, + onClose: () => {}, + onMessage: (msg) => {}, + onError: (err) => {}, +}); +``` + +## API + +```typescript +ChatCops.init(config) // Initialize the widget +ChatCops.open() // Open the chat panel (popup mode) +ChatCops.close() // Close the chat panel +ChatCops.destroy() // Remove widget and clean up +ChatCops.on(event, handler) // Subscribe to events +ChatCops.off(event, handler) // Unsubscribe +``` + +Events: `open`, `close`, `message`, `error`. + +## Script Tag Attributes + +All configuration can be set via `data-*` attributes: + +```html + +``` + +## Features + +- **Shadow DOM** - Fully isolated CSS/DOM, no conflicts with host site +- **SSE Streaming** - Real-time streaming responses via Server-Sent Events +- **Markdown** - Renders bold, italic, code blocks, links, and lists +- **Conversation Persistence** - localStorage for history, sessionStorage for session ID +- **Theming** - CSS custom properties, auto-derived surface colors from `bgColor` +- **Mobile Responsive** - Full-screen panel on screens below 768px +- **i18n** - Built-in locale strings with custom override support +- **Accessible** - ARIA labels, semantic HTML, keyboard navigation + +## License + +MIT diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07860bb..04821e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ importers: '@astrojs/react': specifier: ^4.4.2 version: 4.4.2(@types/node@25.3.3)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(jiti@2.6.1)(lightningcss@1.31.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(terser@5.46.0)(yaml@2.8.2) + '@astrojs/sitemap': + specifier: ^3.7.1 + version: 3.7.1 '@astrojs/starlight': specifier: ^0.34.0 version: 0.34.8(astro@5.18.0(@types/node@25.3.3)(@vercel/functions@2.2.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2)) @@ -174,8 +177,8 @@ packages: react: ^17.0.2 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 - '@astrojs/sitemap@3.7.0': - resolution: {integrity: sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA==} + '@astrojs/sitemap@3.7.1': + resolution: {integrity: sha512-IzQqdTeskaMX+QDZCzMuJIp8A8C1vgzMBp/NmHNnadepHYNHcxQdGLQZYfkbd2EbRXUfOS+UDIKx8sKg0oWVdw==} '@astrojs/starlight@0.34.8': resolution: {integrity: sha512-XuYz0TfCZhje2u1Q9FNtmTdm7/B9QP91RDI1VkPgYvDhSYlME3k8gwgcBMHnR9ASDo2p9gskrqe7t1Pub/qryg==} @@ -1350,12 +1353,12 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@17.0.45': - resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + '@types/node@25.3.3': resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} @@ -3233,9 +3236,9 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - sitemap@8.0.3: - resolution: {integrity: sha512-9Ew1tR2WYw8RGE2XLy7GjkusvYXy8Rg6y8TYuBuQMfIEdGcWoJpY2Wr5DzsEiL/TKCw56+YKTCCUHglorEYK+A==} - engines: {node: '>=14.0.0', npm: '>=6.0.0'} + sitemap@9.0.1: + resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==} + engines: {node: '>=20.19.5', npm: '>=10.8.2'} hasBin: true slash@3.0.0: @@ -3428,6 +3431,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -3742,6 +3748,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3835,17 +3844,17 @@ snapshots: - tsx - yaml - '@astrojs/sitemap@3.7.0': + '@astrojs/sitemap@3.7.1': dependencies: - sitemap: 8.0.3 + sitemap: 9.0.1 stream-replace-string: 2.0.0 - zod: 3.25.76 + zod: 4.3.6 '@astrojs/starlight@0.34.8(astro@5.18.0(@types/node@25.3.3)(@vercel/functions@2.2.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2))': dependencies: '@astrojs/markdown-remark': 6.3.10 '@astrojs/mdx': 4.3.13(astro@5.18.0(@types/node@25.3.3)(@vercel/functions@2.2.13)(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2)) - '@astrojs/sitemap': 3.7.0 + '@astrojs/sitemap': 3.7.1 '@pagefind/default-ui': 1.4.0 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 @@ -4968,12 +4977,14 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@17.0.45': {} - '@types/node@18.19.130': dependencies: undici-types: 5.26.5 + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + '@types/node@25.3.3': dependencies: undici-types: 7.18.2 @@ -7487,9 +7498,9 @@ snapshots: sisteransi@1.0.5: {} - sitemap@8.0.3: + sitemap@9.0.1: dependencies: - '@types/node': 17.0.45 + '@types/node': 24.12.0 '@types/sax': 1.2.7 arg: 5.0.2 sax: 1.5.0 @@ -7657,6 +7668,8 @@ snapshots: undici-types@5.26.5: {} + undici-types@7.16.0: {} + undici-types@7.18.2: {} unified@11.0.5: @@ -7932,4 +7945,6 @@ snapshots: zod@3.25.76: {} + zod@4.3.6: {} + zwitch@2.0.4: {} diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 2b82d17..7a0355c 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; +import sitemap from '@astrojs/sitemap'; import react from '@astrojs/react'; import tailwindcss from '@tailwindcss/vite'; import vercel from '@astrojs/vercel'; @@ -13,6 +14,7 @@ export default defineConfig({ }, integrations: [ react(), + sitemap(), starlight({ title: 'ChatCops', description: 'Universal embeddable AI chatbot widget documentation', @@ -46,6 +48,28 @@ export default defineConfig({ defer: true, }, }, + // OG Image + { + tag: 'meta', + attrs: { + property: 'og:image', + content: 'https://og.codercops.com/api/og?template=saas-gradient-mesh&title=ChatCops+Docs&description=Universal+embeddable+AI+chatbot+widget+documentation&color1=%236366f1&color2=%23ec4899&color3=%2306b6d4&textColor=%23FFFFFF', + }, + }, + { tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } }, + { tag: 'meta', attrs: { property: 'og:image:height', content: '630' } }, + { + tag: 'meta', + attrs: { + name: 'twitter:image', + content: 'https://og.codercops.com/api/og?template=saas-gradient-mesh&title=ChatCops+Docs&description=Universal+embeddable+AI+chatbot+widget+documentation&color1=%236366f1&color2=%23ec4899&color3=%2306b6d4&textColor=%23FFFFFF', + }, + }, + // SEO + { tag: 'meta', attrs: { property: 'og:site_name', content: 'ChatCops' } }, + { tag: 'meta', attrs: { property: 'og:locale', content: 'en_US' } }, + { tag: 'meta', attrs: { name: 'twitter:site', content: '@codercops' } }, + { tag: 'meta', attrs: { name: 'theme-color', content: '#6366f1' } }, ], customCss: [ '@fontsource/inter/400.css', diff --git a/website/package.json b/website/package.json index d95dd38..67584e7 100644 --- a/website/package.json +++ b/website/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@astrojs/react": "^4.4.2", + "@astrojs/sitemap": "^3.7.1", "@astrojs/starlight": "^0.34.0", "@astrojs/vercel": "^9.0.4", "@chatcops/core": "^0.2.0", diff --git a/website/public/apple-touch-icon.png b/website/public/apple-touch-icon.png new file mode 100644 index 0000000..02c47bf Binary files /dev/null and b/website/public/apple-touch-icon.png differ diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 0000000..2a1c162 --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://chat.codercops.com/sitemap-index.xml diff --git a/website/src/layouts/Landing.astro b/website/src/layouts/Landing.astro index 424fc57..cc915c8 100644 --- a/website/src/layouts/Landing.astro +++ b/website/src/layouts/Landing.astro @@ -9,14 +9,18 @@ import '../styles/global.css'; interface Props { title: string; description: string; + ogImage?: string; } -const { title, description } = Astro.props; +const { title, description, ogImage } = Astro.props; const canonicalURL = new URL(Astro.url.pathname, Astro.site); + +const defaultOgImage = `https://og.codercops.com/api/og?template=saas-gradient-mesh&title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}&color1=%236366f1&color2=%23ec4899&color3=%2306b6d4&textColor=%23FFFFFF`; +const ogImageUrl = ogImage ?? defaultOgImage; --- - + @@ -31,11 +35,58 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); + + + + + + + + + + + + + + + + diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 13b2bd0..3690c27 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -15,6 +15,7 @@ import SupportBanner from '../components/landing/SupportBanner.astro';