Initial commit: AI Ops Templates repository

- Schema.org JSON-LD templates (product, event, local-business, faq)
- Brand, UI, SEO, and decision guide rules
- Working code snippets (vendor-card, schema-inject, deploy-webhook)
- JSON schemas for project config validation
- Client presets (slc-bride, default)
- Self-update protocol with changelog tracking

Made-with: Cursor
This commit is contained in:
DMleadgen 2026-03-06 16:03:31 -07:00
commit 3cb8d3cb3f
24 changed files with 2202 additions and 0 deletions

View file

@ -0,0 +1,128 @@
# AI Context Update Rule
This rule defines when and how context files must be updated to prevent drift.
---
## MANDATORY: Update Context After These Events
When ANY of the following occur, you MUST update the relevant context files BEFORE completing the task:
### 1. New Feature Added
**Update**: Client's SKILL.md with new patterns
**File**: `.cursor/skills/{{CLIENT}}/SKILL.md`
**Section**: Add to "Recent Changes" and relevant feature sections
### 2. Brand Colors/Fonts Changed
**Update**: Presets file
**File**: `ai-ops-templates/presets/{{CLIENT}}.json`
**Section**: `brand.colors` or `brand.fonts`
### 3. New API Endpoint Created
**Update**: Project config and context manifest
**Files**:
- `.ai-cli.json` under `apis` section
- `.ai-context.json` under `api_endpoints`
### 4. New Database Table/Column
**Update**: Context manifest and relevant skill file
**Files**:
- `.ai-context.json` under `database_schema.tables`
- `.cursor/skills/{{CLIENT}}/SKILL.md` under "Database Schema"
### 5. New Component Pattern
**Update**: Add working example to snippets
**File**: `ai-ops-templates/snippets/{{COMPONENT_NAME}}.tsx`
**Include**: Full working code with usage comments
### 6. Bug Fix Revealed New Pattern
**Update**: Common fixes documentation
**File**: `ai-ops-templates/rules/common-fixes.md`
**Include**: Symptom, cause, solution, and code example
### 7. New MCP Tool Added
**Update**: Decision guide
**File**: `ai-ops-templates/rules/ai-decision-guide.md`
**Section**: "Tool Selection Quick Reference"
---
## Update Command Pattern
After making changes, ALWAYS ask the user:
> "Should I update the AI context files to reflect these changes?"
If **yes**, update:
1. The specific file that changed
2. The `CHANGELOG.md` with date and summary
3. The `_meta.last_updated` timestamp in the changed file
4. Any dependent files that reference the changed file
---
## Change Log Format
Every context file must include a `_meta` section:
```json
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"updated_by": "ai-agent",
"change_summary": "Description of what changed"
}
}
```
For Markdown files, use this footer format:
```markdown
---
## Context Metadata (Auto-Updated)
_last_updated: 2026-03-06
_version: 1.0.0
_synced_from: git.abundancepartners.app/abundance/ai-ops-templates
```
---
## Files That Require _meta Sections
| File Type | _meta Required |
|-----------|----------------|
| JSON configs | Yes |
| JSON schemas | Yes |
| TypeScript snippets | No (use comments) |
| Shell scripts | No (use comments) |
| Markdown files | Yes (footer format) |
---
## Example Update Workflow
```
User: "Add a new vendor search feature"
AI Actions:
1. Implement the feature
2. Ask: "Should I update the AI context files?"
3. If yes:
a. Add to snippets/vendor-search.tsx
b. Update .ai-context.json with new API endpoint
c. Update SKILL.md with new pattern
d. Update CHANGELOG.md
e. Update last-sync.json timestamps
```
---
## Never Skip Context Updates
Context that isn't updated becomes actively harmful:
- Wrong patterns get repeated
- Time is wasted re-explaining the same things
- AI hallucinations increase
Always update context. It takes 30 seconds but saves hours later.

108
README.md Normal file
View file

@ -0,0 +1,108 @@
# AI Ops Templates
A curated repository of templates, rules, snippets, and presets for AI-assisted development across all Abundance client projects.
## Purpose
This repository provides structured context that enables AI agents (like Cursor's Composer) to:
- Build features 10x faster with accurate brand/style context
- Follow established patterns without re-explanation
- Maintain consistency across all client projects
- Self-update context to prevent drift
## Structure
```
ai-ops-templates/
├── templates/ # Schema.org JSON-LD templates
│ ├── schema-product.json
│ ├── schema-event.json
│ ├── schema-local-business.json
│ └── schema-faq.json
├── rules/ # Development rules and guidelines
│ ├── tailwind-brand.json
│ ├── ui-fixes.json
│ ├── seo-rules.md
│ ├── common-fixes.md
│ └── ai-decision-guide.md
├── snippets/ # Reusable code components
│ ├── add-internal-link.tsx
│ ├── alt-text-placeholder.go
│ ├── breadcrumb.tsx
│ ├── vendor-card.tsx
│ ├── schema-inject.ts
│ └── deploy-webhook.sh
├── schemas/ # JSON schemas for validation
│ ├── ai-cli.schema.json
│ └── ai-context.schema.json
├── presets/ # Client brand presets
│ ├── slc-bride.json
│ └── default.json
├── context/ # Context tracking
│ ├── CHANGELOG.md
│ └── last-sync.json
├── skill-templates/ # Templates for client SKILL.md files
│ └── client-skill.template.md
└── .cursor/rules/ # Cursor-specific rules
└── ai-context-update.md
```
## Usage
### For AI Agents
1. Read `.ai-context.json` in the project root first
2. Reference `presets/<client>.json` for brand rules
3. Use `snippets/` for component patterns
4. Check `rules/ai-decision-guide.md` for tool selection
5. Update context after making changes (see `.cursor/rules/ai-context-update.md`)
### For Humans
1. Copy `presets/default.json` to create new client presets
2. Use `skill-templates/client-skill.template.md` for new client SKILL.md files
3. Update `CHANGELOG.md` when making changes
4. Run `context/last-sync.json` updates via the MCP tool
## Per-Project Setup
Each client project needs:
1. **`.ai-cli.json`** - Project configuration (use `schemas/ai-cli.schema.json`)
2. **`.ai-context.json`** - Context manifest (use `schemas/ai-context.schema.json`)
3. **`.cursor/skills/{client}/SKILL.md`** - Client-specific skill file
## Self-Update Protocol
This repository uses a self-update protocol to prevent context drift. See `.cursor/rules/ai-context-update.md` for details.
Key triggers for updates:
- New feature added
- Brand changes
- New API endpoints
- Database schema changes
- Bug fixes that reveal patterns
## Adding New Clients
1. Create preset: `presets/{client-slug}.json`
2. Create skill file: `.cursor/skills/{client-slug}/SKILL.md`
3. Add to project: `.ai-cli.json` and `.ai-context.json`
4. Update this README if needed
## Repository URL
```
https://git.abundancepartners.app/abundance/ai-ops-templates
```
Raw file access:
```
https://git.abundancepartners.app/abundance/ai-ops-templates/raw/main/{path}
```
## Version
- **Version**: 1.0.0
- **Last Updated**: 2026-03-06
- **Maintained By**: AI Agent (auto-updated)

52
context/CHANGELOG.md Normal file
View file

@ -0,0 +1,52 @@
# AI Context Changelog
This file tracks all changes to the ai-ops-templates repository. Updates are logged automatically when context files are modified.
---
## Format
```
## [YYYY-MM-DD] - Version X.X.X
### Added
- New features or files
### Changed
- Modifications to existing files
### Fixed
- Bug fixes or corrections
### Context Updates
- Changes triggered by project work
```
---
## [2026-03-06] - Version 1.0.0
### Added
- Initial repository structure
- templates/ - Schema.org JSON-LD templates (product, event, local-business, faq)
- rules/ - Brand, UI, SEO, and decision guide rules
- snippets/ - Reusable code components and utilities
- schemas/ - JSON schemas for configuration validation
- presets/ - Client brand presets (slc-bride, default)
- context/ - Changelog and sync tracking
- skill-templates/ - Client SKILL.md template
### Architecture
- Self-update protocol implemented
- Meta sections in all JSON files for version tracking
- Decision guide for AI tool/pattern selection
---
<!--
To add new entries:
1. Add new section with date and version
2. List changes in appropriate category
3. Update last-sync.json timestamp
4. Commit changes
-->

42
context/last-sync.json Normal file
View file

@ -0,0 +1,42 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Timestamp tracking for context synchronization"
},
"last_sync": "2026-03-06T15:52:00Z",
"repository_url": "https://git.abundancepartners.app/abundance/ai-ops-templates",
"branch": "main",
"commit_hash": "initial",
"files": {
"templates": {
"schema-product.json": "2026-03-06T15:52:00Z",
"schema-event.json": "2026-03-06T15:52:00Z",
"schema-local-business.json": "2026-03-06T15:52:00Z",
"schema-faq.json": "2026-03-06T15:52:00Z"
},
"rules": {
"tailwind-brand.json": "2026-03-06T15:52:00Z",
"ui-fixes.json": "2026-03-06T15:52:00Z",
"seo-rules.md": "2026-03-06T15:52:00Z",
"common-fixes.md": "2026-03-06T15:52:00Z",
"ai-decision-guide.md": "2026-03-06T15:52:00Z"
},
"snippets": {
"add-internal-link.tsx": "2026-03-06T15:52:00Z",
"alt-text-placeholder.go": "2026-03-06T15:52:00Z",
"breadcrumb.tsx": "2026-03-06T15:52:00Z",
"vendor-card.tsx": "2026-03-06T15:52:00Z",
"schema-inject.ts": "2026-03-06T15:52:00Z",
"deploy-webhook.sh": "2026-03-06T15:52:00Z"
},
"presets": {
"slc-bride.json": "2026-03-06T15:52:00Z",
"default.json": "2026-03-06T15:52:00Z"
},
"schemas": {
"ai-cli.schema.json": "2026-03-06T15:52:00Z",
"ai-context.schema.json": "2026-03-06T15:52:00Z"
}
}
}

88
presets/default.json Normal file
View file

@ -0,0 +1,88 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Default brand preset - neutral starting point for new clients"
},
"client": "default",
"site_url": "{{SITE_URL}}",
"brand": {
"name": "{{BRAND_NAME}}",
"tagline": "{{TAGLINE}}",
"colors": {
"primary": {
"50": "#f0f9ff",
"100": "#e0f2fe",
"200": "#bae6fd",
"300": "#7dd3fc",
"400": "#38bdf8",
"500": "#0ea5e9",
"600": "#0284c7",
"700": "#0369a1",
"800": "#075985",
"900": "#0c4a6e",
"950": "#082f49"
},
"secondary": {
"50": "#f8fafc",
"100": "#f1f5f9",
"200": "#e2e8f0",
"300": "#cbd5e1",
"400": "#94a3b8",
"500": "#64748b",
"600": "#475569",
"700": "#334155",
"800": "#1e293b",
"900": "#0f172a",
"950": "#020617"
},
"background": "#ffffff",
"foreground": "#0f172a",
"muted": "#f1f5f9",
"mutedForeground": "#64748b",
"card": "#ffffff",
"cardForeground": "#0f172a",
"popover": "#ffffff",
"popoverForeground": "#0f172a",
"border": "#e2e8f0",
"input": "#e2e8f0",
"ring": "#0ea5e9",
"destructive": "#ef4444",
"destructiveForeground": "#fafafa"
},
"fonts": {
"heading": "Inter",
"body": "Inter",
"mono": "JetBrains Mono"
},
"borderRadius": {
"sm": "0.25rem",
"md": "0.5rem",
"lg": "0.75rem",
"xl": "1rem"
},
"spacing": {
"section": "4rem",
"container": "1280px"
}
},
"seo": {
"title_template": "{page} | {{BRAND_NAME}}",
"default_title": "{{SEO_TITLE}}",
"default_description": "{{SEO_DESCRIPTION}}",
"schema_types": ["LocalBusiness", "Product"],
"social": {
"facebook": "{{FACEBOOK_URL}}",
"instagram": "{{INSTAGRAM_URL}}",
"twitter": "{{TWITTER_URL}}"
}
},
"infrastructure": {
"server": "{{SERVER_NAME}}",
"server_ip": "{{SERVER_IP}}",
"database": "{{DATABASE_NAME}}",
"coolify_url": "https://app.abundancepartners.app"
},
"features": {},
"routes": {}
}

100
presets/slc-bride.json Normal file
View file

@ -0,0 +1,100 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "SLC Bride brand preset - Salt Lake City wedding vendor directory"
},
"client": "slc-bride",
"site_url": "https://saltlakebride.com",
"brand": {
"name": "Salt Lake Bride",
"tagline": "Your Complete Salt Lake City Wedding Resource",
"colors": {
"primary": {
"50": "#fdf2f8",
"100": "#fce7f3",
"200": "#fbcfe8",
"300": "#f9a8d4",
"400": "#f472b6",
"500": "#ec4899",
"600": "#db2777",
"700": "#be185d",
"800": "#9d174d",
"900": "#831843",
"950": "#500724"
},
"secondary": {
"50": "#faf5ff",
"100": "#f3e8ff",
"200": "#e9d5ff",
"300": "#d8b4fe",
"400": "#c084fc",
"500": "#a855f7",
"600": "#9333ea",
"700": "#7e22ce",
"800": "#6b21a8",
"900": "#581c87",
"950": "#3b0764"
},
"background": "#ffffff",
"foreground": "#1a1a2e",
"muted": "#f4f4f5",
"mutedForeground": "#71717a",
"card": "#ffffff",
"cardForeground": "#1a1a2e",
"popover": "#ffffff",
"popoverForeground": "#1a1a2e",
"border": "#e4e4e7",
"input": "#e4e4e7",
"ring": "#ec4899",
"destructive": "#ef4444",
"destructiveForeground": "#fafafa"
},
"fonts": {
"heading": "Playfair Display",
"body": "Inter",
"mono": "JetBrains Mono"
},
"borderRadius": {
"sm": "0.25rem",
"md": "0.5rem",
"lg": "0.75rem",
"xl": "1rem"
},
"spacing": {
"section": "4rem",
"container": "1280px"
}
},
"seo": {
"title_template": "{page} | Salt Lake Bride",
"default_title": "Salt Lake Bride - Your Complete Salt Lake City Wedding Resource",
"default_description": "Find the best wedding vendors, venues, and resources in Salt Lake City. Browse photographers, florists, caterers, and more for your perfect Utah wedding.",
"schema_types": ["LocalBusiness", "Event", "FAQPage"],
"social": {
"facebook": "https://facebook.com/saltlakebride",
"instagram": "https://instagram.com/saltlakebride",
"pinterest": "https://pinterest.com/saltlakebride"
}
},
"infrastructure": {
"server": "slcbride",
"server_ip": "89.117.22.126",
"database": "slcbride",
"coolify_url": "https://app.abundancepartners.app"
},
"features": {
"vendor_directory": true,
"venue_listings": true,
"blog": true,
"real_weddings": true,
"planning_tools": true
},
"routes": {
"vendors": "/vendors",
"venues": "/venues",
"blog": "/blog",
"real_weddings": "/real-weddings",
"planning": "/planning"
}
}

166
rules/ai-decision-guide.md Normal file
View file

@ -0,0 +1,166 @@
# AI Decision Guide
This document maps common scenarios to the correct tools and patterns.
---
## Adding a New Page
1. Check `.ai-context.json` for route patterns
2. Read brand preset from `presets/<client>.json`
3. Copy structure from `snippets/` similar pages
4. Add schema from `templates/` if SEO-relevant
5. Follow heading hierarchy from `rules/seo-rules.md`
---
## Deploying Changes
### ALWAYS follow this sequence:
```bash
# Step 1: Deploy to staging
deploy_push({ target: "staging", app_id: "<from .ai-cli.json>" })
# Step 2: Verify on staging URL
# Check: functionality, styling, data loading
# Step 3: If successful, deploy to prod
deploy_push({ target: "prod", app_id: "<from .ai-cli.json>" })
```
### Never skip staging deployment for production changes.
---
## Fixing Brand/Style Issues
1. Read `presets/<client>.json` for brand rules
2. Check `rules/ui-fixes.json` for patterns
3. Use shadcn MCP for component updates
4. Verify dark mode support if applicable
### Common Patterns:
| Issue | Solution |
|-------|----------|
| Colors not matching | Check CSS variables match preset |
| Font not loading | Verify Google Fonts import |
| Spacing inconsistent | Use Tailwind spacing scale |
---
## Adding Structured Data
1. Fetch template: `seo_add_schema({ template: "product", data: {...} })`
2. Template pulled from Forgejo with current brand
3. Inject into page `<head>` as JSON-LD script tag
4. Validate with Google Rich Results Test
### Template Selection:
| Content Type | Template |
|--------------|----------|
| Product/Service | `schema-product.json` |
| Event | `schema-event.json` |
| Business Listing | `schema-local-business.json` |
| FAQ Section | `schema-faq.json` |
---
## Database Operations
1. Read `.ai-cli.json` for database connection info
2. Use `supabase_query` with parameterized queries only
3. **NEVER** use string concatenation for queries
4. Check RLS policies if query fails
### Safe Query Pattern:
```javascript
// Correct
supabase_query({
query: "SELECT * FROM vendors WHERE category = $1 AND active = $2",
params: ["photography", true]
})
// WRONG - Never do this
supabase_query({
query: `SELECT * FROM vendors WHERE category = '${category}'`
})
```
---
## Creating New Components
1. Check `snippets/` for existing similar components
2. Follow brand preset for styling
3. Use shadcn/ui as base when possible
4. Add accessibility attributes (see `rules/ui-fixes.json`)
5. After creation, update context:
```
context_update({
file: "snippets/components.md",
section: "new-components",
content: { name: "ComponentName", path: "..." }
})
```
---
## Handling Errors
### Step 1: Identify Error Type
| Error Type | Check |
|------------|-------|
| Type Error | TypeScript interfaces, props |
| Runtime Error | Console logs, stack trace |
| Build Error | Dependencies, env vars |
| Database Error | RLS policies, query syntax |
### Step 2: Check Common Fixes
Look in `rules/common-fixes.md` for known solutions
### Step 3: If New Pattern Found
1. Document the fix
2. Update `rules/common-fixes.md`
3. Run `context_update` to log the change
---
## Context Update Required When
| Event | Action |
|-------|--------|
| New feature added | Update client's SKILL.md |
| Bug fix discovered | Add to `rules/common-fixes.md` |
| Brand changed | Update `presets/<client>.json` |
| New component pattern | Add to `snippets/` |
| New API endpoint | Add to `.ai-cli.json` |
| Database schema change | Update relevant schemas |
### After ANY context update:
1. Update `context/CHANGELOG.md`
2. Update `last_updated` in `_meta` section
3. Notify user if major change
---
## Tool Selection Quick Reference
| Task | Tool |
|------|------|
| Check MCP connection | `ping()` |
| Deploy app | `deploy_push()` |
| Add SEO schema | `seo_add_schema()` |
| Query database | `supabase_query()` |
| Update context files | `context_update()` |
| UI components/themes | shadcn MCP |
| File operations | filesystem MCP |
---
## Emergency Contacts
If unable to resolve:
1. Check server logs: `abundance docker logs <server> <container> -f --tail 100`
2. Check health: `abundance health check`
3. Escalate with full error context

208
rules/common-fixes.md Normal file
View file

