@pixelated-tech/components 3.5.13 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/components/admin/site-health/google.api.integration.js +258 -0
- package/dist/components/admin/site-health/google.api.utils.js +47 -0
- package/dist/components/admin/site-health/site-health-accessibility.js +5 -103
- package/dist/components/admin/site-health/site-health-axe-core.js +4 -10
- package/dist/components/admin/site-health/site-health-cloudwatch.js +24 -26
- package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +4 -10
- package/dist/components/admin/site-health/site-health-github.js +9 -15
- package/dist/components/admin/site-health/site-health-google-analytics.integration.js +1 -107
- package/dist/components/admin/site-health/site-health-google-analytics.js +21 -29
- package/dist/components/admin/site-health/site-health-google-search-console.integration.js +1 -113
- package/dist/components/admin/site-health/site-health-google-search-console.js +22 -28
- package/dist/components/admin/site-health/site-health-on-site-seo.js +8 -82
- package/dist/components/admin/site-health/site-health-overview.js +5 -19
- package/dist/components/admin/site-health/site-health-performance.js +5 -167
- package/dist/components/admin/site-health/site-health-security.js +7 -148
- package/dist/components/admin/site-health/site-health-seo.js +5 -103
- package/dist/components/admin/site-health/site-health-template.js +68 -43
- package/dist/components/admin/site-health/site-health-uptime.js +4 -9
- package/dist/components/admin/site-health/site-health-utils.js +170 -0
- package/dist/components/admin/site-health/site-health.css +8 -9
- package/dist/index.adminclient.js +2 -5
- package/dist/index.adminserver.js +6 -4
- package/dist/index.js +39 -45
- package/dist/types/components/admin/site-health/google.api.integration.d.ts +82 -0
- package/dist/types/components/admin/site-health/google.api.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/google.api.utils.d.ts +32 -0
- package/dist/types/components/admin/site-health/google.api.utils.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-cloudwatch.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +1 -21
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +1 -41
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-template.d.ts +8 -1
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-utils.d.ts +17 -0
- package/dist/types/components/admin/site-health/site-health-utils.d.ts.map +1 -0
- package/dist/types/index.adminclient.d.ts +2 -5
- package/dist/types/index.adminserver.d.ts +6 -4
- package/dist/types/index.d.ts +38 -44
- package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -1
- package/dist/types/tests/google.api.integration.test.d.ts +2 -0
- package/dist/types/tests/google.api.integration.test.d.ts.map +1 -0
- package/dist/types/tests/google.api.utils.test.d.ts +5 -0
- package/dist/types/tests/google.api.utils.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/components/admin/site-health/google-api-auth.js +0 -69
- package/dist/types/components/admin/site-health/google-api-auth.d.ts +0 -37
- package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +0 -1
|
@@ -3,110 +3,4 @@
|
|
|
3
3
|
* Server-side utilities for Google Analytics data retrieval
|
|
4
4
|
*/
|
|
5
5
|
"use server";
|
|
6
|
-
|
|
7
|
-
import { createAnalyticsClient } from './google-api-auth';
|
|
8
|
-
// Cache for analytics data (1 hour)
|
|
9
|
-
const analyticsCache = new RouteCache();
|
|
10
|
-
/**
|
|
11
|
-
* Get Google Analytics data for a site with current/previous period comparison
|
|
12
|
-
*/
|
|
13
|
-
export async function getGoogleAnalyticsData(config, siteName, startDate, endDate) {
|
|
14
|
-
try {
|
|
15
|
-
// Check cache first
|
|
16
|
-
const cacheKey = `analytics-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
17
|
-
const cached = analyticsCache.get(cacheKey);
|
|
18
|
-
if (cached) {
|
|
19
|
-
return { success: true, data: cached };
|
|
20
|
-
}
|
|
21
|
-
if (!config.ga4PropertyId || config.ga4PropertyId === 'GA4_PROPERTY_ID_HERE') {
|
|
22
|
-
return {
|
|
23
|
-
success: false,
|
|
24
|
-
error: 'GA4 Property ID not configured for this site'
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
// Set up authentication
|
|
28
|
-
const authResult = await createAnalyticsClient(config);
|
|
29
|
-
if (!authResult.success) {
|
|
30
|
-
return {
|
|
31
|
-
success: false,
|
|
32
|
-
error: authResult.error || 'Authentication failed'
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const analyticsData = authResult.client;
|
|
36
|
-
// Calculate date ranges
|
|
37
|
-
const currentEndDate = endDate ? new Date(endDate) : new Date();
|
|
38
|
-
const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
39
|
-
// Calculate previous period (same duration before the current period)
|
|
40
|
-
const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
|
|
41
|
-
const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
|
|
42
|
-
const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
|
|
43
|
-
const currentStartStr = currentStartDate.toISOString().split('T')[0];
|
|
44
|
-
const currentEndStr = currentEndDate.toISOString().split('T')[0];
|
|
45
|
-
const previousStartStr = previousStartDate.toISOString().split('T')[0];
|
|
46
|
-
const previousEndStr = previousEndDate.toISOString().split('T')[0];
|
|
47
|
-
// Fetch current period data
|
|
48
|
-
const currentResponse = await analyticsData.properties.runReport({
|
|
49
|
-
property: `properties/${config.ga4PropertyId}`,
|
|
50
|
-
requestBody: {
|
|
51
|
-
dateRanges: [{ startDate: currentStartStr, endDate: currentEndStr }],
|
|
52
|
-
dimensions: [{ name: 'date' }],
|
|
53
|
-
metrics: [{ name: 'screenPageViews' }],
|
|
54
|
-
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
// Fetch previous period data
|
|
58
|
-
const previousResponse = await analyticsData.properties.runReport({
|
|
59
|
-
property: `properties/${config.ga4PropertyId}`,
|
|
60
|
-
requestBody: {
|
|
61
|
-
dateRanges: [{ startDate: previousStartStr, endDate: previousEndStr }],
|
|
62
|
-
dimensions: [{ name: 'date' }],
|
|
63
|
-
metrics: [{ name: 'screenPageViews' }],
|
|
64
|
-
orderBys: [{ dimension: { dimensionName: 'date' } }],
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
// Create a map of previous period data by date
|
|
68
|
-
const previousDataMap = new Map();
|
|
69
|
-
previousResponse.data.rows?.forEach((row) => {
|
|
70
|
-
const dateStr = row.dimensionValues?.[0]?.value || '';
|
|
71
|
-
if (dateStr) {
|
|
72
|
-
previousDataMap.set(dateStr, parseInt(row.metricValues?.[0]?.value || '0'));
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
// Combine current and previous period data
|
|
76
|
-
const chartData = [];
|
|
77
|
-
const daysInRange = Math.ceil((currentEndDate.getTime() - currentStartDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
78
|
-
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
79
|
-
const currentDate = new Date(currentEndDate);
|
|
80
|
-
currentDate.setDate(currentDate.getDate() - i);
|
|
81
|
-
const currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
82
|
-
// Calculate corresponding previous period date
|
|
83
|
-
const previousDate = new Date(currentDate.getTime() - periodDuration);
|
|
84
|
-
const previousDateStr = previousDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD format
|
|
85
|
-
// Get current period data
|
|
86
|
-
const currentRow = currentResponse.data.rows?.find((row) => row.dimensionValues?.[0]?.value === currentDateStr);
|
|
87
|
-
const currentPageViews = parseInt(currentRow?.metricValues?.[0]?.value || '0');
|
|
88
|
-
// Get previous period data
|
|
89
|
-
const previousPageViews = previousDataMap.get(previousDateStr) || 0;
|
|
90
|
-
// Format date for display
|
|
91
|
-
const formattedDate = currentDate.toLocaleDateString('en-US', {
|
|
92
|
-
month: 'short',
|
|
93
|
-
day: 'numeric'
|
|
94
|
-
});
|
|
95
|
-
chartData.push({
|
|
96
|
-
date: formattedDate,
|
|
97
|
-
currentPageViews: currentPageViews,
|
|
98
|
-
previousPageViews: previousPageViews,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
// Cache the result
|
|
102
|
-
analyticsCache.set(cacheKey, chartData);
|
|
103
|
-
return { success: true, data: chartData };
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
console.error('Google Analytics error:', error);
|
|
107
|
-
return {
|
|
108
|
-
success: false,
|
|
109
|
-
error: error.message
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
6
|
+
export {};
|
|
@@ -9,41 +9,33 @@ SiteHealthGoogleAnalytics.propTypes = {
|
|
|
9
9
|
endDate: PropTypes.string,
|
|
10
10
|
};
|
|
11
11
|
export function SiteHealthGoogleAnalytics({ siteName, startDate, endDate }) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const result = await response.json();
|
|
23
|
-
if (!result.success) {
|
|
24
|
-
// Handle specific error types
|
|
25
|
-
if (result.error?.includes('invalid_grant') || result.error?.includes('authentication')) {
|
|
26
|
-
throw new Error('Google Analytics authentication expired. Please re-authorize the application.');
|
|
27
|
-
}
|
|
28
|
-
else if (result.error?.includes('GA4 Property ID not configured')) {
|
|
29
|
-
throw new Error('GA4 Property ID not configured for this site');
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
throw new Error(result.error || 'Failed to load analytics data');
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return result.data;
|
|
36
|
-
};
|
|
37
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Google Analytics", columnSpan: 2, fetchData: fetchAnalyticsData, children: (data) => {
|
|
38
|
-
if (!data || data.length === 0) {
|
|
12
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Google Analytics", columnSpan: 2, endpoint: {
|
|
13
|
+
endpoint: '/api/site-health/google-analytics',
|
|
14
|
+
params: {
|
|
15
|
+
...(startDate && { startDate }),
|
|
16
|
+
...(endDate && { endDate }),
|
|
17
|
+
},
|
|
18
|
+
responseTransformer: (result) => result.data, // Extract the data array from the response
|
|
19
|
+
}, children: (data) => {
|
|
20
|
+
// Ensure data is an array
|
|
21
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
39
22
|
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "No data available for the selected date range" }) }));
|
|
40
23
|
}
|
|
41
|
-
|
|
24
|
+
// Filter out any invalid data points
|
|
25
|
+
const validData = data.filter((point) => point &&
|
|
26
|
+
typeof point === 'object' &&
|
|
27
|
+
typeof point.date === 'string' &&
|
|
28
|
+
typeof point.currentPageViews === 'number' &&
|
|
29
|
+
typeof point.previousPageViews === 'number');
|
|
30
|
+
if (validData.length === 0) {
|
|
31
|
+
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "Invalid data format received from Google Analytics API." }) }));
|
|
32
|
+
}
|
|
33
|
+
return (_jsx("div", { children: _jsx("div", { style: { width: '100%', height: '400px', border: '1px solid #ddd' }, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: validData, margin: { top: 40, right: 30, left: 20, bottom: 5 }, children: [_jsx("text", { x: "50%", y: 20, textAnchor: "middle", fontSize: "16", fontWeight: "bold", fill: "#374151", children: "Page Views (Current vs Previous Period)" }), _jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date", tick: { fontSize: 12 }, angle: -45, textAnchor: "end", height: 60 }), _jsx(YAxis, { tick: { fontSize: 12 } }), _jsx(Tooltip, { formatter: (value, name) => [
|
|
42
34
|
value?.toLocaleString() || '0',
|
|
43
35
|
name || 'Unknown'
|
|
44
36
|
], labelFormatter: (label) => `Date: ${label}` }), _jsx(Legend, { wrapperStyle: {
|
|
45
37
|
fontSize: '12px',
|
|
46
38
|
paddingTop: '10px'
|
|
47
|
-
} }), _jsx(Bar, { dataKey: "currentPageViews", fill: "#3b82f6", name: "Current Period", radius: [2, 2, 0, 0] }), _jsx(Line, { type: "monotone", dataKey: "previousPageViews", stroke: "#ef4444", strokeWidth: 2, strokeDasharray: "5 5", dot: { fill: '#ef4444', strokeWidth: 2, r: 3 }, activeDot: { r: 5, stroke: '#ef4444', strokeWidth: 2 }, name: "Previous Period" })] }, `chart-${
|
|
39
|
+
} }), _jsx(Bar, { dataKey: "currentPageViews", fill: "#3b82f6", name: "Current Period", radius: [2, 2, 0, 0] }), _jsx(Line, { type: "monotone", dataKey: "previousPageViews", stroke: "#ef4444", strokeWidth: 2, strokeDasharray: "5 5", dot: { fill: '#ef4444', strokeWidth: 2, r: 3 }, activeDot: { r: 5, stroke: '#ef4444', strokeWidth: 2 }, name: "Previous Period" })] }, `chart-${validData.length}`) }) }) }));
|
|
48
40
|
} }));
|
|
49
41
|
}
|
|
@@ -3,116 +3,4 @@
|
|
|
3
3
|
* Server-side utilities for Google Search Console data retrieval
|
|
4
4
|
*/
|
|
5
5
|
"use server";
|
|
6
|
-
|
|
7
|
-
import { createSearchConsoleClient } from './google-api-auth';
|
|
8
|
-
// Cache for search console data (1 hour)
|
|
9
|
-
const searchConsoleCache = new RouteCache();
|
|
10
|
-
/**
|
|
11
|
-
* Get Google Search Console data for a site with current/previous period comparison
|
|
12
|
-
*/
|
|
13
|
-
export async function getSearchConsoleData(config, siteName, startDate, endDate) {
|
|
14
|
-
try {
|
|
15
|
-
// Check cache first
|
|
16
|
-
const cacheKey = `searchconsole-${siteName}-${startDate || 'default'}-${endDate || 'default'}`;
|
|
17
|
-
const cached = searchConsoleCache.get(cacheKey);
|
|
18
|
-
if (cached) {
|
|
19
|
-
return { success: true, data: cached };
|
|
20
|
-
}
|
|
21
|
-
if (!config.siteUrl) {
|
|
22
|
-
return {
|
|
23
|
-
success: false,
|
|
24
|
-
error: 'Site URL not configured for Search Console'
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
// Set up authentication
|
|
28
|
-
const authResult = await createSearchConsoleClient(config);
|
|
29
|
-
if (!authResult.success) {
|
|
30
|
-
return {
|
|
31
|
-
success: false,
|
|
32
|
-
error: authResult.error || 'Authentication failed'
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
const searchconsole = authResult.client;
|
|
36
|
-
// Calculate date ranges
|
|
37
|
-
const currentEndDate = endDate ? new Date(endDate) : new Date();
|
|
38
|
-
const currentStartDate = startDate ? new Date(startDate) : new Date(currentEndDate.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
39
|
-
// Calculate previous period (same duration before the current period)
|
|
40
|
-
const periodDuration = currentEndDate.getTime() - currentStartDate.getTime();
|
|
41
|
-
const previousEndDate = new Date(currentStartDate.getTime() - 24 * 60 * 60 * 1000); // One day before start
|
|
42
|
-
const previousStartDate = new Date(previousEndDate.getTime() - periodDuration);
|
|
43
|
-
const currentStartStr = currentStartDate.toISOString().split('T')[0];
|
|
44
|
-
const currentEndStr = currentEndDate.toISOString().split('T')[0];
|
|
45
|
-
const previousStartStr = previousStartDate.toISOString().split('T')[0];
|
|
46
|
-
const previousEndStr = previousEndDate.toISOString().split('T')[0];
|
|
47
|
-
// Fetch current period data
|
|
48
|
-
const currentResponse = await searchconsole.searchanalytics.query({
|
|
49
|
-
siteUrl: config.siteUrl,
|
|
50
|
-
requestBody: {
|
|
51
|
-
startDate: currentStartStr,
|
|
52
|
-
endDate: currentEndStr,
|
|
53
|
-
dimensions: ['date'],
|
|
54
|
-
rowLimit: 10000,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
// Fetch previous period data
|
|
58
|
-
const previousResponse = await searchconsole.searchanalytics.query({
|
|
59
|
-
siteUrl: config.siteUrl,
|
|
60
|
-
requestBody: {
|
|
61
|
-
startDate: previousStartStr,
|
|
62
|
-
endDate: previousEndStr,
|
|
63
|
-
dimensions: ['date'],
|
|
64
|
-
rowLimit: 10000,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
// Create a map of previous period data by date
|
|
68
|
-
const previousDataMap = new Map();
|
|
69
|
-
previousResponse.data.rows?.forEach((row) => {
|
|
70
|
-
const dateStr = row.keys?.[0] || '';
|
|
71
|
-
if (dateStr) {
|
|
72
|
-
previousDataMap.set(dateStr, {
|
|
73
|
-
clicks: parseFloat(String(row.clicks || '0')),
|
|
74
|
-
impressions: parseFloat(String(row.impressions || '0'))
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
// Combine current and previous period data
|
|
79
|
-
const chartData = [];
|
|
80
|
-
const daysInRange = Math.ceil((currentEndDate.getTime() - currentStartDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
81
|
-
for (let i = daysInRange - 1; i >= 0; i--) {
|
|
82
|
-
const currentDate = new Date(currentEndDate);
|
|
83
|
-
currentDate.setDate(currentDate.getDate() - i);
|
|
84
|
-
const currentDateStr = currentDate.toISOString().split('T')[0];
|
|
85
|
-
// Calculate corresponding previous period date
|
|
86
|
-
const previousDate = new Date(currentDate.getTime() - periodDuration);
|
|
87
|
-
const previousDateStr = previousDate.toISOString().split('T')[0];
|
|
88
|
-
// Get current period data
|
|
89
|
-
const currentRow = currentResponse.data.rows?.find((row) => row.keys?.[0] === currentDateStr);
|
|
90
|
-
const currentClicks = parseFloat(String(currentRow?.clicks || '0'));
|
|
91
|
-
const currentImpressions = parseFloat(String(currentRow?.impressions || '0'));
|
|
92
|
-
// Get previous period data
|
|
93
|
-
const previousData = previousDataMap.get(previousDateStr) || { clicks: 0, impressions: 0 };
|
|
94
|
-
// Format date for display
|
|
95
|
-
const formattedDate = currentDate.toLocaleDateString('en-US', {
|
|
96
|
-
month: 'short',
|
|
97
|
-
day: 'numeric'
|
|
98
|
-
});
|
|
99
|
-
chartData.push({
|
|
100
|
-
date: formattedDate,
|
|
101
|
-
currentImpressions: Math.round(currentImpressions),
|
|
102
|
-
currentClicks: Math.round(currentClicks),
|
|
103
|
-
previousImpressions: Math.round(previousData.impressions),
|
|
104
|
-
previousClicks: Math.round(previousData.clicks),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
// Cache the result
|
|
108
|
-
searchConsoleCache.set(cacheKey, chartData);
|
|
109
|
-
return { success: true, data: chartData };
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
console.error('Google Search Console error:', error);
|
|
113
|
-
return {
|
|
114
|
-
success: false,
|
|
115
|
-
error: error.message
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
6
|
+
export {};
|
|
@@ -9,36 +9,30 @@ SiteHealthGoogleSearchConsole.propTypes = {
|
|
|
9
9
|
endDate: PropTypes.string,
|
|
10
10
|
};
|
|
11
11
|
export function SiteHealthGoogleSearchConsole({ siteName, startDate, endDate }) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const result = await response.json();
|
|
23
|
-
if (!result.success) {
|
|
24
|
-
// Handle specific error types
|
|
25
|
-
if (result.error?.includes('invalid_grant') || result.error?.includes('authentication')) {
|
|
26
|
-
throw new Error('Google Search Console authentication expired. Please re-authorize the application.');
|
|
27
|
-
}
|
|
28
|
-
else if (result.error?.includes('GSC Site URL not configured')) {
|
|
29
|
-
throw new Error('GSC Site URL not configured for this site');
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
throw new Error(result.error || 'Failed to load search console data');
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return result.data;
|
|
36
|
-
};
|
|
37
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Google Search Console", columnSpan: 2, fetchData: fetchSearchConsoleData, children: (data) => {
|
|
38
|
-
if (!data || data.length === 0) {
|
|
12
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "Google Search Console", columnSpan: 2, endpoint: {
|
|
13
|
+
endpoint: '/api/site-health/google-search-console',
|
|
14
|
+
params: {
|
|
15
|
+
...(startDate && { startDate }),
|
|
16
|
+
...(endDate && { endDate }),
|
|
17
|
+
},
|
|
18
|
+
responseTransformer: (result) => result.data, // Extract the data array from the response
|
|
19
|
+
}, children: (data) => {
|
|
20
|
+
// Ensure data is an array
|
|
21
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
39
22
|
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "No indexing data available for the selected date range" }) }));
|
|
40
23
|
}
|
|
41
|
-
|
|
24
|
+
// Filter out any invalid data points
|
|
25
|
+
const validData = data.filter((point) => point &&
|
|
26
|
+
typeof point === 'object' &&
|
|
27
|
+
typeof point.date === 'string' &&
|
|
28
|
+
typeof point.currentImpressions === 'number' &&
|
|
29
|
+
typeof point.currentClicks === 'number' &&
|
|
30
|
+
typeof point.previousImpressions === 'number' &&
|
|
31
|
+
typeof point.previousClicks === 'number');
|
|
32
|
+
if (validData.length === 0) {
|
|
33
|
+
return (_jsx("div", { className: "health-visualization-placeholder", children: _jsx("div", { className: "health-text-secondary", children: "Invalid data format received from Google Search Console API." }) }));
|
|
34
|
+
}
|
|
35
|
+
return (_jsx("div", { children: _jsx("div", { style: { width: '100%', height: '400px', border: '1px solid #ddd' }, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(ComposedChart, { data: validData, margin: { top: 40, right: 30, left: 20, bottom: 5 }, children: [_jsx("text", { x: "50%", y: 20, textAnchor: "middle", fontSize: "16", fontWeight: "bold", fill: "#374151", children: "Impressions vs Clicks (Current vs Previous Period)" }), _jsx(CartesianGrid, { strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "date", tick: { fontSize: 12 }, angle: -45, textAnchor: "end", height: 60 }), _jsx(YAxis, { tick: { fontSize: 12 } }), _jsx(Tooltip, { formatter: (value, name) => [
|
|
42
36
|
value?.toLocaleString() || '0',
|
|
43
37
|
name || 'Unknown'
|
|
44
38
|
], labelFormatter: (label) => `Date: ${label}` }), _jsx(Legend, { wrapperStyle: {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
import { SiteHealthTemplate } from './site-health-template';
|
|
5
|
-
import {
|
|
5
|
+
import { formatAuditItem, getAuditScoreIcon } from './site-health-utils';
|
|
6
6
|
/**
|
|
7
7
|
* Restructure audits by type with all pages and their individual results
|
|
8
8
|
*/
|
|
@@ -61,42 +61,14 @@ function restructureAuditsByType(pagesAnalyzed) {
|
|
|
61
61
|
});
|
|
62
62
|
return restructuredAudits.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
63
63
|
}
|
|
64
|
-
// Fetch real SEO data from API
|
|
65
|
-
async function fetchOnSiteSEOData(siteName) {
|
|
66
|
-
try {
|
|
67
|
-
const response = await fetch(`/api/site-health/on-site-seo?siteName=${encodeURIComponent(siteName)}`);
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
70
|
-
}
|
|
71
|
-
const data = await response.json();
|
|
72
|
-
if (data.status === 'error') {
|
|
73
|
-
throw new Error(data.error || 'SEO analysis failed');
|
|
74
|
-
}
|
|
75
|
-
// Process data to aggregate audits by type across all pages
|
|
76
|
-
// const aggregatedOnPageAudits = restructureAuditsByType(data.pagesAnalyzed);
|
|
77
|
-
return data;
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
console.error('Error fetching SEO data:', error);
|
|
81
|
-
// Return error structure that the template can display
|
|
82
|
-
return {
|
|
83
|
-
site: siteName,
|
|
84
|
-
url: '',
|
|
85
|
-
overallScore: null,
|
|
86
|
-
pagesAnalyzed: [],
|
|
87
|
-
onSiteAudits: [],
|
|
88
|
-
totalPages: 0,
|
|
89
|
-
timestamp: new Date().toISOString(),
|
|
90
|
-
status: 'error',
|
|
91
|
-
error: error instanceof Error ? error.message : 'Failed to fetch SEO data'
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
64
|
SiteHealthOnSiteSEO.propTypes = {
|
|
96
65
|
siteName: PropTypes.string.isRequired,
|
|
97
66
|
};
|
|
98
67
|
export function SiteHealthOnSiteSEO({ siteName }) {
|
|
99
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "On-Site SEO",
|
|
68
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "On-Site SEO", endpoint: {
|
|
69
|
+
endpoint: '/api/site-health/on-site-seo',
|
|
70
|
+
responseTransformer: (result) => result.data, // Extract the data from the response
|
|
71
|
+
}, children: (data) => {
|
|
100
72
|
if (!data)
|
|
101
73
|
return null;
|
|
102
74
|
if (data.status === 'error') {
|
|
@@ -104,12 +76,6 @@ export function SiteHealthOnSiteSEO({ siteName }) {
|
|
|
104
76
|
}
|
|
105
77
|
// Process data to aggregate audits by type across all pages
|
|
106
78
|
const aggregatedOnPageAudits = restructureAuditsByType(data.pagesAnalyzed);
|
|
107
|
-
const getScoreColor = (score) => {
|
|
108
|
-
return getScoreIndicator(score).color;
|
|
109
|
-
};
|
|
110
|
-
const getAuditScoreIcon = (score) => {
|
|
111
|
-
return getScoreIndicator(score).icon;
|
|
112
|
-
};
|
|
113
79
|
const formatPageIssue = (item) => {
|
|
114
80
|
// Handle page-specific results from restructured data
|
|
115
81
|
if (item.page && typeof item.page === 'string') {
|
|
@@ -153,52 +119,12 @@ export function SiteHealthOnSiteSEO({ siteName }) {
|
|
|
153
119
|
// Fallback to original formatting
|
|
154
120
|
return formatAuditItem(item);
|
|
155
121
|
};
|
|
156
|
-
|
|
157
|
-
// Handle specific SEO element formatting based on the user's list
|
|
158
|
-
// Title Tags
|
|
159
|
-
if (item.title && typeof item.title === 'string') {
|
|
160
|
-
return `Title: "${item.title}" (${item.length || 'unknown'} chars)`;
|
|
161
|
-
}
|
|
162
|
-
// Meta tags
|
|
163
|
-
if (item.name && typeof item.name === 'string' && item.content) {
|
|
164
|
-
return `${item.name}: ${String(item.content)}`;
|
|
165
|
-
}
|
|
166
|
-
// Headings
|
|
167
|
-
if (item.tag && typeof item.tag === 'string' && item.tag.match(/^h[1-6]$/i)) {
|
|
168
|
-
return `${item.tag}: "${item.text || ''}"`;
|
|
169
|
-
}
|
|
170
|
-
// Links and URLs
|
|
171
|
-
if (item.url && typeof item.url === 'string') {
|
|
172
|
-
if (item.issue && typeof item.issue === 'string') {
|
|
173
|
-
return `${item.url} - ${item.issue}`;
|
|
174
|
-
}
|
|
175
|
-
return item.url;
|
|
176
|
-
}
|
|
177
|
-
// Images with alt tags
|
|
178
|
-
if (item.src && typeof item.src === 'string') {
|
|
179
|
-
return `Image: ${item.src} ${item.alt ? '(has alt)' : '(missing alt)'}`;
|
|
180
|
-
}
|
|
181
|
-
// Robots.txt directives
|
|
182
|
-
if (item.directive && typeof item.directive === 'string') {
|
|
183
|
-
return `${item.directive}: ${item.value || ''}`;
|
|
184
|
-
}
|
|
185
|
-
// Sitemap entries
|
|
186
|
-
if (item.loc && typeof item.loc === 'string') {
|
|
187
|
-
return item.loc;
|
|
188
|
-
}
|
|
189
|
-
// Default formatting
|
|
190
|
-
return Object.entries(item)
|
|
191
|
-
.map(([key, value]) => `${key}: ${String(value)}`)
|
|
192
|
-
.join(', ');
|
|
193
|
-
};
|
|
194
|
-
return (_jsxs(_Fragment, { children: [_jsx("h4", { className: "health-site-name", children: data.site.replace('-', ' ') }), _jsxs("p", { className: "health-site-url", children: ["URL: ", data.url] }), data.overallScore !== null && (_jsx("div", { className: "health-score-container", children: _jsxs("div", { className: "health-score-item", children: [_jsx("div", { className: "health-score-label", children: "On-Site SEO Score" }), _jsxs("div", { className: "health-score-value", style: { color: getScoreColor(data.overallScore) }, children: [Math.round((data.overallScore || 0) * 100), "%"] }), _jsx("div", { className: "health-score-bar", children: _jsx("div", { className: "health-score-fill", style: {
|
|
195
|
-
width: `${(data.overallScore || 0) * 100}%`,
|
|
196
|
-
backgroundColor: getScoreColor(data.overallScore)
|
|
197
|
-
} }) })] }) })), aggregatedOnPageAudits.length > 0 && (_jsxs("div", { children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "On-Page SEO Audits" }), _jsx("div", { className: "health-audit-list", children: aggregatedOnPageAudits
|
|
122
|
+
return (_jsxs(_Fragment, { children: [aggregatedOnPageAudits.length > 0 && (_jsxs("div", { children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "On-Page SEO Audits" }), _jsx("div", { className: "health-audit-list", children: aggregatedOnPageAudits
|
|
198
123
|
.filter(audit => audit.scoreDisplayMode !== 'notApplicable')
|
|
124
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
199
125
|
.map((audit) => (_jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", children: getAuditScoreIcon(audit.score) }), _jsxs("div", { className: "health-audit-content", children: [_jsxs("span", { className: "health-audit-title", children: [audit.score === null ? '(N/A)' : `(${Math.round((audit.score || 0) * 100)}%)`, " ", audit.title] }), audit.displayValue && audit.score !== 1 && (_jsx("p", { className: "health-audit-description", children: audit.displayValue })), audit.details && audit.details.items && Array.isArray(audit.details.items) && audit.details.items.length > 0 && audit.score !== 1 && (_jsx("div", { className: "health-audit-details", children: _jsx("div", { style: { fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }, children: audit.details.items
|
|
200
126
|
.filter((item) => item.score !== 1)
|
|
201
|
-
.map((item, idx) => (_jsx("div", { style: { marginBottom: '0.125rem' }, children: formatPageIssue(item) }, idx))) }) }))] })] }, audit.id))) })] })), data.onSiteAudits.length > 0 && (_jsxs("div", { style: { marginTop:
|
|
127
|
+
.map((item, idx) => (_jsx("div", { style: { marginBottom: '0.125rem' }, children: formatPageIssue(item) }, idx))) }) }))] })] }, audit.id))) })] })), data.onSiteAudits.length > 0 && (_jsxs("div", { style: { marginTop: aggregatedOnPageAudits.length > 0 ? '2rem' : '0' }, children: [_jsx("h5", { style: { fontSize: '1rem', fontWeight: '600', marginBottom: '1rem' }, children: "On-Site SEO Audits" }), _jsx("div", { className: "health-audit-list", children: data.onSiteAudits
|
|
202
128
|
.filter(audit => audit.scoreDisplayMode !== 'notApplicable')
|
|
203
129
|
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
204
130
|
.map((audit) => (_jsxs("div", { className: "health-audit-item", children: [_jsx("span", { className: "health-audit-icon", children: getAuditScoreIcon(audit.score) }), _jsxs("div", { className: "health-audit-content", children: [_jsxs("span", { className: "health-audit-title", children: [audit.score === null ? '(N/A)' : `(${Math.round((audit.score || 0) * 100)}%)`, " ", audit.title] }), audit.displayValue && audit.score !== 1 && (_jsx("p", { className: "health-audit-description", children: audit.displayValue })), audit.details && audit.details.items && Array.isArray(audit.details.items) && audit.details.items.length > 0 && audit.score !== 1 && (_jsx("div", { className: "health-audit-details", children: _jsx("div", { style: { fontSize: '0.75rem', color: '#6b7280', marginTop: '0.25rem' }, children: audit.details.items
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
3
|
import PropTypes from 'prop-types';
|
|
5
4
|
import { SiteHealthTemplate } from './site-health-template';
|
|
6
|
-
import {
|
|
5
|
+
import { formatScore, getScoreColor } from './site-health-utils';
|
|
7
6
|
SiteHealthOverview.propTypes = {
|
|
8
7
|
siteName: PropTypes.string.isRequired,
|
|
9
8
|
};
|
|
10
9
|
export function SiteHealthOverview({ siteName }) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
throw new Error(result.error || 'Failed to fetch Core Web Vitals data');
|
|
16
|
-
}
|
|
17
|
-
return result;
|
|
18
|
-
}, []);
|
|
19
|
-
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Site Overview", fetchData: fetchCWVData, children: (data) => {
|
|
10
|
+
return (_jsx(SiteHealthTemplate, { siteName: siteName, title: "PageSpeed - Site Overview", endpoint: {
|
|
11
|
+
endpoint: '/api/site-health/core-web-vitals',
|
|
12
|
+
responseTransformer: (result) => result, // Result is already in the correct format
|
|
13
|
+
}, children: (data) => {
|
|
20
14
|
if (!data?.data || data.data.length === 0) {
|
|
21
15
|
return (_jsx("p", { style: { color: '#6b7280' }, children: "No site health data available for this site." }));
|
|
22
16
|
}
|
|
@@ -25,14 +19,6 @@ export function SiteHealthOverview({ siteName }) {
|
|
|
25
19
|
return (_jsxs("p", { style: { color: '#ef4444', fontSize: '0.875rem' }, children: ["Error: ", siteData.error] }));
|
|
26
20
|
}
|
|
27
21
|
// Helper functions
|
|
28
|
-
const getScoreColor = (score) => {
|
|
29
|
-
return getScoreIndicator(score).color;
|
|
30
|
-
};
|
|
31
|
-
const formatScore = (score) => {
|
|
32
|
-
if (score === null)
|
|
33
|
-
return 'N/A';
|
|
34
|
-
return `${Math.round(score * 100)}%`;
|
|
35
|
-
};
|
|
36
22
|
const getStatusColor = (status) => {
|
|
37
23
|
switch (status) {
|
|
38
24
|
case 'good': return '#10b981'; // green
|