@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.
Files changed (37) hide show
  1. package/README.md +5 -0
  2. package/dist/components/cms/wordpress.components.js +14 -9
  3. package/dist/components/seo/schema-blogposting.js +53 -0
  4. package/dist/components/seo/schema-localbusiness.js +27 -0
  5. package/dist/components/seo/schema-recipe.js +5 -0
  6. package/dist/components/seo/schema-services.js +24 -0
  7. package/dist/components/seo/schema-website.js +13 -0
  8. package/dist/components/structured/recipe.css +1 -1
  9. package/dist/components/structured/recipe.js +58 -8
  10. package/dist/data/recipes.json +3157 -1821
  11. package/dist/index.js +5 -0
  12. package/dist/types/components/cms/wordpress.components.d.ts +1 -0
  13. package/dist/types/components/cms/wordpress.components.d.ts.map +1 -1
  14. package/dist/types/components/seo/schema-blogposting.d.ts +31 -0
  15. package/dist/types/components/seo/schema-blogposting.d.ts.map +1 -0
  16. package/dist/types/components/seo/schema-localbusiness.d.ts +25 -0
  17. package/dist/types/components/seo/schema-localbusiness.d.ts.map +1 -0
  18. package/dist/types/components/seo/schema-recipe.d.ts +34 -0
  19. package/dist/types/components/seo/schema-recipe.d.ts.map +1 -0
  20. package/dist/types/components/seo/schema-services.d.ts +25 -0
  21. package/dist/types/components/seo/schema-services.d.ts.map +1 -0
  22. package/dist/types/components/seo/schema-website.d.ts +21 -0
  23. package/dist/types/components/seo/schema-website.d.ts.map +1 -0
  24. package/dist/types/components/structured/recipe.d.ts +34 -13
  25. package/dist/types/components/structured/recipe.d.ts.map +1 -1
  26. package/dist/types/index.d.ts +5 -0
  27. package/dist/types/tests/schema-blogposting.test.d.ts +2 -0
  28. package/dist/types/tests/schema-blogposting.test.d.ts.map +1 -0
  29. package/dist/types/tests/schema-localbusiness.test.d.ts +2 -0
  30. package/dist/types/tests/schema-localbusiness.test.d.ts.map +1 -0
  31. package/dist/types/tests/schema-recipe.test.d.ts +2 -0
  32. package/dist/types/tests/schema-recipe.test.d.ts.map +1 -0
  33. package/dist/types/tests/schema-services.test.d.ts +2 -0
  34. package/dist/types/tests/schema-services.test.d.ts.map +1 -0
  35. package/dist/types/tests/schema-website.test.d.ts +2 -0
  36. package/dist/types/tests/schema-website.test.d.ts.map +1 -0
  37. 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(s) {
11
- let temp = document.createElement('p');
12
- temp.innerHTML = s;
13
- const str = temp.textContent || temp.innerText;
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,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function RecipeSchema({ recipe }) {
3
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(recipe) } }));
4
+ }
5
+ export default RecipeSchema;
@@ -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;
@@ -27,7 +27,7 @@
27
27
 
28
28
  /* https://microformats.org/wiki/h-recipe */
29
29
 
30
- .h-recipe { font-size: 12px; }
30
+ .h-recipe { --do-nothing: true; }
31
31
  .h-recipe p { margin: 0px !important; }
32
32
  .h-recipe.p-name {
33
33
  background-color: #F9F9F9;
@@ -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 cats = recipe.properties.category;
29
- if (cats.includes(category)) {
30
- myElems[category].push(recipe);
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.properties.category;
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 recipeData = props.recipeData;
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 cats = recipe.properties.category;
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: recipe.properties.name }, cID + '-r' + rID));
179
+ myOpts.push(_jsx("option", { value: cID + '-r' + rID, children: outputRecipe.name }, cID + '-r' + rID));
130
180
  rID += 1;
131
181
  }
132
182
  }