@ -0,0 +1,208 @@
# Common Fixes
This document tracks recurring issues and their solutions. Update when new patterns are discovered.
---
## React/Next.js Issues
### Hydration Mismatch
**Symptom:** "Hydration failed because the server rendered HTML didn't match the client"
**Solution:**
```tsx
// Use useEffect for client-only rendering
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
```
### Event Handler Type Errors
**Symptom:** Type errors on onClick handlers
**Solution:**
```tsx
// Always type the event parameter
onClick={(e: React.MouseEvent<HTMLButtonElement>) => handleClick(e)}
// For forms
onSubmit={(e: React.FormEvent<HTMLFormElement>) => handleSubmit(e)}
```
### State Not Updating
**Symptom:** State updates not reflecting in UI
**Solution:**
```tsx
// For arrays/objects, always create new references
setItems([...items, newItem])
setUser({ ...user, name: 'New Name' })
```
---
## Tailwind CSS Issues
### Dark Mode Not Working
**Symptom:** Dark mode classes not applying
**Solution:**
```js
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'media'
}
```
### Custom Colors Not Applying
**Symptom:** Custom color classes not generating
**Solution:**
```js
// tailwind.config.js - extend, don't replace
module.exports = {
theme: {
extend: {
colors: {
primary: 'var(--primary)',
}
}
}
}
```
---
## Form Issues
### Zod Validation Not Triggering
**Symptom:** Form submits with invalid data
**Solution:**
```tsx
// Ensure resolver is passed correctly
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: 'onChange', // or 'onBlur', 'all'
})
```
### React Hook Form Not Re-rendering
**Symptom:** Form values update but UI doesn't
**Solution:**
```tsx
// Use watch or useWatch for reactive updates
const watchedValue = form.watch('fieldName')
```
---
## Database/Supabase Issues
### RLS Policy Blocking Queries
**Symptom:** "new row violates row-level security policy"
**Solution:**
```sql
-- Check policies on table
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- Add policy for authenticated users
CREATE POLICY "Users can insert own data"
ON your_table FOR INSERT
WITH CHECK (auth.uid() = user_id);
```
### Connection Pool Exhausted
**Symptom:** "remaining connection slots are reserved"
**Solution:**
- Use Supabase connection pooling (port 6543)
- Close connections properly
- Use transaction mode for serverless
---
## Deployment Issues
### Build Fails on Server
**Symptom:** Works locally but fails in production
**Common Causes:**
1. Environment variables missing
2. Node version mismatch
3. Missing devDependencies in production
**Solution:**
```bash
# Check Node version
node --version
# Verify env vars are set
# Add to Coolify environment variables
```
### Container Health Check Failing
**Symptom:** Container restarts repeatedly
**Solution:**
```yaml
# docker-compose.yaml - increase health check interval
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
```
---
## Type Errors
### "Type X is not assignable to type Y"
**Symptom:** TypeScript errors in component props
**Solution:**
```tsx
// Use proper type imports
import type { SomeType } from './types'
// For component props
interface ComponentProps {
data: SomeType
onClick?: () => void
}
```
---
## Performance Issues
### Slow Page Load
**Diagnosis Steps:**
1. Check Network tab for large payloads
2. Use Lighthouse for audit
3. Check for unnecessary re-renders
**Quick Fixes:**
```tsx
// Memoize expensive computations
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])
// Memoize components
const MemoizedComponent = memo(ExpensiveComponent)
// Code split
const HeavyComponent = lazy(() => import('./HeavyComponent'))
```
---
## Update Log
| Date | Issue | Solution Added |
|------|-------|----------------|
| 2026-03-06 | Initial | Created document |
**To add new fixes:** Update this file and increment version in _meta

115
rules/seo-rules.md Normal file
View file

@ -0,0 +1,115 @@
# SEO Rules
## Internal Linking
### Link Patterns
- Link to relevant vendor/category pages from content
- Use descriptive anchor text (avoid "click here")
- Link to related services/products where contextually relevant
- Maximum 100 internal links per page
### URL Structure
- Use kebab-case for URLs: `/vendors/wedding-photography`
- Include location where relevant: `/venues/salt-lake-city/hotels`
- Avoid query parameters for canonical content
### Anchor Text Guidelines
- Include target keywords naturally
- Match the linked page's primary topic
- Keep under 60 characters
- Examples:
- Good: "wedding photography packages"
- Bad: "click here for more info"
---
## Alt Text
### Format
```
[Subject] + [Context/Action] + [Relevance to page content]
```
### Examples
- Product image: "White wedding dress with lace details at Salt Lake Bride bridal expo"
- Vendor photo: "Portrait of wedding photographer Sarah Smith at outdoor ceremony"
- Venue image: "Mountain view ceremony space at The Grand America Hotel Salt Lake City"
### Rules
1. Be descriptive but concise (125 characters max)
2. Include relevant keywords naturally
3. Don't start with "Image of" or "Picture of"
4. Describe what's visible, not decorative elements
5. For decorative images, use empty alt=""
---
## Meta Descriptions
### Format
- 150-160 characters
- Include primary keyword near beginning
- Include call-to-action when appropriate
- Match page content accurately
### Template
```
Discover {{SERVICE}} in {{LOCATION}}. {{UNIQUE_VALUE_PROPOSITION}}. Browse {{COUNT}} options on {{SITE_NAME}}.
```
---
## Schema Markup
### Required Schemas by Page Type
| Page Type | Required Schema |
|-----------|----------------|
| Vendor listing | LocalBusiness |
| Product page | Product |
| Event page | Event |
| FAQ page | FAQPage |
| Article | Article |
| Homepage | Organization + WebSite |
### Implementation
- Use JSON-LD format in `<head>`
- Include all required properties
- Validate with Google Rich Results Test
---
## Heading Hierarchy
### Rules
1. One H1 per page (main page title)
2. H2 for major sections
3. H3 for subsections
4. Never skip heading levels
5. Include keywords naturally in headings
### Example Structure
```
H1: Wedding Photographers in Salt Lake City
H2: Top Rated Photographers
H3: Portrait Photography
H3: Documentary Style
H2: How to Choose Your Photographer
H3: Budget Considerations
H3: Style Matching
```
---
## Image Optimization
### Technical Requirements
- Format: WebP with JPEG fallback
- Max file size: 200KB for hero, 100KB for thumbnails
- Use responsive images with srcset
- Lazy load images below the fold
### Naming Convention
```
[category]-[subject]-[detail]-[location].webp
Example: venue-ballroom-crystal-chandeliers-salt-lake-city.webp
```

68
rules/tailwind-brand.json Normal file
View file

@ -0,0 +1,68 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Tailwind brand configuration template - customize per client"
},
"colors": {
"primary": {
"50": "{{PRIMARY_50}}",
"100": "{{PRIMARY_100}}",
"200": "{{PRIMARY_200}}",
"300": "{{PRIMARY_300}}",
"400": "{{PRIMARY_400}}",
"500": "{{PRIMARY_500}}",
"600": "{{PRIMARY_600}}",
"700": "{{PRIMARY_700}}",
"800": "{{PRIMARY_800}}",
"900": "{{PRIMARY_900}}",
"950": "{{PRIMARY_950}}"
},
"secondary": {
"50": "{{SECONDARY_50}}",
"100": "{{SECONDARY_100}}",
"200": "{{SECONDARY_200}}",
"300": "{{SECONDARY_300}}",
"400": "{{SECONDARY_400}}",
"500": "{{SECONDARY_500}}",
"600": "{{SECONDARY_600}}",
"700": "{{SECONDARY_700}}",
"800": "{{SECONDARY_800}}",
"900": "{{SECONDARY_900}}",
"950": "{{SECONDARY_950}}"
},
"background": "{{BACKGROUND}}",
"foreground": "{{FOREGROUND}}",
"muted": "{{MUTED}}",
"mutedForeground": "{{MUTED_FOREGROUND}}",
"card": "{{CARD}}",
"cardForeground": "{{CARD_FOREGROUND}}",
"popover": "{{POPOVER}}",
"popoverForeground": "{{POPOVER_FOREGROUND}}",
"border": "{{BORDER}}",
"input": "{{INPUT}}",
"ring": "{{RING}}",
"destructive": "{{DESTRUCTIVE}}",
"destructiveForeground": "{{DESTRUCTIVE_FOREGROUND}}"
},
"fonts": {
"heading": "{{HEADING_FONT}}",
"body": "{{BODY_FONT}}",
"mono": "{{MONO_FONT}}"
},
"borderRadius": {
"sm": "{{RADIUS_SM}}",
"md": "{{RADIUS_MD}}",
"lg": "{{RADIUS_LG}}",
"xl": "{{RADIUS_XL}}"
},
"spacing": {
"section": "{{SECTION_SPACING}}",
"container": "{{CONTAINER_MAX_WIDTH}}"
},
"shadows": {
"sm": "{{SHADOW_SM}}",
"md": "{{SHADOW_MD}}",
"lg": "{{SHADOW_LG}}"
}
}

58
rules/ui-fixes.json Normal file
View file

@ -0,0 +1,58 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Common UI fixes and patterns for consistent implementation"
},
"eventHandling": {
"description": "Use onClick handlers with proper event typing",
"pattern": "onClick={(e: React.MouseEvent<HTMLButtonElement>) => handleClick(e)}",
"avoid": "onClick={handleClick()} or onClick={() => handleClick}"
},
"darkMode": {
"description": "Use CSS variables for dark mode support",
"pattern": "className=\"bg-background text-foreground dark:bg-background-dark dark:text-foreground-dark\"",
"cssVariables": [
"--background",
"--foreground",
"--muted",
"--muted-foreground",
"--card",
"--card-foreground"
],
"tailwindConfig": {
"darkMode": "class"
}
},
"responsiveDesign": {
"mobileFirst": true,
"breakpoints": {
"sm": "640px",
"md": "768px",
"lg": "1024px",
"xl": "1280px",
"2xl": "1536px"
},
"pattern": "className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3\""
},
"accessibility": {
"description": "Required accessibility attributes",
"rules": [
"Always include alt text for images",
"Use aria-label for icon-only buttons",
"Ensure color contrast ratio >= 4.5:1",
"Use semantic HTML elements",
"Include focus states for interactive elements"
]
},
"formHandling": {
"description": "React Hook Form patterns with Zod validation",
"pattern": "const form = useForm<FormData>({ resolver: zodResolver(formSchema) })",
"validation": "Use Zod schemas for both client and server validation"
},
"loadingStates": {
"skeleton": "Use Skeleton component from shadcn/ui",
"spinner": "Use Loader2 icon with animate-spin",
"disabled": "Disable buttons during async operations: disabled={isSubmitting}"
}
}

113
schemas/ai-cli.schema.json Normal file
View file

@ -0,0 +1,113 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://git.abundancepartners.app/abundance/ai-ops-templates/schemas/ai-cli.schema.json",
"title": "AI CLI Configuration",
"description": "Schema for per-project AI CLI configuration",
"type": "object",
"required": ["project", "infrastructure"],
"properties": {
"_meta": {
"type": "object",
"properties": {
"last_updated": { "type": "string", "format": "date-time" },
"version": { "type": "string" },
"updated_by": { "type": "string" },
"change_summary": { "type": "string" }
}
},
"project": {
"type": "string",
"description": "Project identifier matching the preset name"
},
"preset": {
"type": "string",
"description": "URL to the brand preset JSON file"
},
"brand": {
"type": "object",
"description": "Brand overrides (if different from preset)",
"properties": {
"colors": {
"type": "object",
"properties": {
"primary": { "type": "string" },
"secondary": { "type": "string" },
"background": { "type": "string" },
"foreground": { "type": "string" }
}
},
"fonts": {
"type": "object",
"properties": {
"heading": { "type": "string" },
"body": { "type": "string" }
}
}
}
},
"infrastructure": {
"type": "object",
"required": ["server"],
"properties": {
"server": {
"type": "string",
"description": "Server short name (e.g., slcbride, abundance)"
},
"server_ip": {
"type": "string",
"format": "ipv4"
},
"database": {
"type": "string",
"description": "Database identifier"
},
"coolify_app_id": {
"type": "string",
"description": "Coolify application UUID"
},
"coolify_url": {
"type": "string",
"format": "uri"
}
}
},
"seo": {
"type": "object",
"properties": {
"schema_templates": {
"type": "array",
"items": {
"type": "string",
"enum": ["product", "event", "local-business", "faq"]
}
},
"internal_link_patterns": {
"type": "array",
"items": { "type": "string" }
},
"title_template": { "type": "string" },
"default_description": { "type": "string" }
}
},
"apis": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"base_url": { "type": "string", "format": "uri" },
"auth_type": { "type": "string", "enum": ["bearer", "api_key", "basic"] },
"env_key": { "type": "string" }
}
}
},
"features": {
"type": "object",
"additionalProperties": { "type": "boolean" }
},
"routes": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"additionalProperties": true
}

View file

@ -0,0 +1,119 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://git.abundancepartners.app/abundance/ai-ops-templates/schemas/ai-context.schema.json",
"title": "AI Context Manifest",
"description": "Schema for per-project AI context manifest - the single source of truth for AI agents",
"type": "object",
"required": ["client", "infrastructure"],
"properties": {
"_meta": {
"type": "object",
"required": ["last_updated", "version"],
"properties": {
"last_updated": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp of last update"
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+$",
"description": "Semantic version of context"
},
"synced_from": {
"type": "string",
"format": "uri",
"description": "URL of the templates repo this was synced from"
},
"updated_by": {
"type": "string",
"enum": ["ai-agent", "human"],
"description": "Who made the last update"
}
}
},
"client": {
"type": "string",
"description": "Client/project identifier"
},
"brand_preset": {
"type": "string",
"format": "uri",
"description": "URL to the brand preset JSON"
},
"mcp_namespace": {
"type": "string",
"enum": ["all", "ui", "deploy", "seo", "supabase"],
"default": "all",
"description": "Which MCP tools are available for this project"
},
"infrastructure": {
"type": "object",
"required": ["server"],
"properties": {
"server": {
"type": "string",
"description": "Server short name"
},
"database": {
"type": "string",
"description": "Database identifier or 'from-env'"
},
"coolify_app_id": {
"type": "string",
"description": "Coolify app UUID or 'from-env'"
},
"coolify_staging_app_id": {
"type": "string",
"description": "Staging Coolify app UUID"
}
}
},
"common_patterns": {
"type": "array",
"items": { "type": "string" },
"description": "Paths to commonly used snippet files"
},
"routes": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": "Route patterns for the application"
},
"database_schema": {
"type": "object",
"properties": {
"tables": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"description": { "type": "string" },
"key_columns": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
},
"api_endpoints": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"path": { "type": "string" },
"method": { "type": "string", "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"] },
"description": { "type": "string" }
}
}
},
"notes": {
"type": "string",
"description": "Any additional context for AI agents"
}
},
"additionalProperties": true
}

View file

@ -0,0 +1,173 @@
---
name: {{CLIENT_NAME}}
description: Context and operations guide for {{CLIENT_NAME}} project. Use when working on {{DESCRIPTION}}.
---
# {{CLIENT_NAME}}
{{PROJECT_DESCRIPTION}}
## Project Overview
| Property | Value |
|----------|-------|
| **Client** | {{CLIENT_DISPLAY_NAME}} |
| **Site URL** | {{SITE_URL}} |
| **Server** | {{SERVER_NAME}} ({{SERVER_IP}}) |
| **Database** | {{DATABASE_NAME}} |
| **Status** | {{PROJECT_STATUS}} |
---
## Infrastructure
### Server Access
```bash
# SSH into server
ssh root@{{SERVER_IP}}
# Or use abundance CLI
abundance srv ssh {{SERVER_NAME}}
```
### Coolify Deployment
- **Dashboard**: {{COOLIFY_URL}}
- **App ID**: {{COOLIFY_APP_ID}}
- **Deploy**: `abundance coolify deploy application {{COOLIFY_APP_ID}}`
### Database
```bash
# Query database
abundance db query {{DATABASE_NAME}} "SELECT * FROM {{TABLE}} LIMIT 5"
# Create SSH tunnel
abundance db tunnel {{DATABASE_NAME}}
```
---
## Brand Guidelines
### Colors
| Color | Hex | Usage |
|-------|-----|-------|
| Primary | `{{PRIMARY_COLOR}}` | Buttons, links, highlights |
| Secondary | `{{SECONDARY_COLOR}}` | Accents, badges |
| Background | `{{BACKGROUND_COLOR}}` | Page background |
| Text | `{{TEXT_COLOR}}` | Body text |
### Typography
- **Headings**: {{HEADING_FONT}}
- **Body**: {{BODY_FONT}}
### Tailwind Config
Brand colors are defined in `presets/{{CLIENT_SLUG}}.json`. Reference via CSS variables:
```css
background-color: var(--primary);
```
---
## Key Routes
| Route | Description | Component |
|-------|-------------|-----------|
| {{ROUTE_1}} | {{ROUTE_1_DESC}} | `{{ROUTE_1_COMPONENT}}` |
| {{ROUTE_2}} | {{ROUTE_2_DESC}} | `{{ROUTE_2_COMPONENT}}` |
| {{ROUTE_3}} | {{ROUTE_3_DESC}} | `{{ROUTE_3_COMPONENT}}` |
---
## Database Schema
### Key Tables
| Table | Description | Key Columns |
|-------|-------------|-------------|
| {{TABLE_1}} | {{TABLE_1_DESC}} | {{TABLE_1_COLS}} |
| {{TABLE_2}} | {{TABLE_2_DESC}} | {{TABLE_2_COLS}} |
### Common Queries
```sql
-- {{QUERY_1_DESC}}
SELECT * FROM {{TABLE}} WHERE {{CONDITION}};
```
---
## APIs
### {{API_1_NAME}}
- **Base URL**: `{{API_1_URL}}`
- **Auth**: {{API_1_AUTH}}
- **Env Key**: `{{API_1_ENV_KEY}}`
---
## Common Patterns
### {{PATTERN_1_NAME}}
```{{PATTERN_1_LANG}}
{{PATTERN_1_CODE}}
```
### {{PATTERN_2_NAME}}
```{{PATTERN_2_LANG}}
{{PATTERN_2_CODE}}
```
---
## Integrations
| Service | Purpose | Config Location |
|---------|---------|-----------------|
| {{SERVICE_1}} | {{SERVICE_1_PURPOSE}} | {{SERVICE_1_CONFIG}} |
| {{SERVICE_2}} | {{SERVICE_2_PURPOSE}} | {{SERVICE_2_CONFIG}} |
---
## Troubleshooting
### {{ISSUE_1}}
**Symptom**: {{ISSUE_1_SYMPTOM}}
**Solution**: {{ISSUE_1_SOLUTION}}
### {{ISSUE_2}}
**Symptom**: {{ISSUE_2_SYMPTOM}}
**Solution**: {{ISSUE_2_SOLUTION}}
---
## Recent Changes
| Date | Change | Notes |
|------|--------|-------|
| {{DATE_1}} | {{CHANGE_1}} | {{NOTES_1}} |
---
## Context Metadata (Auto-Updated)
| Field | Value |
|-------|-------|
| _last_updated | {{LAST_UPDATED}} |
| _version | {{VERSION}} |
| _synced_from | git.abundancepartners.app/abundance/ai-ops-templates |
---
## Quick Commands
```bash
# Check server status
abundance srv status
# View logs
abundance docker logs {{SERVER_NAME}} {{CONTAINER}} -f --tail 100
# Deploy to staging
deploy_push({ target: "staging", app_id: "{{STAGING_APP_ID}}" })
# Deploy to production
deploy_push({ target: "prod", app_id: "{{PROD_APP_ID}}" })
```

View file

@ -0,0 +1,39 @@
import Link from 'next/link'
import { cn } from '@/lib/utils'
interface InternalLinkProps {
href: string
children: React.ReactNode
className?: string
variant?: 'default' | 'muted' | 'underline'
}
export function InternalLink({
href,
children,
className,
variant = 'default',
}: InternalLinkProps) {
const variants = {
default: 'text-primary hover:text-primary/80',
muted: 'text-muted-foreground hover:text-foreground',
underline: 'text-foreground underline underline-offset-4 hover:text-primary',
}
return (
<Link
href={href}
className={cn(
'transition-colors duration-200',
variants[variant],
className
)}
>
{children}
</Link>
)
}
// Usage example:
// <InternalLink href="/vendors/photography">Wedding Photography</InternalLink>
// <InternalLink href="/venues" variant="muted">View all venues</InternalLink>

View file

@ -0,0 +1,49 @@
package templates
// AltTextPlaceholder generates SEO-friendly alt text for images
// Replace placeholders with actual data before use
type ImageContext struct {
Subject string // What is shown in the image
Context string // What's happening / setting
Location string // Where the image was taken
Relevance string // Why it's relevant to the page
}
func GenerateAltText(ctx ImageContext) string {
// Pattern: [Subject] + [Context] + [Location] + [Relevance]
// Example: "Wedding photographer capturing moments at outdoor ceremony in Salt Lake City"
parts := []string{}
if ctx.Subject != "" {
parts = append(parts, ctx.Subject)
}
if ctx.Context != "" {
parts = append(parts, ctx.Context)
}
if ctx.Location != "" {
parts = append(parts, "in "+ctx.Location)
}
if ctx.Relevance != "" {
parts = append(parts, ctx.Relevance)
}
alt := strings.Join(parts, " ")
// Truncate to 125 characters max (SEO best practice)
if len(alt) > 125 {
alt = alt[:122] + "..."
}
return alt
}
// Example usage:
// alt := GenerateAltText(ImageContext{
// Subject: "Bride and groom",
// Context: "exchanging vows",
// Location: "Salt Lake City Temple",
// Relevance: "wedding ceremony",
// })
// Result: "Bride and groom exchanging vows in Salt Lake City Temple wedding ceremony"

