| Server IP : 3.147.158.171 / Your IP : 216.73.216.229 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/www/html/api.turmansolutions.ai/scripts/ |
Upload File : |
"""
Site Indexing Script
Indexes WordPress sites by fetching metadata (name, tagline, logo) via the WP REST API
and storing results in a local SQLite database.
Each environment indexes its own sites by loading them from Drupal via
SiteConfigService. The script reconciles the database to the live config:
sites returned by the service are inserted/updated, and any rows in the
database whose slug is no longer present are pruned. To remove a site from
the index, delete the site_config node in Drupal and re-run this script.
Usage:
cd api
python scripts/index_sites.py [--env development|production|scike_ai]
The TSAI_ENV environment variable is used by default. Pass --env to override.
"""
import argparse
import json
import logging
import os
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
import re
import requests
API_DIR = Path(__file__).resolve().parent.parent
DB_PATH = API_DIR / 'data' / 'site_index.db'
WP_JSON_ROOT = '/wp-json/'
WP_JSON_MEDIA = '/wp-json/wp/v2/media/'
logging.basicConfig(
level=logging.INFO,
format='%(levelname)s %(asctime)s %(message)s',
datefmt='%m/%d/%Y %I:%M:%S %p'
)
logger = logging.getLogger(__name__)
def init_db() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.execute('''
CREATE TABLE IF NOT EXISTS site_index (
slug TEXT PRIMARY KEY,
wp_host TEXT NOT NULL,
name TEXT DEFAULT '',
tagline TEXT DEFAULT '',
link TEXT DEFAULT '',
chat_url TEXT DEFAULT '',
logo_full TEXT DEFAULT '',
logo_72 TEXT DEFAULT '',
logo_150 TEXT DEFAULT '',
logo_300 TEXT DEFAULT '',
indexed_at TEXT NOT NULL,
promoted INTEGER DEFAULT 1
)
''')
conn.commit()
return conn
def compute_chat_url(slug: str, wp_host: str) -> str:
"""Compute chat URL using the same prefix logic as site_config_service."""
prefix = 'chat.' if slug in ('jpt', 'tsai') else 'chat-'
if '://' in wp_host:
protocol, host = wp_host.split('://', 1)
return f"{protocol}://{prefix}{host}"
return f"{prefix}{wp_host}"
def fetch_blog_public(wp_host: str) -> bool:
"""Check if a site allows search engine indexing (blog_public setting).
Fetches the site's homepage HTML and looks for a <meta name='robots'
tag containing 'noindex'. WordPress outputs this when blog_public is 0
(Settings > Reading > "Discourage search engines from indexing this site").
Returns True (promoted) if the site allows indexing, False if noindex is found.
"""
url = wp_host.rstrip('/')
try:
resp = requests.get(url, timeout=15)
resp.raise_for_status()
# WordPress outputs: <meta name='robots' content='noindex, ...'>
if re.search(r"<meta\s+name=['\"]robots['\"][^>]*noindex", resp.text, re.IGNORECASE):
return False
return True
except Exception as e:
logger.warning(f"Failed to check blog_public for {url}: {e}")
return True # Default to promoted if we can't check
def fetch_site_info(wp_host: str) -> dict:
"""Fetch site name, tagline, and logo info from the WP REST API root."""
url = wp_host.rstrip('/') + WP_JSON_ROOT
try:
resp = requests.get(url, timeout=15)
resp.raise_for_status()
data = resp.json()
return {
'name': data.get('name', ''),
'tagline': data.get('description', ''),
'site_logo': data.get('site_logo'),
'site_icon_url': data.get('site_icon_url', ''),
}
except Exception as e:
logger.warning(f"Failed to fetch site info from {url}: {e}")
return {}
def fetch_logo_sizes(wp_host: str, media_id: int) -> dict:
"""Fetch logo URLs at various sizes from the WP media endpoint."""
url = wp_host.rstrip('/') + WP_JSON_MEDIA + str(media_id)
try:
resp = requests.get(url, timeout=15)
resp.raise_for_status()
data = resp.json()
source_url = data.get('source_url', '')
sizes = data.get('media_details', {}).get('sizes', {})
return {
'logo_full': source_url,
'logo_72': sizes.get('thumbnail', {}).get('source_url', source_url),
'logo_150': sizes.get('medium', {}).get('source_url',
sizes.get('thumbnail', {}).get('source_url', source_url)),
'logo_300': sizes.get('medium_large', {}).get('source_url',
sizes.get('medium', {}).get('source_url', source_url)),
}
except Exception as e:
logger.warning(f"Failed to fetch logo from {url}: {e}")
return {}
def index_sites(env: str):
os.environ.setdefault('TSAI_ENV', env)
sys.path.insert(0, str(API_DIR))
from app.services.site_config_service import SiteConfigService
service = SiteConfigService()
all_sites = service.get_all_sites()
print(f"Loaded {len(all_sites)} sites from SiteConfigService")
# De-duplicate by slug, keeping first occurrence
seen = {}
for site in all_sites:
if site.slug not in seen:
seen[site.slug] = site
conn = init_db()
now = datetime.now(timezone.utc).isoformat()
indexed = 0
failed = 0
for slug, site in seen.items():
wp_host = site.wp_host
print(f" Indexing {slug} ({wp_host})...", end=' ')
info = fetch_site_info(wp_host)
if not info:
print("FAILED")
failed += 1
continue
chat_url = compute_chat_url(slug, wp_host)
promoted = fetch_blog_public(wp_host)
# Try to get logo from custom logo (media ID)
logos = {}
site_logo_id = info.get('site_logo')
if site_logo_id and site_logo_id != 0:
logos = fetch_logo_sizes(wp_host, site_logo_id)
# Fall back to site icon URL if no custom logo
if not logos.get('logo_full') and info.get('site_icon_url'):
icon_url = info['site_icon_url']
logos = {
'logo_full': icon_url,
'logo_72': icon_url,
'logo_150': icon_url,
'logo_300': icon_url,
}
conn.execute('''
INSERT OR REPLACE INTO site_index
(slug, wp_host, name, tagline, link, chat_url,
logo_full, logo_72, logo_150, logo_300, indexed_at, promoted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
slug, wp_host,
info.get('name', ''), info.get('tagline', ''),
wp_host, chat_url,
logos.get('logo_full', ''), logos.get('logo_72', ''),
logos.get('logo_150', ''), logos.get('logo_300', ''),
now, int(promoted)
))
conn.commit()
indexed += 1
print(f"OK - {info.get('name', slug)}")
# Prune slugs that are no longer in the config
configured_slugs = set(seen.keys())
existing_rows = conn.execute('SELECT slug FROM site_index').fetchall()
stale_slugs = [row[0] for row in existing_rows if row[0] not in configured_slugs]
for stale in stale_slugs:
conn.execute('DELETE FROM site_index WHERE slug = ?', (stale,))
print(f" Pruned {stale} (no longer in config)")
if stale_slugs:
conn.commit()
conn.close()
print(f"\nDone. Indexed {indexed} sites, {failed} failed, {len(stale_slugs)} pruned.")
def main():
parser = argparse.ArgumentParser(description='Index WordPress sites')
parser.add_argument(
'--env',
choices=['development', 'production', 'scike_ai'],
default=os.getenv('TSAI_ENV', 'development'),
help='Environment to index (default: TSAI_ENV or development)'
)
args = parser.parse_args()
index_sites(args.env)
if __name__ == '__main__':
main()