Initial commit: Claude Skills Factory with 8 refined custom skills
Custom Skills (ourdigital-custom-skills/): - 00-ourdigital-visual-storytelling: Blog featured image prompt generator - 01-ourdigital-research-publisher: Research-to-publication workflow - 02-notion-organizer: Notion workspace management - 03-research-to-presentation: Notion research to PPT/Figma - 04-seo-gateway-strategist: SEO gateway page strategy planning - 05-gateway-page-content-builder: Gateway page content generation - 20-jamie-brand-editor: Jamie Clinic branded content GENERATION - 21-jamie-brand-guardian: Jamie Clinic content REVIEW & evaluation Refinements applied: - All skills converted to SKILL.md format with YAML frontmatter - Added version fields to all skills - Flattened nested folder structures - Removed packaging artifacts (.zip, .skill files) - Reorganized file structures (scripts/, references/, etc.) - Differentiated Jamie skills with clear roles 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Gateway Page Content Generator
|
||||
Automates the creation of SEO-optimized gateway pages for local services
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
"""Location data structure"""
|
||||
id: str
|
||||
name_en: str
|
||||
name_kr: str
|
||||
full_address: str
|
||||
landmarks: List[str]
|
||||
subway_stations: List[str]
|
||||
demographics: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
@dataclass
|
||||
class Service:
|
||||
"""Service data structure"""
|
||||
id: str
|
||||
name_en: str
|
||||
name_kr: str
|
||||
category: str
|
||||
description: str
|
||||
keywords: List[str]
|
||||
procedure_time: str
|
||||
recovery_time: str
|
||||
price_range: str
|
||||
|
||||
@dataclass
|
||||
class Brand:
|
||||
"""Brand/Clinic information"""
|
||||
name_en: str
|
||||
name_kr: str
|
||||
website: str
|
||||
phone: str
|
||||
email: str
|
||||
established_year: int
|
||||
certifications: List[str]
|
||||
unique_selling_points: List[str]
|
||||
|
||||
class GatewayPageGenerator:
|
||||
"""Main class for generating gateway page content"""
|
||||
|
||||
def __init__(self, brand: Brand, template_path: str = "templates/"):
|
||||
self.brand = brand
|
||||
self.template_path = Path(template_path)
|
||||
self.generated_pages = []
|
||||
|
||||
def load_template(self, template_name: str) -> str:
|
||||
"""Load a template file"""
|
||||
template_file = self.template_path / template_name
|
||||
if template_file.exists():
|
||||
with open(template_file, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
else:
|
||||
raise FileNotFoundError(f"Template {template_name} not found")
|
||||
|
||||
def generate_meta_tags(self, service: Service, location: Location) -> Dict:
|
||||
"""Generate SEO meta tags"""
|
||||
return {
|
||||
"title": f"{service.name_en} in {location.name_en} | Expert {service.category} | {self.brand.name_en}",
|
||||
"description": f"Looking for {service.name_en.lower()} in {location.name_en}? "
|
||||
f"{self.brand.name_en} offers professional {service.category.lower()} services. "
|
||||
f"✓ Experienced team ✓ Latest technology ✓ {self.brand.unique_selling_points[0]}",
|
||||
"keywords": ", ".join([
|
||||
f"{service.name_en} {location.name_en}",
|
||||
f"{location.name_en} {service.name_en}",
|
||||
*service.keywords,
|
||||
f"{service.category} {location.name_en}"
|
||||
]),
|
||||
"canonical": f"https://{self.brand.website}/{location.id}/{service.id}/",
|
||||
"og:title": f"{service.name_en} in {location.name_en} - {self.brand.name_en}",
|
||||
"og:description": f"Professional {service.name_en} services in {location.name_en}. "
|
||||
f"Book your consultation today.",
|
||||
"og:image": f"https://{self.brand.website}/images/{service.id}-{location.id}-og.jpg"
|
||||
}
|
||||
|
||||
def generate_schema_markup(self, service: Service, location: Location) -> str:
|
||||
"""Generate JSON-LD schema markup"""
|
||||
schema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "MedicalBusiness",
|
||||
"name": f"{self.brand.name_en} - {location.name_en}",
|
||||
"url": f"https://{self.brand.website}",
|
||||
"telephone": self.brand.phone,
|
||||
"email": self.brand.email,
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": location.full_address,
|
||||
"addressLocality": location.name_en,
|
||||
"addressCountry": "KR"
|
||||
},
|
||||
"geo": {
|
||||
"@type": "GeoCoordinates",
|
||||
"latitude": location.latitude,
|
||||
"longitude": location.longitude
|
||||
},
|
||||
"areaServed": {
|
||||
"@type": "City",
|
||||
"name": location.name_en
|
||||
},
|
||||
"availableService": {
|
||||
"@type": "MedicalProcedure",
|
||||
"name": service.name_en,
|
||||
"description": service.description
|
||||
},
|
||||
"priceRange": service.price_range
|
||||
}
|
||||
return json.dumps(schema, indent=2, ensure_ascii=False)
|
||||
|
||||
def generate_content_variations(self, service: Service, location: Location) -> Dict[str, List[str]]:
|
||||
"""Generate content variations for uniqueness"""
|
||||
return {
|
||||
"hero_headlines": [
|
||||
f"Professional {service.name_en} in {location.name_en}",
|
||||
f"{location.name_en}'s Premier {service.name_en} {service.category}",
|
||||
f"Expert {service.name_en} Services for {location.name_en} Residents",
|
||||
f"Transform Your Look with {service.name_en} in {location.name_en}"
|
||||
],
|
||||
"intro_paragraphs": [
|
||||
f"Welcome to {self.brand.name_en}, where we specialize in providing exceptional "
|
||||
f"{service.name_en} services to the {location.name_en} community. "
|
||||
f"Our state-of-the-art facility, conveniently located near {location.landmarks[0]}, "
|
||||
f"combines advanced technology with personalized care.",
|
||||
|
||||
f"Looking for trusted {service.name_en} in {location.name_en}? "
|
||||
f"At {self.brand.name_en}, we've been serving the {location.demographics} "
|
||||
f"for over {datetime.now().year - self.brand.established_year} years. "
|
||||
f"Our expert team understands the unique needs of {location.name_en} residents.",
|
||||
|
||||
f"Discover why {location.name_en} residents choose {self.brand.name_en} "
|
||||
f"for their {service.name_en} needs. Located just minutes from "
|
||||
f"{', '.join(location.subway_stations[:2])}, we offer {service.category} "
|
||||
f"services that deliver remarkable results."
|
||||
],
|
||||
"cta_buttons": [
|
||||
f"Book Your {location.name_en} Consultation",
|
||||
f"Schedule {service.name_en} Today",
|
||||
f"Get Started in {location.name_en}",
|
||||
f"Reserve Your Appointment"
|
||||
],
|
||||
"trust_signals": [
|
||||
f"Trusted by {location.name_en} residents since {self.brand.established_year}",
|
||||
f"Over 10,000 successful {service.category} treatments",
|
||||
f"5-star rated {service.name_en} clinic in {location.name_en}",
|
||||
f"Certified specialists serving {location.demographics}"
|
||||
]
|
||||
}
|
||||
|
||||
def localize_content(self, content: str, service: Service, location: Location) -> str:
|
||||
"""Add local elements to content"""
|
||||
local_elements = {
|
||||
"transportation": f"Easily accessible via {', '.join(location.subway_stations)} stations",
|
||||
"landmarks": f"Located near {' and '.join(location.landmarks[:2])}",
|
||||
"community": f"Proud to serve the {location.name_en} community",
|
||||
"convenience": f"Convenient for {location.demographics} in {location.name_en}",
|
||||
"local_stats": f"Join thousands of satisfied patients from {location.name_en}"
|
||||
}
|
||||
|
||||
# Add local elements naturally throughout content
|
||||
for key, value in local_elements.items():
|
||||
placeholder = f"[LOCAL_{key.upper()}]"
|
||||
if placeholder in content:
|
||||
content = content.replace(placeholder, value)
|
||||
|
||||
return content
|
||||
|
||||
def generate_page(self, service: Service, location: Location,
|
||||
template_name: str = "gateway-page-medical.md") -> str:
|
||||
"""Generate a complete gateway page"""
|
||||
|
||||
# Load template
|
||||
template = self.load_template(template_name)
|
||||
|
||||
# Generate components
|
||||
meta_tags = self.generate_meta_tags(service, location)
|
||||
schema = self.generate_schema_markup(service, location)
|
||||
variations = self.generate_content_variations(service, location)
|
||||
|
||||
# Replace placeholders in template
|
||||
replacements = {
|
||||
"[Medical Service]": service.name_en,
|
||||
"[Location]": location.name_en,
|
||||
"[location]": location.name_en.lower(),
|
||||
"[Clinic Name]": self.brand.name_en,
|
||||
"[service-slug]": service.id,
|
||||
"[X years]": str(datetime.now().year - self.brand.established_year),
|
||||
"[specific address near landmark]": f"{location.full_address}, near {location.landmarks[0]}",
|
||||
"[nearby subway/bus stations]": ", ".join(location.subway_stations),
|
||||
"[certification details]": ", ".join(self.brand.certifications[:2]),
|
||||
"[equipment type]": f"{service.category} equipment",
|
||||
"[duration]": service.procedure_time,
|
||||
"[Medical Specialty]": service.category,
|
||||
"[phone-number]": self.brand.phone,
|
||||
"[website-url]": f"https://{self.brand.website}",
|
||||
"[page-url]": f"https://{self.brand.website}/{location.id}/{service.id}/",
|
||||
"[latitude]": str(location.latitude),
|
||||
"[longitude]": str(location.longitude),
|
||||
}
|
||||
|
||||
# Apply replacements
|
||||
content = template
|
||||
for placeholder, value in replacements.items():
|
||||
content = content.replace(placeholder, value)
|
||||
|
||||
# Add localized content
|
||||
content = self.localize_content(content, service, location)
|
||||
|
||||
# Add schema markup at the end if not already present
|
||||
if '"@context": "https://schema.org"' not in content:
|
||||
content += f"\n\n<!-- Schema Markup -->\n<script type='application/ld+json'>\n{schema}\n</script>"
|
||||
|
||||
return content
|
||||
|
||||
def generate_batch(self, services: List[Service], locations: List[Location],
|
||||
output_dir: str = "output/") -> List[str]:
|
||||
"""Generate multiple gateway pages"""
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
generated_files = []
|
||||
|
||||
for location in locations:
|
||||
location_dir = output_path / location.id
|
||||
location_dir.mkdir(exist_ok=True)
|
||||
|
||||
for service in services:
|
||||
# Generate content
|
||||
content = self.generate_page(service, location)
|
||||
|
||||
# Save to file
|
||||
filename = f"{service.id}-{location.id}.md"
|
||||
filepath = location_dir / filename
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
generated_files.append(str(filepath))
|
||||
print(f"✓ Generated: {filepath}")
|
||||
|
||||
# Generate index file
|
||||
self.generate_index(services, locations, output_path)
|
||||
|
||||
return generated_files
|
||||
|
||||
def generate_index(self, services: List[Service], locations: List[Location],
|
||||
output_path: Path):
|
||||
"""Generate an index of all created pages"""
|
||||
index_content = f"# Gateway Pages Index - {self.brand.name_en}\n\n"
|
||||
index_content += f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
|
||||
index_content += "## Pages by Location\n\n"
|
||||
|
||||
for location in locations:
|
||||
index_content += f"### {location.name_en}\n"
|
||||
for service in services:
|
||||
url = f"/{location.id}/{service.id}/"
|
||||
index_content += f"- [{service.name_en} in {location.name_en}]({url})\n"
|
||||
index_content += "\n"
|
||||
|
||||
index_content += "## Pages by Service\n\n"
|
||||
for service in services:
|
||||
index_content += f"### {service.name_en}\n"
|
||||
for location in locations:
|
||||
url = f"/{location.id}/{service.id}/"
|
||||
index_content += f"- [{location.name_en}]({url})\n"
|
||||
index_content += "\n"
|
||||
|
||||
index_content += f"\n---\nTotal Pages Generated: {len(services) * len(locations)}\n"
|
||||
|
||||
with open(output_path / "index.md", 'w', encoding='utf-8') as f:
|
||||
f.write(index_content)
|
||||
|
||||
def create_sample_data():
|
||||
"""Create sample data for testing"""
|
||||
|
||||
# Sample brand
|
||||
brand = Brand(
|
||||
name_en="Jamie Clinic",
|
||||
name_kr="제이미 클리닉",
|
||||
website="www.jamieclinic.com",
|
||||
phone="+82-2-1234-5678",
|
||||
email="info@jamieclinic.com",
|
||||
established_year=2010,
|
||||
certifications=["ISO 9001", "KAHF Certified", "JCI Accredited"],
|
||||
unique_selling_points=[
|
||||
"Same-day appointments available",
|
||||
"15+ years of experience",
|
||||
"Latest medical technology"
|
||||
]
|
||||
)
|
||||
|
||||
# Sample locations
|
||||
locations = [
|
||||
Location(
|
||||
id="gangnam",
|
||||
name_en="Gangnam",
|
||||
name_kr="강남",
|
||||
full_address="123 Teheran-ro, Gangnam-gu, Seoul",
|
||||
landmarks=["COEX", "Gangnam Station", "Samsung Station"],
|
||||
subway_stations=["Gangnam Station (Line 2)", "Sinnonhyeon Station (Line 9)"],
|
||||
demographics="Young professionals and affluent residents",
|
||||
latitude=37.4979,
|
||||
longitude=127.0276
|
||||
),
|
||||
Location(
|
||||
id="myeongdong",
|
||||
name_en="Myeongdong",
|
||||
name_kr="명동",
|
||||
full_address="456 Myeongdong-gil, Jung-gu, Seoul",
|
||||
landmarks=["Myeongdong Cathedral", "Lotte Department Store"],
|
||||
subway_stations=["Myeongdong Station (Line 4)", "Euljiro 1-ga Station (Line 2)"],
|
||||
demographics="Tourists and young shoppers",
|
||||
latitude=37.5636,
|
||||
longitude=126.9869
|
||||
)
|
||||
]
|
||||
|
||||
# Sample services
|
||||
services = [
|
||||
Service(
|
||||
id="laser-hair-removal",
|
||||
name_en="Laser Hair Removal",
|
||||
name_kr="레이저 제모",
|
||||
category="Dermatology",
|
||||
description="Advanced laser technology for permanent hair reduction",
|
||||
keywords=["permanent hair removal", "IPL", "diode laser"],
|
||||
procedure_time="30-60 minutes",
|
||||
recovery_time="No downtime",
|
||||
price_range="₩₩₩"
|
||||
),
|
||||
Service(
|
||||
id="botox",
|
||||
name_en="Botox Treatment",
|
||||
name_kr="보톡스",
|
||||
category="Cosmetic Dermatology",
|
||||
description="FDA-approved botulinum toxin for wrinkle reduction",
|
||||
keywords=["wrinkle treatment", "anti-aging", "facial rejuvenation"],
|
||||
procedure_time="15-30 minutes",
|
||||
recovery_time="No downtime",
|
||||
price_range="₩₩₩₩"
|
||||
)
|
||||
]
|
||||
|
||||
return brand, locations, services
|
||||
|
||||
def main():
|
||||
"""Main execution function"""
|
||||
print("=" * 60)
|
||||
print("Gateway Page Content Generator")
|
||||
print("=" * 60)
|
||||
|
||||
# Get sample data
|
||||
brand, locations, services = create_sample_data()
|
||||
|
||||
# Initialize generator
|
||||
generator = GatewayPageGenerator(brand)
|
||||
|
||||
# Generate pages
|
||||
print(f"\nGenerating {len(services) * len(locations)} gateway pages...")
|
||||
print("-" * 40)
|
||||
|
||||
generated_files = generator.generate_batch(services, locations)
|
||||
|
||||
print("-" * 40)
|
||||
print(f"\n✅ Successfully generated {len(generated_files)} pages!")
|
||||
print(f"📁 Output directory: output/")
|
||||
print(f"📋 Index file created: output/index.md")
|
||||
|
||||
# Generate report
|
||||
print("\n" + "=" * 60)
|
||||
print("GENERATION REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Brand: {brand.name_en}")
|
||||
print(f"Locations: {', '.join([loc.name_en for loc in locations])}")
|
||||
print(f"Services: {', '.join([svc.name_en for svc in services])}")
|
||||
print(f"Total Pages: {len(generated_files)}")
|
||||
print(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user