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:
commit
3cb8d3cb3f
24 changed files with 2202 additions and 0 deletions
128
.cursor/rules/ai-context-update.md
Normal file
128
.cursor/rules/ai-context-update.md
Normal 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
108
README.md
Normal 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
52
context/CHANGELOG.md
Normal 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
42
context/last-sync.json
Normal 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
88
presets/default.json
Normal 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
100
presets/slc-bride.json
Normal 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
166
rules/ai-decision-guide.md
Normal 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
208
rules/common-fixes.md
Normal 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
115
rules/seo-rules.md
Normal 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
68
rules/tailwind-brand.json
Normal 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
58
rules/ui-fixes.json
Normal 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
113
schemas/ai-cli.schema.json
Normal 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
|
||||||
|
}
|
||||||
119
schemas/ai-context.schema.json
Normal file
119
schemas/ai-context.schema.json
Normal 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
|
||||||
|
}
|
||||||
173
skill-templates/client-skill.template.md
Normal file
173
skill-templates/client-skill.template.md
Normal 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}}" })
|
||||||
|
```
|
||||||
39
snippets/add-internal-link.tsx
Normal file
39
snippets/add-internal-link.tsx
Normal 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>
|
||||||
49
snippets/alt-text-placeholder.go
Normal file
49
snippets/alt-text-placeholder.go
Normal 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
78
snippets/breadcrumb.tsx
Normal 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
134
snippets/deploy-webhook.sh
Normal 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
99
snippets/schema-inject.ts
Normal 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
92
snippets/vendor-card.tsx
Normal 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>
|
||||||
45
templates/schema-event.json
Normal file
45
templates/schema-event.json
Normal 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
35
templates/schema-faq.json
Normal 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}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
templates/schema-local-business.json
Normal file
59
templates/schema-local-business.json
Normal 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}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
34
templates/schema-product.json
Normal file
34
templates/schema-product.json
Normal 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}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue