@pixelated-tech/components 3.14.4 → 3.15.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/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +21 -8
- package/dist/components/admin/site-health/site-health-github.integration.js +6 -6
- package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +36 -16
- package/dist/components/admin/site-health/site-health-template.js +10 -6
- package/dist/components/config/config.types.js +12 -0
- package/dist/components/general/markdown.js +35 -0
- package/dist/components/general/nerdjoke.js +2 -4
- package/dist/components/general/proxy-handler.js +2 -2
- package/dist/components/general/sitemap.js +2 -4
- package/dist/components/general/smartfetch.js +211 -0
- package/dist/components/general/tiles.js +1 -1
- package/dist/components/general/urlbuilder.js +74 -0
- package/dist/components/integrations/contentful.delivery.js +24 -20
- package/dist/components/integrations/contentful.management.js +188 -151
- package/dist/components/integrations/flickr.js +15 -22
- package/dist/components/integrations/gemini-api.client.js +22 -21
- package/dist/components/integrations/gemini-api.server.js +50 -46
- package/dist/components/integrations/google.reviews.functions.js +19 -5
- package/dist/components/integrations/googleplaces.js +33 -9
- package/dist/components/integrations/gravatar.functions.js +15 -7
- package/dist/components/integrations/hubspot.components.js +8 -10
- package/dist/components/integrations/instagram.functions.js +9 -4
- package/dist/components/integrations/lipsum.js +6 -10
- package/dist/components/integrations/loremipsum.js +21 -21
- package/dist/components/integrations/socialcard.js +14 -8
- package/dist/components/integrations/spotify.functions.js +7 -4
- package/dist/components/integrations/wordpress.functions.js +17 -19
- package/dist/components/integrations/yelp.js +6 -7
- package/dist/components/shoppingcart/ebay.functions.js +69 -53
- package/dist/components/shoppingcart/shoppingcart.components.js +1 -1
- package/dist/components/sitebuilder/config/google-fonts.js +13 -6
- package/dist/components/sitebuilder/form/formbuilder.js +1 -1
- package/dist/components/sitebuilder/form/formengine.js +37 -10
- package/dist/components/sitebuilder/form/formsubmit.js +205 -0
- package/dist/components/sitebuilder/page/components/SaveLoadSection.js +24 -12
- package/dist/config/pixelated.config.json.enc +1 -1
- package/dist/data/form.json +7 -0
- package/dist/index.js +4 -2
- package/dist/index.server.js +3 -1
- package/dist/scripts/pixelated-eslint-plugin.js +51 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
- package/dist/types/components/config/config.types.d.ts +11 -0
- package/dist/types/components/config/config.types.d.ts.map +1 -1
- package/dist/types/components/general/markdown.d.ts +12 -0
- package/dist/types/components/general/markdown.d.ts.map +1 -1
- package/dist/types/components/general/nerdjoke.d.ts.map +1 -1
- package/dist/types/components/general/proxy-handler.d.ts.map +1 -1
- package/dist/types/components/general/sitemap.d.ts.map +1 -1
- package/dist/types/components/general/smartfetch.d.ts +85 -0
- package/dist/types/components/general/smartfetch.d.ts.map +1 -0
- package/dist/types/components/general/tiles.d.ts.map +1 -1
- package/dist/types/components/general/urlbuilder.d.ts +64 -0
- package/dist/types/components/general/urlbuilder.d.ts.map +1 -0
- package/dist/types/components/integrations/contentful.delivery.d.ts.map +1 -1
- package/dist/types/components/integrations/contentful.management.d.ts.map +1 -1
- package/dist/types/components/integrations/flickr.d.ts.map +1 -1
- package/dist/types/components/integrations/gemini-api.client.d.ts.map +1 -1
- package/dist/types/components/integrations/gemini-api.server.d.ts +1 -1
- package/dist/types/components/integrations/gemini-api.server.d.ts.map +1 -1
- package/dist/types/components/integrations/google.reviews.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/googleplaces.d.ts.map +1 -1
- package/dist/types/components/integrations/gravatar.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/hubspot.components.d.ts.map +1 -1
- package/dist/types/components/integrations/instagram.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/lipsum.d.ts.map +1 -1
- package/dist/types/components/integrations/loremipsum.d.ts.map +1 -1
- package/dist/types/components/integrations/socialcard.d.ts.map +1 -1
- package/dist/types/components/integrations/spotify.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/wordpress.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/yelp.d.ts.map +1 -1
- package/dist/types/components/shoppingcart/ebay.functions.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/google-fonts.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/form/formengine.d.ts +4 -4
- package/dist/types/components/sitebuilder/form/formengine.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/form/{formutils.d.ts → formengineutilities.d.ts} +1 -1
- package/dist/types/components/sitebuilder/form/formengineutilities.d.ts.map +1 -0
- package/dist/types/components/sitebuilder/form/formsubmit.d.ts +70 -0
- package/dist/types/components/sitebuilder/form/formsubmit.d.ts.map +1 -0
- package/dist/types/components/sitebuilder/page/components/SaveLoadSection.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.server.d.ts +3 -1
- package/dist/types/scripts/pixelated-eslint-plugin.d.ts +21 -0
- package/dist/types/stories/admin/contentful-migration.stories.d.ts +43 -0
- package/dist/types/stories/admin/contentful-migration.stories.d.ts.map +1 -1
- package/dist/types/stories/general/text-generation.stories.d.ts +116 -0
- package/dist/types/stories/general/text-generation.stories.d.ts.map +1 -0
- package/dist/types/stories/integrations/google.reviews.stories.d.ts +52 -0
- package/dist/types/stories/integrations/google.reviews.stories.d.ts.map +1 -1
- package/dist/types/stories/integrations/gravatar.stories.d.ts.map +1 -1
- package/dist/types/stories/integrations/instagram.stories.d.ts +38 -0
- package/dist/types/stories/integrations/instagram.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/form-engine.stories.d.ts +13 -7
- package/dist/types/stories/sitebuilder/form-engine.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/form.honeypot.stories.d.ts +0 -19
- package/dist/types/stories/sitebuilder/form.honeypot.stories.d.ts.map +1 -1
- package/dist/types/test/test-utils.d.ts +2 -0
- package/dist/types/test/test-utils.d.ts.map +1 -1
- package/dist/types/tests/formengineutilities.test.d.ts +2 -0
- package/dist/types/tests/formengineutilities.test.d.ts.map +1 -0
- package/dist/types/tests/google-apis.test.d.ts +2 -0
- package/dist/types/tests/google-apis.test.d.ts.map +1 -0
- package/dist/types/tests/google-fonts.test.d.ts +2 -0
- package/dist/types/tests/google-fonts.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-core-web-vitals.test.d.ts +2 -0
- package/dist/types/tests/site-health-core-web-vitals.test.d.ts.map +1 -0
- package/dist/types/tests/smartfetch.test.d.ts +2 -0
- package/dist/types/tests/smartfetch.test.d.ts.map +1 -0
- package/dist/types/tests/social-media-apis.test.d.ts +7 -0
- package/dist/types/tests/social-media-apis.test.d.ts.map +1 -0
- package/dist/types/tests/specialized-apis.test.d.ts +7 -0
- package/dist/types/tests/specialized-apis.test.d.ts.map +1 -0
- package/dist/types/tests/urlbuilder.test.d.ts +2 -0
- package/dist/types/tests/urlbuilder.test.d.ts.map +1 -0
- package/dist/types/tests/useFormSubmit.test.d.ts +2 -0
- package/dist/types/tests/useFormSubmit.test.d.ts.map +1 -0
- package/package.json +6 -6
- package/dist/components/sitebuilder/form/formemailer.js +0 -119
- package/dist/types/components/sitebuilder/form/formemailer.d.ts +0 -3
- package/dist/types/components/sitebuilder/form/formemailer.d.ts.map +0 -1
- package/dist/types/components/sitebuilder/form/formutils.d.ts.map +0 -1
- package/dist/types/stories/integrations/lipsum.stories.d.ts +0 -38
- package/dist/types/stories/integrations/lipsum.stories.d.ts.map +0 -1
- package/dist/types/stories/integrations/loremipsum.stories.d.ts +0 -46
- package/dist/types/stories/integrations/loremipsum.stories.d.ts.map +0 -1
- package/dist/types/tests/formemailer.honeypot.test.d.ts +0 -2
- package/dist/types/tests/formemailer.honeypot.test.d.ts.map +0 -1
- /package/dist/components/sitebuilder/form/{formutils.js → formengineutilities.js} +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
const debug = false;
|
|
3
3
|
import { getFullPixelatedConfig } from '../../config/config';
|
|
4
|
+
import { smartFetch } from '../../general/smartfetch';
|
|
5
|
+
import { buildUrl } from '../../general/urlbuilder';
|
|
4
6
|
const psiCache = new Map();
|
|
5
7
|
const CACHE_TTL_SUCCESS = 60 * 60 * 1000; // 1 hour for successful results
|
|
6
8
|
const CACHE_TTL_ERROR = 5 * 60 * 1000; // 5 minutes for error results
|
|
@@ -86,12 +88,20 @@ export async function performCoreWebVitalsAnalysis(url, siteName, useCache = tru
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
export async function fetchPSIData(url) {
|
|
89
|
-
// Require the API key from the unified pixelated.config.json. No environment fallback.
|
|
90
|
-
const apiKey = getFullPixelatedConfig()?.
|
|
91
|
+
// Require the PSI API key from the unified pixelated.config.json. No environment fallback.
|
|
92
|
+
const apiKey = getFullPixelatedConfig()?.googlePSI?.api_key;
|
|
91
93
|
if (!apiKey) {
|
|
92
|
-
throw new Error('Google API key is not set; set
|
|
94
|
+
throw new Error('Google PSI API key is not set; set googlePSI.api_key in pixelated.config.json');
|
|
93
95
|
}
|
|
94
|
-
const psiUrl =
|
|
96
|
+
const psiUrl = buildUrl({
|
|
97
|
+
baseUrl: 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed',
|
|
98
|
+
params: {
|
|
99
|
+
url,
|
|
100
|
+
key: apiKey,
|
|
101
|
+
strategy: 'mobile',
|
|
102
|
+
category: 'performance,accessibility,best-practices,seo'
|
|
103
|
+
}
|
|
104
|
+
});
|
|
95
105
|
const fetchWithRetry = async (url, maxRetries = 2) => {
|
|
96
106
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
97
107
|
try {
|
|
@@ -100,10 +110,13 @@ export async function fetchPSIData(url) {
|
|
|
100
110
|
const controller = new AbortController();
|
|
101
111
|
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
|
|
102
112
|
const start = Date.now();
|
|
103
|
-
const response = await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
const response = await smartFetch(url, {
|
|
114
|
+
responseType: 'ok',
|
|
115
|
+
requestInit: {
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
headers: {
|
|
118
|
+
'User-Agent': 'Mozilla/5.0 (compatible; SiteHealthMonitor/1.0)'
|
|
119
|
+
}
|
|
107
120
|
}
|
|
108
121
|
});
|
|
109
122
|
const elapsed = Date.now() - start;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { getFullPixelatedConfig } from '../../config/config';
|
|
3
|
+
import { buildUrl } from '../../general/urlbuilder';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
// Debug logging is off by default. Set to true/false here (do not use env vars).
|
|
5
6
|
const debug = false;
|
|
@@ -61,12 +62,11 @@ export async function analyzeGitHealth(siteConfig, startDate, endDate, httpFetch
|
|
|
61
62
|
'Accept': 'application/vnd.github+json',
|
|
62
63
|
'Authorization': `token ${token}`
|
|
63
64
|
};
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const commitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?${params.toString()}`;
|
|
65
|
+
const commitsUrl = buildUrl({
|
|
66
|
+
baseUrl: 'https://api.github.com',
|
|
67
|
+
pathSegments: ['repos', owner, repo, 'commits'],
|
|
68
|
+
params: { ...(since && { since }), ...(until && { until }) }
|
|
69
|
+
});
|
|
70
70
|
const fetcher = httpFetch || globalThis.fetch;
|
|
71
71
|
const commitsRes = await fetcher(commitsUrl, { headers });
|
|
72
72
|
if (!commitsRes.ok) {
|
|
@@ -8,6 +8,7 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import puppeteer from 'puppeteer';
|
|
11
|
+
import { smartFetch } from '../../general/smartfetch';
|
|
11
12
|
import { EXCLUDED_URL_PATTERNS, EXCLUDED_FILE_EXTENSIONS, EXCLUDED_DIRECTORY_NAMES } from './seo-constants';
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
14
|
const __dirname = path.dirname(__filename);
|
|
@@ -373,10 +374,13 @@ function calculateFacetedNavigationScore(data) {
|
|
|
373
374
|
*/
|
|
374
375
|
async function collectBrowserCachingData(url) {
|
|
375
376
|
try {
|
|
376
|
-
const response = await
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
377
|
+
const response = await smartFetch(url, {
|
|
378
|
+
responseType: 'ok',
|
|
379
|
+
requestInit: {
|
|
380
|
+
method: 'HEAD',
|
|
381
|
+
headers: {
|
|
382
|
+
'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)'
|
|
383
|
+
}
|
|
380
384
|
}
|
|
381
385
|
});
|
|
382
386
|
if (!response.ok) {
|
|
@@ -486,11 +490,14 @@ function calculateBrowserCachingScore(data) {
|
|
|
486
490
|
*/
|
|
487
491
|
async function collectGzipCompressionData(url) {
|
|
488
492
|
try {
|
|
489
|
-
const response = await
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
493
|
+
const response = await smartFetch(url, {
|
|
494
|
+
responseType: 'ok',
|
|
495
|
+
requestInit: {
|
|
496
|
+
method: 'GET',
|
|
497
|
+
headers: {
|
|
498
|
+
'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)',
|
|
499
|
+
'Accept-Encoding': 'gzip, deflate'
|
|
500
|
+
}
|
|
494
501
|
}
|
|
495
502
|
});
|
|
496
503
|
if (!response.ok) {
|
|
@@ -733,8 +740,11 @@ async function crawlSite(baseUrl, maxPages = 10) {
|
|
|
733
740
|
visited.add(currentUrl);
|
|
734
741
|
discovered.push(currentUrl);
|
|
735
742
|
try {
|
|
736
|
-
const response = await
|
|
737
|
-
|
|
743
|
+
const response = await smartFetch(currentUrl, {
|
|
744
|
+
responseType: 'ok',
|
|
745
|
+
requestInit: {
|
|
746
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)' }
|
|
747
|
+
}
|
|
738
748
|
});
|
|
739
749
|
if (!response.ok)
|
|
740
750
|
continue;
|
|
@@ -1018,7 +1028,9 @@ async function performSiteWideAudits(baseUrl) {
|
|
|
1018
1028
|
case 'robots-txt':
|
|
1019
1029
|
try {
|
|
1020
1030
|
const robotsUrl = `${protocol}//${baseDomain}/robots.txt`;
|
|
1021
|
-
const robotsResponse = await
|
|
1031
|
+
const robotsResponse = await smartFetch(robotsUrl, {
|
|
1032
|
+
responseType: 'ok'
|
|
1033
|
+
});
|
|
1022
1034
|
score = robotsResponse.ok ? 1 : 0;
|
|
1023
1035
|
displayValue = score ? 'Robots.txt accessible' : 'Robots.txt not found or inaccessible';
|
|
1024
1036
|
}
|
|
@@ -1030,7 +1042,9 @@ async function performSiteWideAudits(baseUrl) {
|
|
|
1030
1042
|
case 'sitemap-xml':
|
|
1031
1043
|
try {
|
|
1032
1044
|
const sitemapUrl = `${protocol}//${baseDomain}/sitemap.xml`;
|
|
1033
|
-
const sitemapResponse = await
|
|
1045
|
+
const sitemapResponse = await smartFetch(sitemapUrl, {
|
|
1046
|
+
responseType: 'ok'
|
|
1047
|
+
});
|
|
1034
1048
|
score = sitemapResponse.ok ? 1 : 0;
|
|
1035
1049
|
displayValue = score ? 'Sitemap.xml accessible' : 'Sitemap.xml not found or inaccessible';
|
|
1036
1050
|
}
|
|
@@ -1054,7 +1068,9 @@ async function performSiteWideAudits(baseUrl) {
|
|
|
1054
1068
|
case 'manifest-file':
|
|
1055
1069
|
try {
|
|
1056
1070
|
const manifestUrl = `${protocol}//${baseDomain}/manifest.webmanifest`;
|
|
1057
|
-
const manifestResponse = await
|
|
1071
|
+
const manifestResponse = await smartFetch(manifestUrl, {
|
|
1072
|
+
responseType: 'ok'
|
|
1073
|
+
});
|
|
1058
1074
|
score = manifestResponse.ok ? 1 : 0;
|
|
1059
1075
|
displayValue = score ? 'Manifest.webmanifest accessible' : 'Manifest.webmanifest not found or inaccessible';
|
|
1060
1076
|
}
|
|
@@ -1129,7 +1145,9 @@ async function getUrlsFromSitemap(baseUrl) {
|
|
|
1129
1145
|
const candidates = [`${baseUrl}/sitemap.xml`, `${baseUrl}/sitemap_index.xml`];
|
|
1130
1146
|
// Attempt to parse robots.txt for sitemap directives
|
|
1131
1147
|
try {
|
|
1132
|
-
const robotsResp = await
|
|
1148
|
+
const robotsResp = await smartFetch(`${baseUrl}/robots.txt`, {
|
|
1149
|
+
responseType: 'ok'
|
|
1150
|
+
});
|
|
1133
1151
|
if (robotsResp.ok) {
|
|
1134
1152
|
const robotsText = await robotsResp.text();
|
|
1135
1153
|
const sitemapRegex = /^sitemap:\s*(.+)$/gim;
|
|
@@ -1150,7 +1168,9 @@ async function getUrlsFromSitemap(baseUrl) {
|
|
|
1150
1168
|
for (const sitemapUrl of candidates) {
|
|
1151
1169
|
triedUrls.push(sitemapUrl);
|
|
1152
1170
|
try {
|
|
1153
|
-
const response = await
|
|
1171
|
+
const response = await smartFetch(sitemapUrl, {
|
|
1172
|
+
responseType: 'ok'
|
|
1173
|
+
});
|
|
1154
1174
|
if (!response.ok) {
|
|
1155
1175
|
console.warn(`Sitemap URL ${sitemapUrl} returned status ${response.status}`);
|
|
1156
1176
|
continue;
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react';
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
5
|
import { PageGridItem } from '../../general/semantic';
|
|
6
|
+
import { smartFetch } from '../../general/smartfetch';
|
|
6
7
|
import "./site-health.css";
|
|
7
8
|
import { useSiteHealthMockData } from './site-health-mock-context';
|
|
8
9
|
/**
|
|
@@ -73,13 +74,16 @@ export function SiteHealthTemplate(props) {
|
|
|
73
74
|
if (!useCache) {
|
|
74
75
|
url.searchParams.set('cache', 'false');
|
|
75
76
|
}
|
|
76
|
-
const response = await
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
const response = await smartFetch(url.toString(), {
|
|
78
|
+
responseType: 'ok',
|
|
79
|
+
requestInit: {
|
|
80
|
+
method,
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
...headers,
|
|
84
|
+
},
|
|
85
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
81
86
|
},
|
|
82
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
83
87
|
});
|
|
84
88
|
if (!response.ok) {
|
|
85
89
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
@@ -29,6 +29,18 @@ export const SECRET_CONFIG_KEYS = {
|
|
|
29
29
|
github: [
|
|
30
30
|
'token'
|
|
31
31
|
],
|
|
32
|
+
google: [
|
|
33
|
+
'api_key',
|
|
34
|
+
'client_id',
|
|
35
|
+
'client_secret',
|
|
36
|
+
'refresh_token'
|
|
37
|
+
],
|
|
38
|
+
googlePSI: [
|
|
39
|
+
'api_key'
|
|
40
|
+
],
|
|
41
|
+
googleGemini: [
|
|
42
|
+
'api_key'
|
|
43
|
+
],
|
|
32
44
|
instagram: [
|
|
33
45
|
'accessToken'
|
|
34
46
|
],
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* https://randyperkins2k.medium.com/writing-a-simple-markdown-parser-using-javascript-1f2e9449a558 */
|
|
4
|
+
import { useState, useEffect } from "react";
|
|
3
5
|
import PropTypes from "prop-types";
|
|
4
6
|
import { SmartImage } from "./smartimage";
|
|
5
7
|
import { usePixelatedConfig } from "../config/config.client";
|
|
8
|
+
import { smartFetch } from "./smartfetch";
|
|
6
9
|
import "./markdown.css";
|
|
7
10
|
/* ========== MARKDOWN ========== */
|
|
8
11
|
/**
|
|
@@ -43,3 +46,35 @@ export function Markdown(props) {
|
|
|
43
46
|
}
|
|
44
47
|
return (_jsx("div", { className: "section-container", children: _jsx("div", { className: "markdown", dangerouslySetInnerHTML: { __html: markdownParser(props.markdowndata) } }) }));
|
|
45
48
|
}
|
|
49
|
+
/* ========== HOOK: useFileData ========== */
|
|
50
|
+
/**
|
|
51
|
+
* useFileData — Load markdown or JSON files from /data/ directory
|
|
52
|
+
*
|
|
53
|
+
* @param {string} filePath - Path to file (e.g., '/data/readme.md')
|
|
54
|
+
* @param {string} responseType - 'text' or 'json' (default: 'text')
|
|
55
|
+
* @returns {Object} { data, loading, error }
|
|
56
|
+
*/
|
|
57
|
+
export function useFileData(filePath, responseType = 'text') {
|
|
58
|
+
const [data, setData] = useState(null);
|
|
59
|
+
const [loading, setLoading] = useState(true);
|
|
60
|
+
const [error, setError] = useState(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const fetchData = async () => {
|
|
63
|
+
try {
|
|
64
|
+
setLoading(true);
|
|
65
|
+
setError(null);
|
|
66
|
+
const result = await smartFetch(filePath, { responseType: responseType });
|
|
67
|
+
setData(result);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
setError(err instanceof Error ? err.message : 'Failed to load file');
|
|
71
|
+
setData(null);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
fetchData();
|
|
78
|
+
}, [filePath, responseType]);
|
|
79
|
+
return { data, loading, error };
|
|
80
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import PropTypes from "prop-types";
|
|
5
|
+
import { smartFetch } from '../general/smartfetch';
|
|
5
6
|
import "../../css/pixelated.grid.scss";
|
|
6
7
|
import "./nerdjoke.css";
|
|
7
8
|
const debug = false;
|
|
@@ -61,10 +62,7 @@ export function NerdJoke(props) {
|
|
|
61
62
|
const myURLProps = { command: "%2Fnerdjokes", text: "getjokejson" };
|
|
62
63
|
try {
|
|
63
64
|
const url = myURL + "command=" + myURLProps.command + "&text=" + myURLProps.text;
|
|
64
|
-
const
|
|
65
|
-
if (!response.ok)
|
|
66
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
67
|
-
const jokeData = await response.json();
|
|
65
|
+
const jokeData = await smartFetch(url);
|
|
68
66
|
setJoke(jokeData);
|
|
69
67
|
}
|
|
70
68
|
catch (error) {
|
|
@@ -46,12 +46,12 @@ export function handlePixelatedProxy(req) {
|
|
|
46
46
|
"default-src 'self'",
|
|
47
47
|
`script-src ${scriptSrc}`,
|
|
48
48
|
`script-src-elem ${scriptSrc}`,
|
|
49
|
-
"connect-src 'self' https: https://*.hubspot.com https
|
|
49
|
+
"connect-src 'self' https: https://*.hubspot.com https://*.pixelated.tech https://*.google-analytics.com https://*.analytics.google.com https://cdn.jsdelivr.net https://*.gravatar.com",
|
|
50
50
|
"img-src 'self' data: https: https://*.gravatar.com https://*.staticflickr.com https://*.ctfassets.net https://res.cloudinary.com https://*.ebayimg.com",
|
|
51
51
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://*.google.com https://www.paypalobjects.com https://cdn.curator.io",
|
|
52
52
|
"font-src 'self' data: https://fonts.gstatic.com",
|
|
53
53
|
"media-src 'self' https://*.ctfassets.net",
|
|
54
|
-
"frame-src 'self' https://*.hubspot.com https://*.googletagmanager.com https://*.adtrafficquality.google https://*.google.com https
|
|
54
|
+
"frame-src 'self' https://calendly.com https://*.hubspot.com https://*.googletagmanager.com https://*.adtrafficquality.google https://*.google.com https://*.calendly.com https://*.hsforms.net https://www.paypal.com https://www.paypalobjects.com https://syndicatedsearch.goog",
|
|
55
55
|
"frame-ancestors 'none'",
|
|
56
56
|
"object-src 'none'",
|
|
57
57
|
].join("; ");
|
|
@@ -7,6 +7,7 @@ import { getEbayAppToken, getEbayItemsSearch } from "../shoppingcart/ebay.functi
|
|
|
7
7
|
import { getFullPixelatedConfig } from '../config/config';
|
|
8
8
|
import { CacheManager } from '../general/cache-manager';
|
|
9
9
|
import { getDomain } from './utilities';
|
|
10
|
+
import { smartFetch } from './smartfetch';
|
|
10
11
|
/**
|
|
11
12
|
* Helper to construct an origin string from a Next-like headers() object or plain values.
|
|
12
13
|
* Accepts an object with `get(key)` method, or `undefined` and falls back to localhost origin.
|
|
@@ -166,10 +167,7 @@ export async function createImageURLsFromJSON(origin, jsonPath = 'public/site-im
|
|
|
166
167
|
urlPath = urlPath.slice('public/'.length);
|
|
167
168
|
if (!urlPath.startsWith('/'))
|
|
168
169
|
urlPath = `/${urlPath}`;
|
|
169
|
-
const
|
|
170
|
-
if (!resp.ok)
|
|
171
|
-
return sitemap;
|
|
172
|
-
const json = await resp.json();
|
|
170
|
+
const json = await smartFetch(`${origin}${urlPath}`);
|
|
173
171
|
let imgs = [];
|
|
174
172
|
if (Array.isArray(json)) {
|
|
175
173
|
imgs = json;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* smartFetch - Intelligent fetch wrapper with caching, retries, proxy, and timeout support
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Multiple response types (json, text, blob)
|
|
6
|
+
* - Automatic proxy fallback on CORS errors
|
|
7
|
+
* - Retry with exponential backoff
|
|
8
|
+
* - Request timeout handling
|
|
9
|
+
* - Dual caching: Next.js fetch cache + CacheManager
|
|
10
|
+
* - Enhanced error messages with domain info
|
|
11
|
+
* - Optional callbacks (onSuccess, onError, onComplete)
|
|
12
|
+
* - Debug logging
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Simple JSON fetch with caching
|
|
16
|
+
* const data = await smartFetch('https://api.example.com/user/123', {
|
|
17
|
+
* responseType: 'json',
|
|
18
|
+
* cache: cacheManager,
|
|
19
|
+
* cacheKey: 'user:123'
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // With proxy fallback on CORS
|
|
24
|
+
* const data = await smartFetch('https://api.external.com/data', {
|
|
25
|
+
* responseType: 'json',
|
|
26
|
+
* proxy: {
|
|
27
|
+
* url: 'https://proxy.pixelated.tech/',
|
|
28
|
+
* fallbackOnCors: true
|
|
29
|
+
* },
|
|
30
|
+
* retries: 2
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Server-side with Next.js caching
|
|
35
|
+
* const data = await smartFetch(url, {
|
|
36
|
+
* cacheStrategy: 'next',
|
|
37
|
+
* nextCache: { revalidate: 3600 } // 1 hour
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Extract domain from URL for enhanced error messages
|
|
42
|
+
*/
|
|
43
|
+
function getDomain(url) {
|
|
44
|
+
try {
|
|
45
|
+
return new URL(url).hostname;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return 'unknown';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if error is CORS-related
|
|
53
|
+
*/
|
|
54
|
+
function isCorsError(error) {
|
|
55
|
+
const message = error.message.toLowerCase();
|
|
56
|
+
return (message.includes('cors') ||
|
|
57
|
+
message.includes('cross-origin') ||
|
|
58
|
+
message.includes('network') ||
|
|
59
|
+
message.includes('failed to fetch'));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Intelligent fetch with caching, retries, proxy fallback, and timeout
|
|
63
|
+
*/
|
|
64
|
+
export async function smartFetch(url, options = {}) {
|
|
65
|
+
const { responseType = 'json', proxy, cacheStrategy = typeof window === 'undefined' ? 'next' : 'local', nextCache, cache, cacheKey, retries = 1, timeout = 10000, requestInit = {}, debug = false, onSuccess, onError, onComplete, } = options;
|
|
66
|
+
const domain = getDomain(url);
|
|
67
|
+
try {
|
|
68
|
+
// Step 1: Check CacheManager first (fastest, cross-request)
|
|
69
|
+
if ((cacheStrategy === 'local' || cacheStrategy === 'both') && cache && cacheKey) {
|
|
70
|
+
const cached = cache.get(cacheKey);
|
|
71
|
+
if (cached) {
|
|
72
|
+
if (debug)
|
|
73
|
+
console.log(`[smartFetch] ${domain}: Cache hit (${cacheKey})`);
|
|
74
|
+
onSuccess?.(cached);
|
|
75
|
+
onComplete?.();
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Determine fetch URL (direct or via proxy)
|
|
80
|
+
let fetchUrl = url;
|
|
81
|
+
let tryDirect = !proxy?.forceProxy;
|
|
82
|
+
if (proxy?.forceProxy) {
|
|
83
|
+
fetchUrl = proxy.url + encodeURIComponent(url);
|
|
84
|
+
tryDirect = false;
|
|
85
|
+
if (debug)
|
|
86
|
+
console.log(`[smartFetch] ${domain}: Using proxy (force)`);
|
|
87
|
+
}
|
|
88
|
+
// Step 3: Fetch with retry loop
|
|
89
|
+
let lastError;
|
|
90
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
91
|
+
try {
|
|
92
|
+
if (debug && attempt > 0) {
|
|
93
|
+
console.log(`[smartFetch] ${domain}: Retry attempt ${attempt + 1}/${retries + 1}`);
|
|
94
|
+
}
|
|
95
|
+
// Attempt direct fetch
|
|
96
|
+
if (tryDirect) {
|
|
97
|
+
try {
|
|
98
|
+
if (debug)
|
|
99
|
+
console.log(`[smartFetch] ${domain}: Fetching (direct)`);
|
|
100
|
+
// Set up timeout for this fetch attempt
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
...requestInit,
|
|
106
|
+
...(cacheStrategy === 'next' || cacheStrategy === 'both'
|
|
107
|
+
? { next: nextCache }
|
|
108
|
+
: {}),
|
|
109
|
+
});
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
113
|
+
}
|
|
114
|
+
// If responseType is 'ok' or 'status', return the raw Response object
|
|
115
|
+
if (responseType === 'ok' || responseType === 'status') {
|
|
116
|
+
if (debug)
|
|
117
|
+
console.log(`[smartFetch] ${domain}: Success (returning Response object)`);
|
|
118
|
+
onSuccess?.(response);
|
|
119
|
+
onComplete?.();
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
const data = await response[responseType]();
|
|
123
|
+
// Cache in CacheManager
|
|
124
|
+
if ((cacheStrategy === 'local' || cacheStrategy === 'both') && cache && cacheKey) {
|
|
125
|
+
cache.set(cacheKey, data);
|
|
126
|
+
}
|
|
127
|
+
if (debug)
|
|
128
|
+
console.log(`[smartFetch] ${domain}: Success`);
|
|
129
|
+
onSuccess?.(data);
|
|
130
|
+
onComplete?.();
|
|
131
|
+
return data;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// On CORS error, try proxy if available
|
|
135
|
+
if (proxy?.fallbackOnCors && isCorsError(error)) {
|
|
136
|
+
if (debug)
|
|
137
|
+
console.log(`[smartFetch] ${domain}: CORS error, falling back to proxy`);
|
|
138
|
+
tryDirect = false;
|
|
139
|
+
fetchUrl = proxy.url + encodeURIComponent(url);
|
|
140
|
+
// Fall through to proxy attempt below
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Attempt via proxy (if set or after direct CORS failure)
|
|
148
|
+
if (!tryDirect) {
|
|
149
|
+
if (debug)
|
|
150
|
+
console.log(`[smartFetch] ${domain}: Fetching (proxy)`);
|
|
151
|
+
// Set up timeout for this fetch attempt
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
154
|
+
const response = await fetch(fetchUrl, {
|
|
155
|
+
signal: controller.signal,
|
|
156
|
+
...requestInit,
|
|
157
|
+
...(cacheStrategy === 'next' || cacheStrategy === 'both'
|
|
158
|
+
? { next: nextCache }
|
|
159
|
+
: {}),
|
|
160
|
+
});
|
|
161
|
+
clearTimeout(timeoutId);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
// If responseType is 'ok' or 'status', return the raw Response object
|
|
166
|
+
if (responseType === 'ok' || responseType === 'status') {
|
|
167
|
+
if (debug)
|
|
168
|
+
console.log(`[smartFetch] ${domain}: Success (via proxy, returning Response object)`);
|
|
169
|
+
onSuccess?.(response);
|
|
170
|
+
onComplete?.();
|
|
171
|
+
return response;
|
|
172
|
+
}
|
|
173
|
+
const data = await response[responseType]();
|
|
174
|
+
// Cache in CacheManager
|
|
175
|
+
if ((cacheStrategy === 'local' || cacheStrategy === 'both') && cache && cacheKey) {
|
|
176
|
+
cache.set(cacheKey, data);
|
|
177
|
+
}
|
|
178
|
+
if (debug)
|
|
179
|
+
console.log(`[smartFetch] ${domain}: Success (via proxy)`);
|
|
180
|
+
onSuccess?.(data);
|
|
181
|
+
onComplete?.();
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
lastError = error;
|
|
187
|
+
// If we have retries left, wait before retrying
|
|
188
|
+
if (attempt < retries) {
|
|
189
|
+
const delay = Math.pow(2, attempt) * 100; // Exponential backoff: 100ms, 200ms, 400ms...
|
|
190
|
+
if (debug)
|
|
191
|
+
console.log(`[smartFetch] ${domain}: Waiting ${delay}ms before retry`);
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// If we got here, all attempts failed
|
|
197
|
+
const errorMessage = `[smartFetch] ${domain}: ${lastError?.message || 'Unknown error'}`;
|
|
198
|
+
const error = new Error(errorMessage);
|
|
199
|
+
if (debug)
|
|
200
|
+
console.error(errorMessage);
|
|
201
|
+
onError?.(error);
|
|
202
|
+
onComplete?.();
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const err = error;
|
|
207
|
+
onError?.(err);
|
|
208
|
+
onComplete?.();
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -83,7 +83,7 @@ function Tile(props) {
|
|
|
83
83
|
const config = usePixelatedConfig();
|
|
84
84
|
const imgClick = props.imgClick;
|
|
85
85
|
const captionText = (props.bodyText && props.bodyText.length > 0) ? props.bodyText : (props.imageAlt ?? "");
|
|
86
|
-
const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
|
|
86
|
+
const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, aboveFold: (props.index === 0) ? true : undefined, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
|
|
87
87
|
const rootClass = `tile${(props.variant) ? ' ' + props.variant : ''}`;
|
|
88
88
|
return (_jsx("div", { className: rootClass, id: 'tile-' + props.index, suppressHydrationWarning: true, children: props.link ?
|
|
89
89
|
_jsx("a", { href: props.link, className: "tile-link", children: tileBody })
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildUrl - Unified URL builder supporting multiple patterns from the codebase
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Simple query parameters (Google Places, Instagram style)
|
|
6
|
+
* - Path segments + query params (Contentful style)
|
|
7
|
+
* - Proxy wrapping with encoding
|
|
8
|
+
* - Proper URL encoding of parameter values
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Simple query params
|
|
12
|
+
* buildUrl({
|
|
13
|
+
* baseUrl: 'https://api.example.com/search',
|
|
14
|
+
* params: { q: 'test', limit: 10 }
|
|
15
|
+
* });
|
|
16
|
+
* // → 'https://api.example.com/search?q=test&limit=10'
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // With path segments (Contentful style)
|
|
20
|
+
* buildUrl({
|
|
21
|
+
* baseUrl: 'https://api.contentful.com',
|
|
22
|
+
* pathSegments: ['spaces', 'abc123', 'environments', 'master', 'entries'],
|
|
23
|
+
* params: { access_token: 'xxx' }
|
|
24
|
+
* });
|
|
25
|
+
* // → 'https://api.contentful.com/spaces/abc123/environments/master/entries?access_token=xxx'
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // With proxy wrapping (Flickr style)
|
|
29
|
+
* buildUrl({
|
|
30
|
+
* baseUrl: 'https://www.flickr.com/services/rest',
|
|
31
|
+
* params: { method: 'flickr.photos.search', api_key: 'xxx' },
|
|
32
|
+
* proxyUrl: 'https://proxy.pixelated.tech/'
|
|
33
|
+
* });
|
|
34
|
+
* // → 'https://proxy.pixelated.tech/https%3A%2F%2Fwww.flickr.com%2Fservices%2Frest%3Fmethod%3Dflickr.photos.search%26api_key%3Dxxx'
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Build a URL with optional path segments and query parameters
|
|
38
|
+
*/
|
|
39
|
+
export function buildUrl(options) {
|
|
40
|
+
const { baseUrl, pathSegments, params, proxyUrl } = options;
|
|
41
|
+
// Step 1: Start with base URL
|
|
42
|
+
let url = baseUrl;
|
|
43
|
+
// Step 2: Append path segments (no encoding needed - they're IDs/identifiers)
|
|
44
|
+
if (pathSegments && pathSegments.length > 0) {
|
|
45
|
+
const segmentPath = pathSegments
|
|
46
|
+
.map(segment => String(segment).replace(/^\/+|\/+$/g, '')) // Remove leading/trailing slashes
|
|
47
|
+
.filter(Boolean) // Remove empty strings
|
|
48
|
+
.join('/');
|
|
49
|
+
// Ensure single slash between base and path
|
|
50
|
+
if (!url.endsWith('/')) {
|
|
51
|
+
url += '/';
|
|
52
|
+
}
|
|
53
|
+
url += segmentPath;
|
|
54
|
+
}
|
|
55
|
+
// Step 3: Append query parameters (values ARE encoded)
|
|
56
|
+
if (params && Object.keys(params).length > 0) {
|
|
57
|
+
const searchParams = new URLSearchParams();
|
|
58
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
59
|
+
// Skip null/undefined values
|
|
60
|
+
if (value !== null && value !== undefined) {
|
|
61
|
+
searchParams.append(key, String(value));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const queryString = searchParams.toString();
|
|
65
|
+
if (queryString) {
|
|
66
|
+
url += (url.includes('?') ? '&' : '?') + queryString;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Step 4: Wrap with proxy if provided
|
|
70
|
+
if (proxyUrl) {
|
|
71
|
+
return proxyUrl + encodeURIComponent(url);
|
|
72
|
+
}
|
|
73
|
+
return url;
|
|
74
|
+
}
|