fix: Add missing SEO pages list and sync endpoints
- GET /api/seo/pages - List all SEO pages with filters - POST /api/seo/sync-state-pages - Create pages for states with dispensaries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,130 @@ router.post('/validate', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/seo/pages - List all SEO pages with optional filters
|
||||
*/
|
||||
router.get('/pages', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { type, search } = req.query;
|
||||
const pool = getPool();
|
||||
|
||||
let query = `
|
||||
SELECT p.id, p.type, p.slug, p.page_key, p.primary_keyword,
|
||||
p.status, p.last_generated_at, p.last_reviewed_at,
|
||||
p.created_at, p.updated_at
|
||||
FROM seo_pages p
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
if (type && typeof type === 'string') {
|
||||
params.push(type);
|
||||
query += ` AND p.type = $${params.length}`;
|
||||
}
|
||||
|
||||
if (search && typeof search === 'string') {
|
||||
params.push(`%${search}%`);
|
||||
query += ` AND (p.slug ILIKE $${params.length} OR p.page_key ILIKE $${params.length} OR p.primary_keyword ILIKE $${params.length})`;
|
||||
}
|
||||
|
||||
query += ` ORDER BY p.type, p.slug`;
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
// Get metrics for state pages
|
||||
const pages = await Promise.all(result.rows.map(async (page) => {
|
||||
let metrics = null;
|
||||
if (page.type === 'state') {
|
||||
const stateCode = page.slug.replace('dispensaries-', '').toUpperCase();
|
||||
const metricsResult = await pool.query(`
|
||||
SELECT COUNT(DISTINCT d.id) as dispensary_count,
|
||||
COUNT(DISTINCT p.id) as product_count,
|
||||
COUNT(DISTINCT p.brand_name) as brand_count
|
||||
FROM dispensaries d
|
||||
LEFT JOIN dutchie_products p ON p.dispensary_id = d.id
|
||||
WHERE d.state = $1
|
||||
`, [stateCode]);
|
||||
const m = metricsResult.rows[0];
|
||||
metrics = {
|
||||
dispensaryCount: parseInt(m.dispensary_count, 10) || 0,
|
||||
productCount: parseInt(m.product_count, 10) || 0,
|
||||
brandCount: parseInt(m.brand_count, 10) || 0,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: page.id,
|
||||
type: page.type,
|
||||
slug: page.slug,
|
||||
pageKey: page.page_key,
|
||||
primaryKeyword: page.primary_keyword,
|
||||
status: page.status,
|
||||
lastGeneratedAt: page.last_generated_at,
|
||||
lastReviewedAt: page.last_reviewed_at,
|
||||
metrics,
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({ pages });
|
||||
} catch (error: any) {
|
||||
console.error('[SEO] Error listing pages:', error.message);
|
||||
res.status(500).json({ error: 'Failed to list SEO pages' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/seo/sync-state-pages - Create SEO pages for all states with dispensaries
|
||||
*/
|
||||
router.post('/sync-state-pages', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const pool = getPool();
|
||||
|
||||
// Get all states that have dispensaries
|
||||
const statesResult = await pool.query(`
|
||||
SELECT DISTINCT state, COUNT(*) as dispensary_count
|
||||
FROM dispensaries
|
||||
WHERE state IS NOT NULL AND state != ''
|
||||
GROUP BY state
|
||||
HAVING COUNT(*) > 0
|
||||
ORDER BY state
|
||||
`);
|
||||
|
||||
const states = statesResult.rows;
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const { state, dispensary_count } of states) {
|
||||
const slug = `dispensaries-${state.toLowerCase()}`;
|
||||
const pageKey = `state-${state.toLowerCase()}`;
|
||||
const primaryKeyword = `${state} dispensaries`;
|
||||
|
||||
const result = await pool.query(`
|
||||
INSERT INTO seo_pages (type, slug, page_key, primary_keyword, status, created_at, updated_at)
|
||||
VALUES ('state', $1, $2, $3, 'pending_generation', NOW(), NOW())
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
updated_at = NOW()
|
||||
RETURNING (xmax = 0) as is_new
|
||||
`, [slug, pageKey, primaryKeyword]);
|
||||
|
||||
if (result.rows[0]?.is_new) {
|
||||
created++;
|
||||
} else {
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: `Synced ${states.length} state pages`,
|
||||
created,
|
||||
updated,
|
||||
states: states.map(s => s.state),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[SEO] Error syncing state pages:', error.message);
|
||||
res.status(500).json({ error: 'Failed to sync state pages' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/seo/state/:stateCode - State SEO data with metrics
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user