78
snippets/breadcrumb.tsx Normal file
View file

@ -0,0 +1,78 @@
import Link from 'next/link'
import { ChevronRight, Home } from 'lucide-react'
import { cn } from '@/lib/utils'
interface BreadcrumbItem {
label: string
href?: string
}
interface BreadcrumbProps {
items: BreadcrumbItem[]
className?: string
}
export function Breadcrumb({ items, className }: BreadcrumbProps) {
// Generate JSON-LD structured data for SEO
const structuredData = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.label,
item: item.href ? `${process.env.NEXT_PUBLIC_SITE_URL}${item.href}` : undefined,
})),
}
return (
<>
{/* Inject structured data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>
{/* Visible breadcrumb */}
<nav aria-label="Breadcrumb" className={cn('flex items-center text-sm', className)}>
<ol className="flex items-center gap-2">
<li>
<Link
href="/"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Home"
>
<Home className="h-4 w-4" />
</Link>
</li>
{items.map((item, index) => (
<li key={item.href || index} className="flex items-center gap-2">
<ChevronRight className="h-4 w-4 text-muted-foreground" />
{item.href && index < items.length - 1 ? (
<Link
href={item.href}
className="text-muted-foreground hover:text-foreground transition-colors"
>
{item.label}
</Link>
) : (
<span className="text-foreground font-medium" aria-current="page">
{item.label}
</span>
)}
</li>
))}
</ol>
</nav>
</>
)
}
// Usage example:
// <Breadcrumb
// items={[
// { label: 'Vendors', href: '/vendors' },
// { label: 'Photography', href: '/vendors/photography' },
// { label: 'John Smith Photography' }, // Current page - no href
// ]}
// />

134
snippets/deploy-webhook.sh Normal file
View file

@ -0,0 +1,134 @@
#!/bin/bash
# Deploy Webhook Trigger Script
# Triggers Coolify deployment via webhook or API
# Configuration - Set these in your environment or .env file
# COOLIFY_WEBHOOK_URL - The webhook URL from Coolify dashboard
# COOLIFY_API_URL - Your Coolify instance URL (e.g., https://app.abundancepartners.app)
# COOLIFY_API_TOKEN - Your API token
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Method 1: Trigger via Webhook URL
deploy_via_webhook() {
local webhook_url=$1
if [ -z "$webhook_url" ]; then
log_error "Webhook URL not provided"
exit 1
fi
log_info "Triggering deployment via webhook..."
response=$(curl -s -w "\n%{http_code}" -X POST "$webhook_url")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then
log_info "Deployment triggered successfully"
echo "$body"
else
log_error "Deployment failed with HTTP $http_code"
echo "$body"
exit 1
fi
}
# Method 2: Trigger via Coolify API
deploy_via_api() {
local app_uuid=$1
local api_url=${COOLIFY_API_URL:-"https://app.abundancepartners.app"}
local api_token=$COOLIFY_API_TOKEN
if [ -z "$app_uuid" ] || [ -z "$api_token" ]; then
log_error "App UUID and API token required"
exit 1
fi
log_info "Triggering deployment via API for app: $app_uuid"
response=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Authorization: Bearer $api_token" \
-H "Content-Type: application/json" \
"${api_url}/api/v1/deploy?uuid=${app_uuid}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -eq 200 ] || [ "$http_code" -eq 201 ]; then
log_info "Deployment triggered successfully"
echo "$body"
else
log_error "Deployment failed with HTTP $http_code"
echo "$body"
exit 1
fi
}
# Check deployment status
check_status() {
local app_uuid=$1
local api_url=${COOLIFY_API_URL:-"https://app.abundancepartners.app"}
local api_token=$COOLIFY_API_TOKEN
log_info "Checking deployment status..."
curl -s -H "Authorization: Bearer $api_token" \
"${api_url}/api/v1/applications/${app_uuid}" | jq '.'
}
# Usage
usage() {
echo "Usage: $0 [command] [options]"
echo ""
echo "Commands:"
echo " webhook <webhook_url> Deploy via webhook URL"
echo " api <app_uuid> Deploy via Coolify API"
echo " status <app_uuid> Check deployment status"
echo ""
echo "Environment Variables:"
echo " COOLIFY_API_URL Your Coolify instance URL"
echo " COOLIFY_API_TOKEN Your API token"
echo ""
echo "Examples:"
echo " $0 webhook https://app.abundancepartners.app/api/v1/webhooks/deploy/abc123"
echo " $0 api abc123-def456-ghi789"
echo " COOLIFY_API_TOKEN=your_token $0 api abc123-def456-ghi789"
}
# Main
case "$1" in
webhook)
deploy_via_webhook "$2"
;;
api)
deploy_via_api "$2"
;;
status)
check_status "$2"
;;
*)
usage
exit 1
;;
esac

99
snippets/schema-inject.ts Normal file
View file

@ -0,0 +1,99 @@
/**
* Schema Injection Utility
* Injects JSON-LD structured data into page head
*/
// Schema type definitions
export type SchemaType = 'Product' | 'Event' | 'LocalBusiness' | 'FAQPage' | 'Article' | 'Organization'
interface SchemaConfig {
type: SchemaType
data: Record<string, unknown>
}
/**
* Generates JSON-LD script tag for structured data
*/
export function generateSchemaScript(config: SchemaConfig | SchemaConfig[]): string {
const schemas = Array.isArray(config) ? config : [config]
const structuredData = schemas.map(({ type, data }) => ({
'@context': 'https://schema.org',
'@type': type,
...data,
}))
// Return single schema or array
const output = structuredData.length === 1 ? structuredData[0] : structuredData
return JSON.stringify(output)
}
/**
* React component for injecting schema in Next.js
*/
export function SchemaInjector({ config }: { config: SchemaConfig | SchemaConfig[] }) {
const schemaJson = generateSchemaScript(config)
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: schemaJson }}
/>
)
}
/**
* Server-side schema generation for getServerSideProps / getStaticProps
*/
export function getSchemaForPage(
type: SchemaType,
data: Record<string, unknown>
): { __html: string } {
return {
__html: generateSchemaScript({ type, data }),
}
}
// Example usage in Next.js page:
//
// import { SchemaInjector } from '@/lib/schema-inject'
//
// export default function VendorPage({ vendor }) {
// return (
// <>
// <Head>
// <SchemaInjector
// config={{
// type: 'LocalBusiness',
// data: {
// name: vendor.name,
// description: vendor.description,
// address: {
// '@type': 'PostalAddress',
// streetAddress: vendor.address,
// addressLocality: vendor.city,
// addressRegion: vendor.state,
// },
// aggregateRating: {
// '@type': 'AggregateRating',
// ratingValue: vendor.rating,
// reviewCount: vendor.review_count,
// },
// },
// }}
// />
// </Head>
// {/* Page content */}
// </>
// )
// }
// Example for multiple schemas on one page:
//
// <SchemaInjector
// config={[
// { type: 'LocalBusiness', data: businessData },
// { type: 'FAQPage', data: faqData },
// ]}
// />

