@pixelated-tech/components 3.2.4 → 3.2.5
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 +5 -0
- package/dist/components/cms/wordpress.components.js +14 -9
- package/dist/components/seo/schema-blogposting.js +53 -0
- package/dist/components/seo/schema-localbusiness.js +27 -0
- package/dist/components/seo/schema-recipe.js +5 -0
- package/dist/components/seo/schema-services.js +24 -0
- package/dist/components/seo/schema-website.js +13 -0
- package/dist/components/structured/recipe.css +1 -1
- package/dist/components/structured/recipe.js +58 -8
- package/dist/data/recipes.json +3157 -1821
- package/dist/index.js +5 -0
- package/dist/types/components/cms/wordpress.components.d.ts +1 -0
- package/dist/types/components/cms/wordpress.components.d.ts.map +1 -1
- package/dist/types/components/seo/schema-blogposting.d.ts +31 -0
- package/dist/types/components/seo/schema-blogposting.d.ts.map +1 -0
- package/dist/types/components/seo/schema-localbusiness.d.ts +25 -0
- package/dist/types/components/seo/schema-localbusiness.d.ts.map +1 -0
- package/dist/types/components/seo/schema-recipe.d.ts +34 -0
- package/dist/types/components/seo/schema-recipe.d.ts.map +1 -0
- package/dist/types/components/seo/schema-services.d.ts +25 -0
- package/dist/types/components/seo/schema-services.d.ts.map +1 -0
- package/dist/types/components/seo/schema-website.d.ts +21 -0
- package/dist/types/components/seo/schema-website.d.ts.map +1 -0
- package/dist/types/components/structured/recipe.d.ts +34 -13
- package/dist/types/components/structured/recipe.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/tests/schema-blogposting.test.d.ts +2 -0
- package/dist/types/tests/schema-blogposting.test.d.ts.map +1 -0
- package/dist/types/tests/schema-localbusiness.test.d.ts +2 -0
- package/dist/types/tests/schema-localbusiness.test.d.ts.map +1 -0
- package/dist/types/tests/schema-recipe.test.d.ts +2 -0
- package/dist/types/tests/schema-recipe.test.d.ts.map +1 -0
- package/dist/types/tests/schema-services.test.d.ts +2 -0
- package/dist/types/tests/schema-services.test.d.ts.map +1 -0
- package/dist/types/tests/schema-website.test.d.ts +2 -0
- package/dist/types/tests/schema-website.test.d.ts.map +1 -0
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -96,6 +96,11 @@ Components to help build websites quicker:
|
|
|
96
96
|
1. Form Components and Form Builder
|
|
97
97
|
1. Google Analytics, Map, and Search Integration
|
|
98
98
|
1. Gravatar Card Integration
|
|
99
|
+
1. Local Business JSON-LD Schema for SEO
|
|
100
|
+
1. Website JSON-LD Schema for SEO
|
|
101
|
+
1. Services JSON-LD Schema for SEO
|
|
102
|
+
1. Recipe JSON-LD Schema for SEO
|
|
103
|
+
1. BlogPosting JSON-LD Schema for SEO
|
|
99
104
|
1. Page and Page Section Header Components
|
|
100
105
|
1. Hubspot Calendar and Form Integration
|
|
101
106
|
1. Instagram Image Fetch Integration
|
|
@@ -7,17 +7,22 @@ import { getWordPressItems } from './wordpress.functions';
|
|
|
7
7
|
import { Loading, ToggleLoading } from '../general/loading';
|
|
8
8
|
import "./wordpress.css";
|
|
9
9
|
// https://microformats.org/wiki/h-entry
|
|
10
|
-
function decodeString(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
temp = null;
|
|
15
|
-
return str;
|
|
10
|
+
function decodeString(str) {
|
|
11
|
+
const textarea = { value: '' };
|
|
12
|
+
textarea.innerHTML = str;
|
|
13
|
+
return textarea.value || str;
|
|
16
14
|
}
|
|
17
15
|
export function BlogPostList(props) {
|
|
18
|
-
const { site, count } = props;
|
|
19
|
-
const [posts, setPosts] = useState([]);
|
|
16
|
+
const { site, count, posts: cachedPosts } = props;
|
|
17
|
+
const [posts, setPosts] = useState(cachedPosts ?? []);
|
|
20
18
|
useEffect(() => {
|
|
19
|
+
// If posts are provided, use them directly without fetching
|
|
20
|
+
if (cachedPosts && cachedPosts.length > 0) {
|
|
21
|
+
const sorted = cachedPosts.sort((a, b) => ((a.date ?? '') < (b.date ?? '')) ? 1 : -1);
|
|
22
|
+
setPosts(sorted);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Otherwise, fetch from WordPress
|
|
21
26
|
ToggleLoading({ show: true });
|
|
22
27
|
(async () => {
|
|
23
28
|
const data = (await getWordPressItems({ site, count })) ?? [];
|
|
@@ -25,7 +30,7 @@ export function BlogPostList(props) {
|
|
|
25
30
|
setPosts(sorted);
|
|
26
31
|
ToggleLoading({ show: false });
|
|
27
32
|
})();
|
|
28
|
-
}, [site, count]);
|
|
33
|
+
}, [site, count, cachedPosts]);
|
|
29
34
|
return (_jsxs(_Fragment, { children: [_jsx(Loading, {}), posts.map((post) => (_jsx(PageGridItem, { children: _jsx(BlogPostSummary, { ID: post.ID, title: post.title, date: post.date, excerpt: post.excerpt, URL: post.URL, categories: post.categories, featured_image: post.featured_image }) }, post.ID)))] }));
|
|
30
35
|
}
|
|
31
36
|
export function BlogPostSummary(props) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Converts WordPress REST API blog post to schema.org BlogPosting format
|
|
4
|
+
* @param post WordPress blog post
|
|
5
|
+
* @param includeFullContent Whether to include articleBody (true) or just description (false)
|
|
6
|
+
*/
|
|
7
|
+
export function mapWordPressToBlogPosting(post, includeFullContent = false) {
|
|
8
|
+
const decodeString = (s) => {
|
|
9
|
+
if (typeof document === 'undefined')
|
|
10
|
+
return s;
|
|
11
|
+
const temp = document.createElement('p');
|
|
12
|
+
temp.innerHTML = s;
|
|
13
|
+
return temp.textContent || temp.innerText || s;
|
|
14
|
+
};
|
|
15
|
+
const cleanContent = (content) => {
|
|
16
|
+
if (!content)
|
|
17
|
+
return '';
|
|
18
|
+
return decodeString(content).replace(/\[…\]/g, '').trim();
|
|
19
|
+
};
|
|
20
|
+
const description = cleanContent(post.excerpt);
|
|
21
|
+
const articleBody = includeFullContent ? cleanContent(post.content || '') : undefined;
|
|
22
|
+
const schema = {
|
|
23
|
+
'@context': 'https://schema.org',
|
|
24
|
+
'@type': 'BlogPosting',
|
|
25
|
+
headline: decodeString(post.title),
|
|
26
|
+
description: description || decodeString(post.title),
|
|
27
|
+
datePublished: post.date,
|
|
28
|
+
image: post.featured_image || post.post_thumbnail?.URL,
|
|
29
|
+
articleSection: Array.isArray(post.categories) && post.categories.length > 0
|
|
30
|
+
? post.categories[0]
|
|
31
|
+
: 'Blog',
|
|
32
|
+
keywords: Array.isArray(post.categories) ? post.categories : [],
|
|
33
|
+
};
|
|
34
|
+
if (articleBody) {
|
|
35
|
+
schema.articleBody = articleBody;
|
|
36
|
+
}
|
|
37
|
+
if (post.modified) {
|
|
38
|
+
schema.dateModified = post.modified;
|
|
39
|
+
}
|
|
40
|
+
if (post.author) {
|
|
41
|
+
schema.author = {
|
|
42
|
+
'@type': 'Person',
|
|
43
|
+
name: post.author,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return schema;
|
|
47
|
+
}
|
|
48
|
+
export function SchemaBlogPosting({ post }) {
|
|
49
|
+
return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: {
|
|
50
|
+
__html: JSON.stringify(post),
|
|
51
|
+
} }));
|
|
52
|
+
}
|
|
53
|
+
export default SchemaBlogPosting;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function LocalBusinessSchema({ name, streetAddress, addressLocality, addressRegion, postalCode, addressCountry = 'United States', telephone, url, logo, image, openingHours, description, email, priceRange, sameAs }) {
|
|
3
|
+
const schemaData = {
|
|
4
|
+
'@context': 'https://schema.org',
|
|
5
|
+
'@type': 'LocalBusiness',
|
|
6
|
+
name,
|
|
7
|
+
address: {
|
|
8
|
+
'@type': 'PostalAddress',
|
|
9
|
+
streetAddress,
|
|
10
|
+
addressLocality,
|
|
11
|
+
addressRegion,
|
|
12
|
+
postalCode,
|
|
13
|
+
addressCountry
|
|
14
|
+
},
|
|
15
|
+
telephone,
|
|
16
|
+
url,
|
|
17
|
+
...(logo && { logo }),
|
|
18
|
+
...(image && { image }),
|
|
19
|
+
...(openingHours && { openingHours }),
|
|
20
|
+
...(description && { description }),
|
|
21
|
+
...(email && { email }),
|
|
22
|
+
...(priceRange && { priceRange }),
|
|
23
|
+
...(sameAs && sameAs.length > 0 && { sameAs })
|
|
24
|
+
};
|
|
25
|
+
return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schemaData) } }));
|
|
26
|
+
}
|
|
27
|
+
export default LocalBusinessSchema;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
export function ServicesSchema({ provider, services }) {
|
|
3
|
+
const serviceObjects = services.map(service => ({
|
|
4
|
+
'@type': 'Service',
|
|
5
|
+
name: service.name,
|
|
6
|
+
description: service.description,
|
|
7
|
+
...(service.url && { url: service.url }),
|
|
8
|
+
...(service.image && { image: service.image }),
|
|
9
|
+
...(service.areaServed && { areaServed: service.areaServed }),
|
|
10
|
+
provider: {
|
|
11
|
+
'@type': 'LocalBusiness',
|
|
12
|
+
name: provider.name,
|
|
13
|
+
url: provider.url,
|
|
14
|
+
...(provider.logo && { logo: provider.logo }),
|
|
15
|
+
...(provider.telephone && { telephone: provider.telephone }),
|
|
16
|
+
...(provider.email && { email: provider.email })
|
|
17
|
+
}
|
|
18
|
+
}));
|
|
19
|
+
return (_jsx(_Fragment, { children: serviceObjects.map((service, idx) => (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify({
|
|
20
|
+
'@context': 'https://schema.org',
|
|
21
|
+
...service
|
|
22
|
+
}) } }, idx))) }));
|
|
23
|
+
}
|
|
24
|
+
export default ServicesSchema;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function WebsiteSchema({ name, url, description, potentialAction }) {
|
|
3
|
+
const schemaData = {
|
|
4
|
+
'@context': 'https://schema.org',
|
|
5
|
+
'@type': 'WebSite',
|
|
6
|
+
name,
|
|
7
|
+
url,
|
|
8
|
+
...(description && { description }),
|
|
9
|
+
...(potentialAction && { potentialAction })
|
|
10
|
+
};
|
|
11
|
+
return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schemaData) } }));
|
|
12
|
+
}
|
|
13
|
+
export default WebsiteSchema;
|
|
@@ -4,6 +4,55 @@ import PropTypes from 'prop-types';
|
|
|
4
4
|
import { SmartImage } from '../cms/cloudinary.image';
|
|
5
5
|
import { usePixelatedConfig } from '../config/config.client';
|
|
6
6
|
import './recipe.css';
|
|
7
|
+
export function mapSchemaRecipeToDisplay(schemaRecipe) {
|
|
8
|
+
// Parse ISO 8601 duration and convert to readable format
|
|
9
|
+
function parseDuration(iso8601) {
|
|
10
|
+
if (!iso8601 || iso8601 === 'PT0M')
|
|
11
|
+
return '';
|
|
12
|
+
const regex = /PT(?:(\d+)H)?(?:(\d+)M)?/;
|
|
13
|
+
const match = iso8601.match(regex);
|
|
14
|
+
if (!match)
|
|
15
|
+
return iso8601;
|
|
16
|
+
const hours = match[1] ? parseInt(match[1]) : 0;
|
|
17
|
+
const minutes = match[2] ? parseInt(match[2]) : 0;
|
|
18
|
+
if (hours > 0 && minutes > 0) {
|
|
19
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minutes`;
|
|
20
|
+
}
|
|
21
|
+
else if (hours > 0) {
|
|
22
|
+
return `${hours} hour${hours > 1 ? 's' : ''}`;
|
|
23
|
+
}
|
|
24
|
+
else if (minutes > 0) {
|
|
25
|
+
return `${minutes} minutes`;
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
// Extract author name
|
|
30
|
+
const authorName = typeof schemaRecipe.author === 'string'
|
|
31
|
+
? schemaRecipe.author
|
|
32
|
+
: schemaRecipe.author?.name || '';
|
|
33
|
+
// Convert instructions from HowToStep format to plain text
|
|
34
|
+
const instructions = Array.isArray(schemaRecipe.recipeInstructions)
|
|
35
|
+
? schemaRecipe.recipeInstructions.map((instruction) => typeof instruction === 'string' ? instruction : instruction.text || '')
|
|
36
|
+
: [];
|
|
37
|
+
// Combine cook and prep times for display
|
|
38
|
+
let displayDuration = '';
|
|
39
|
+
if (schemaRecipe.totalTime) {
|
|
40
|
+
displayDuration = parseDuration(schemaRecipe.totalTime);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: schemaRecipe.name || '',
|
|
44
|
+
photo: schemaRecipe.image || '',
|
|
45
|
+
summary: schemaRecipe.description || '',
|
|
46
|
+
author: authorName,
|
|
47
|
+
published: schemaRecipe.datePublished || '',
|
|
48
|
+
duration: displayDuration,
|
|
49
|
+
yield: schemaRecipe.recipeYield || '',
|
|
50
|
+
ingredients: schemaRecipe.recipeIngredient || [],
|
|
51
|
+
instructions,
|
|
52
|
+
category: schemaRecipe.recipeCategory ? [schemaRecipe.recipeCategory] : [],
|
|
53
|
+
license: schemaRecipe.license || ''
|
|
54
|
+
};
|
|
55
|
+
}
|
|
7
56
|
/* ========== RECIPE BOOK ========== */
|
|
8
57
|
RecipeBook.propTypes = {
|
|
9
58
|
recipeData: PropTypes.shape({
|
|
@@ -25,9 +74,10 @@ export function RecipeBook(props) {
|
|
|
25
74
|
myElems[category] = [];
|
|
26
75
|
for (const recipeKey in recipeBookItems) {
|
|
27
76
|
const recipe = recipeBookItems[recipeKey];
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
77
|
+
const outputRecipe = mapSchemaRecipeToDisplay(recipe);
|
|
78
|
+
const recipeCat = outputRecipe.category;
|
|
79
|
+
if (recipeCat.includes(category)) {
|
|
80
|
+
myElems[category].push(outputRecipe);
|
|
31
81
|
}
|
|
32
82
|
}
|
|
33
83
|
}
|
|
@@ -41,7 +91,7 @@ export function RecipeBook(props) {
|
|
|
41
91
|
myElems.push(_jsx(RecipeCategory, { id: cID, className: 'h-recipe-category', category: category, showOnly: showOnlyCat }, cID));
|
|
42
92
|
for (const recipeKey in recipeElems[category]) {
|
|
43
93
|
const recipe = recipeElems[category][recipeKey];
|
|
44
|
-
const cats = recipe.
|
|
94
|
+
const cats = recipe.category;
|
|
45
95
|
const rID = cID + '-r' + (parseInt(recipeKey, 10) + 1);
|
|
46
96
|
if (cats.includes(category)) {
|
|
47
97
|
myElems.push(_jsx(RecipeBookItem, { id: rID, recipeData: recipe, showOnly: showOnlyRecipe }, rID));
|
|
@@ -92,8 +142,7 @@ RecipeBookItem.propTypes = {
|
|
|
92
142
|
};
|
|
93
143
|
export function RecipeBookItem(props) {
|
|
94
144
|
const config = usePixelatedConfig();
|
|
95
|
-
const
|
|
96
|
-
const recipe = recipeData.properties;
|
|
145
|
+
const recipe = props.recipeData;
|
|
97
146
|
const ingredients = recipe.ingredients.map((ingredient, iKey) => _jsx("li", { className: "p-ingredient", children: ingredient }, iKey));
|
|
98
147
|
const instructions = recipe.instructions.map((instruction, iKey) => _jsx("li", { className: "p-instruction", children: instruction }, iKey));
|
|
99
148
|
/* ? <img className='u-photo' src={recipe.photo} title={recipe.name} alt={recipe.name} /> */
|
|
@@ -124,9 +173,10 @@ export function RecipePickList(props) {
|
|
|
124
173
|
const recipeDataItems = recipeData.items;
|
|
125
174
|
for (const recipeKey in recipeDataItems) {
|
|
126
175
|
const recipe = recipeDataItems[recipeKey];
|
|
127
|
-
const
|
|
176
|
+
const outputRecipe = mapSchemaRecipeToDisplay(recipe);
|
|
177
|
+
const cats = outputRecipe.category;
|
|
128
178
|
if (cats.includes(category)) {
|
|
129
|
-
myOpts.push(_jsx("option", { value: cID + '-r' + rID, children:
|
|
179
|
+
myOpts.push(_jsx("option", { value: cID + '-r' + rID, children: outputRecipe.name }, cID + '-r' + rID));
|
|
130
180
|
rID += 1;
|
|
131
181
|
}
|
|
132
182
|
}
|