92
snippets/vendor-card.tsx Normal file
View file

@ -0,0 +1,92 @@
import Image from 'next/image'
import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Star, MapPin } from 'lucide-react'
interface Vendor {
id: string
name: string
slug: string
category: string
description: string
image_url: string
rating: number
review_count: number
location: string
price_range?: string
}
interface VendorCardProps {
vendor: Vendor
className?: string
}
export function VendorCard({ vendor, className }: VendorCardProps) {
return (
<Link href={`/vendors/${vendor.slug}`}>
<Card className={cn(
'group overflow-hidden transition-all duration-300 hover:shadow-lg',
className
)}>
{/* Image Container */}
<div className="relative aspect-[4/3] overflow-hidden">
<Image
src={vendor.image_url}
alt={`${vendor.name} - ${vendor.category} in ${vendor.location}`}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
{vendor.price_range && (
<Badge className="absolute top-3 right-3" variant="secondary">
{vendor.price_range}
</Badge>
)}
</div>
{/* Content */}
<CardContent className="p-4">
{/* Category Badge */}
<Badge variant="outline" className="mb-2">
{vendor.category}
</Badge>
{/* Name */}
<h3 className="font-semibold text-lg mb-1 group-hover:text-primary transition-colors">
{vendor.name}
</h3>
{/* Rating */}
<div className="flex items-center gap-1 mb-2">
<Star className="h-4 w-4 fill-yellow-400 text-yellow-400" />
<span className="font-medium">{vendor.rating.toFixed(1)}</span>
<span className="text-muted-foreground text-sm">
({vendor.review_count} reviews)
</span>
</div>
{/* Location */}
<div className="flex items-center gap-1 text-muted-foreground text-sm">
<MapPin className="h-3 w-3" />
<span>{vendor.location}</span>
</div>
{/* Description - truncated */}
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
{vendor.description}
</p>
</CardContent>
</Card>
</Link>
)
}
// Usage:
// import { VendorCard } from '@/components/vendor-card'
//
// <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
// {vendors.map((vendor) => (
// <VendorCard key={vendor.id} vendor={vendor} />
// ))}
// </div>

View file

@ -0,0 +1,45 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Event structured data template (Schema.org JSON-LD)"
},
"@context": "https://schema.org",
"@type": "Event",
"name": "{{EVENT_NAME}}",
"description": "{{EVENT_DESCRIPTION}}",
"image": "{{EVENT_IMAGE_URL}}",
"startDate": "{{START_DATE}}",
"endDate": "{{END_DATE}}",
"eventStatus": "https://schema.org/EventScheduled",
"eventAttendanceMode": "https://schema.org/{{ATTENDANCE_MODE}}",
"location": {
"@type": "{{LOCATION_TYPE}}",
"name": "{{LOCATION_NAME}}",
"address": {
"@type": "PostalAddress",
"streetAddress": "{{STREET_ADDRESS}}",
"addressLocality": "{{CITY}}",
"addressRegion": "{{STATE}}",
"postalCode": "{{POSTAL_CODE}}",
"addressCountry": "{{COUNTRY}}"
}
},
"organizer": {
"@type": "Organization",
"name": "{{ORGANIZER_NAME}}",
"url": "{{ORGANIZER_URL}}"
},
"offers": {
"@type": "Offer",
"url": "{{TICKET_URL}}",
"price": "{{PRICE}}",
"priceCurrency": "{{CURRENCY}}",
"availability": "https://schema.org/{{TICKET_AVAILABILITY}}",
"validFrom": "{{SALE_START_DATE}}"
},
"performer": {
"@type": "{{PERFORMER_TYPE}}",
"name": "{{PERFORMER_NAME}}"
}
}

35
templates/schema-faq.json Normal file
View file

@ -0,0 +1,35 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "FAQ structured data template (Schema.org JSON-LD)"
},
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "{{QUESTION_1}}",
"acceptedAnswer": {
"@type": "Answer",
"text": "{{ANSWER_1}}"
}
},
{
"@type": "Question",
"name": "{{QUESTION_2}}",
"acceptedAnswer": {
"@type": "Answer",
"text": "{{ANSWER_2}}"
}
},
{
"@type": "Question",
"name": "{{QUESTION_3}}",
"acceptedAnswer": {
"@type": "Answer",
"text": "{{ANSWER_3}}"
}
}
]
}

View file

@ -0,0 +1,59 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Local Business structured data template (Schema.org JSON-LD)"
},
"@context": "https://schema.org",
"@type": "{{BUSINESS_TYPE}}",
"name": "{{BUSINESS_NAME}}",
"description": "{{BUSINESS_DESCRIPTION}}",
"image": "{{BUSINESS_IMAGE_URL}}",
"url": "{{BUSINESS_URL}}",
"telephone": "{{PHONE}}",
"email": "{{EMAIL}}",
"address": {
"@type": "PostalAddress",
"streetAddress": "{{STREET_ADDRESS}}",
"addressLocality": "{{CITY}}",
"addressRegion": "{{STATE}}",
"postalCode": "{{POSTAL_CODE}}",
"addressCountry": "{{COUNTRY}}"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": "{{LATITUDE}}",
"longitude": "{{LONGITUDE}}"
},
"openingHoursSpecification": [
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"opens": "{{WEEKDAY_OPEN}}",
"closes": "{{WEEKDAY_CLOSE}}"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Saturday"],
"opens": "{{SAT_OPEN}}",
"closes": "{{SAT_CLOSE}}"
},
{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Sunday"],
"opens": "{{SUN_OPEN}}",
"closes": "{{SUN_CLOSE}}"
}
],
"priceRange": "{{PRICE_RANGE}}",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{RATING_VALUE}}",
"reviewCount": "{{REVIEW_COUNT}}"
},
"sameAs": [
"{{FACEBOOK_URL}}",
"{{INSTAGRAM_URL}}",
"{{TWITTER_URL}}"
]
}

View file

@ -0,0 +1,34 @@
{
"_meta": {
"last_updated": "2026-03-06T15:52:00Z",
"version": "1.0.0",
"description": "Product structured data template (Schema.org JSON-LD)"
},
"@context": "https://schema.org",
"@type": "Product",
"name": "{{PRODUCT_NAME}}",
"description": "{{PRODUCT_DESCRIPTION}}",
"image": [
"{{PRODUCT_IMAGE_URL}}"
],
"brand": {
"@type": "Brand",
"name": "{{BRAND_NAME}}"
},
"offers": {
"@type": "Offer",
"url": "{{PRODUCT_URL}}",
"priceCurrency": "{{CURRENCY}}",
"price": "{{PRICE}}",
"availability": "https://schema.org/{{AVAILABILITY}}",
"seller": {
"@type": "Organization",
"name": "{{SELLER_NAME}}"
}
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "{{RATING_VALUE}}",
"reviewCount": "{{REVIEW_COUNT}}"
}
}