@lebtiga/sonic-agent 1.0.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/LICENSE.txt +223 -0
- package/README.md +61 -0
- package/bin/sonic.js +304 -0
- package/lib/index.js +20 -0
- package/lib/installer.js +156 -0
- package/lib/license.js +48 -0
- package/package.json +46 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/README.md +100 -0
- package/plugin/agents/sonic.md +80 -0
- package/plugin/commands/sonic-build.md +145 -0
- package/plugin/commands/sonic-help.md +71 -0
- package/plugin/skills/accessibility-qa/SKILL.md +160 -0
- package/plugin/skills/accessibility-qa/templates/accessibility-qa-report-template.md +123 -0
- package/plugin/skills/accessibility-qa/templates/wcag-compliance-statement.md +70 -0
- package/plugin/skills/aka-wireframe-wp/SKILL.md +149 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/README.md +190 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/footer.php +49 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/functions.php +395 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/header.php +58 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/index.php +39 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-answer.php +62 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-authority-hub.php +122 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-knowledge.php +58 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/style.css +633 -0
- package/plugin/skills/aka-wireframe-wp/references/content-generator.md +371 -0
- package/plugin/skills/aka-wireframe-wp/references/internal-linker.md +430 -0
- package/plugin/skills/aka-wireframe-wp/references/orchestrator.md +269 -0
- package/plugin/skills/aka-wireframe-wp/references/prompts-library.md +880 -0
- package/plugin/skills/aka-wireframe-wp/references/seo-optimizer.md +433 -0
- package/plugin/skills/aka-wireframe-wp/references/strategy-planner.md +317 -0
- package/plugin/skills/aka-wireframe-wp/references/wordpress-deployer.md +545 -0
- package/plugin/skills/authority-site-builder/SKILL.md +138 -0
- package/plugin/skills/brand-philosophy/SKILL.md +77 -0
- package/plugin/skills/freepik-spaces/SKILL.md +122 -0
- package/plugin/skills/freepik-spaces/docs/automation-guide.md +233 -0
- package/plugin/skills/freepik-spaces/docs/research-notes.md +264 -0
- package/plugin/skills/freepik-spaces/plans/naseberry-demo-plan.md +320 -0
- package/plugin/skills/freepik-spaces/templates/naseberry-demo.json +302 -0
- package/plugin/skills/freepik-spaces/templates/saas-demo.json +212 -0
- package/plugin/skills/frontend-design/LICENSE.txt +177 -0
- package/plugin/skills/frontend-design/SKILL.md +77 -0
- package/plugin/skills/programmatic-seo/SKILL.md +236 -0
- package/plugin/skills/programmatic-seo/references/playbooks.md +293 -0
- package/plugin/skills/seo-qa/SKILL.md +132 -0
- package/plugin/skills/seo-qa/templates/schema-localbusiness.json +49 -0
- package/plugin/skills/seo-qa/templates/schema-service.json +36 -0
- package/plugin/skills/seo-qa/templates/seo-qa-report-template.md +90 -0
- package/plugin/skills/visual-identity/SKILL.md +109 -0
- package/plugin/skills/visual-identity/templates/style-guide-template.md +108 -0
- package/plugin/skills/website-image-gen/SKILL.md +82 -0
- package/plugin/skills/website-image-gen/templates/blog-featured.md +56 -0
- package/plugin/skills/website-image-gen/templates/hero-service-photo.md +56 -0
- package/plugin/skills/wordpress-pro/SKILL.md +105 -0
- package/plugin/skills/wordpress-pro/references/gutenberg-blocks.md +870 -0
- package/plugin/skills/wordpress-pro/references/hooks-filters.md +845 -0
- package/plugin/skills/wordpress-pro/references/performance-security.md +1012 -0
- package/plugin/skills/wordpress-pro/references/plugin-architecture.md +1041 -0
- package/plugin/skills/wordpress-pro/references/theme-development.md +858 -0
- package/plugin/sops/SOP-Sonic 777/authority-site-sop.html +1100 -0
- package/plugin/sops/SOP-WORDPRESS-330-PAGE-SITES.md +926 -0
- package/scripts/postinstall.js +109 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
# Gutenberg Blocks
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Block Development Overview
|
|
6
|
+
|
|
7
|
+
WordPress 6.4+ uses the Block Editor (Gutenberg) as the primary editing experience. Blocks are the fundamental building units.
|
|
8
|
+
|
|
9
|
+
### Block Types
|
|
10
|
+
|
|
11
|
+
| Type | Description | Use Case |
|
|
12
|
+
|------|-------------|----------|
|
|
13
|
+
| Static | Fixed HTML output | Simple content, images |
|
|
14
|
+
| Dynamic | Server-rendered | Posts list, dynamic data |
|
|
15
|
+
| Interactive | Client-side JS | Accordions, tabs, carousels |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Project Setup
|
|
20
|
+
|
|
21
|
+
### Using @wordpress/create-block
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Create a new block plugin
|
|
25
|
+
npx @wordpress/create-block my-block --namespace my-plugin
|
|
26
|
+
|
|
27
|
+
# Create with specific template
|
|
28
|
+
npx @wordpress/create-block my-block --template @wordpress/create-block-interactive-template
|
|
29
|
+
|
|
30
|
+
# Create dynamic block
|
|
31
|
+
npx @wordpress/create-block my-block --variant dynamic
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Generated Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
my-block/
|
|
38
|
+
├── my-block.php # Plugin file
|
|
39
|
+
├── package.json # NPM dependencies
|
|
40
|
+
├── src/
|
|
41
|
+
│ ├── block.json # Block metadata
|
|
42
|
+
│ ├── edit.js # Editor component
|
|
43
|
+
│ ├── save.js # Frontend save
|
|
44
|
+
│ ├── index.js # Block registration
|
|
45
|
+
│ ├── editor.scss # Editor styles
|
|
46
|
+
│ └── style.scss # Frontend styles
|
|
47
|
+
├── build/ # Compiled assets
|
|
48
|
+
└── readme.txt
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### package.json Scripts
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "my-block",
|
|
56
|
+
"version": "1.0.0",
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "wp-scripts build",
|
|
59
|
+
"start": "wp-scripts start",
|
|
60
|
+
"format": "wp-scripts format",
|
|
61
|
+
"lint:js": "wp-scripts lint-js",
|
|
62
|
+
"lint:css": "wp-scripts lint-style",
|
|
63
|
+
"packages-update": "wp-scripts packages-update"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@wordpress/scripts": "^27.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Block Registration
|
|
74
|
+
|
|
75
|
+
### block.json (WordPress 6.4+)
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"$schema": "https://schemas.wp.org/trunk/block.json",
|
|
80
|
+
"apiVersion": 3,
|
|
81
|
+
"name": "my-plugin/my-block",
|
|
82
|
+
"version": "1.0.0",
|
|
83
|
+
"title": "My Block",
|
|
84
|
+
"category": "widgets",
|
|
85
|
+
"icon": "smiley",
|
|
86
|
+
"description": "A custom block for displaying content.",
|
|
87
|
+
"keywords": ["custom", "content", "block"],
|
|
88
|
+
"supports": {
|
|
89
|
+
"html": false,
|
|
90
|
+
"align": ["wide", "full"],
|
|
91
|
+
"anchor": true,
|
|
92
|
+
"color": {
|
|
93
|
+
"background": true,
|
|
94
|
+
"text": true,
|
|
95
|
+
"link": true,
|
|
96
|
+
"gradients": true
|
|
97
|
+
},
|
|
98
|
+
"spacing": {
|
|
99
|
+
"margin": true,
|
|
100
|
+
"padding": true,
|
|
101
|
+
"blockGap": true
|
|
102
|
+
},
|
|
103
|
+
"typography": {
|
|
104
|
+
"fontSize": true,
|
|
105
|
+
"lineHeight": true
|
|
106
|
+
},
|
|
107
|
+
"__experimentalBorder": {
|
|
108
|
+
"color": true,
|
|
109
|
+
"radius": true,
|
|
110
|
+
"style": true,
|
|
111
|
+
"width": true
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"attributes": {
|
|
115
|
+
"content": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"source": "html",
|
|
118
|
+
"selector": "p"
|
|
119
|
+
},
|
|
120
|
+
"alignment": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"default": "left"
|
|
123
|
+
},
|
|
124
|
+
"showBorder": {
|
|
125
|
+
"type": "boolean",
|
|
126
|
+
"default": false
|
|
127
|
+
},
|
|
128
|
+
"items": {
|
|
129
|
+
"type": "array",
|
|
130
|
+
"default": []
|
|
131
|
+
},
|
|
132
|
+
"selectedPostId": {
|
|
133
|
+
"type": "number"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"example": {
|
|
137
|
+
"attributes": {
|
|
138
|
+
"content": "Example content for the block preview."
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"textdomain": "my-plugin",
|
|
142
|
+
"editorScript": "file:./index.js",
|
|
143
|
+
"editorStyle": "file:./index.css",
|
|
144
|
+
"style": "file:./style-index.css",
|
|
145
|
+
"viewScript": "file:./view.js",
|
|
146
|
+
"render": "file:./render.php"
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### PHP Registration
|
|
151
|
+
|
|
152
|
+
```php
|
|
153
|
+
<?php
|
|
154
|
+
declare(strict_types=1);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register all blocks
|
|
158
|
+
*/
|
|
159
|
+
function my_plugin_register_blocks(): void {
|
|
160
|
+
// Auto-register from block.json
|
|
161
|
+
register_block_type(__DIR__ . '/build/my-block');
|
|
162
|
+
|
|
163
|
+
// Or with additional arguments
|
|
164
|
+
register_block_type(__DIR__ . '/build/another-block', [
|
|
165
|
+
'render_callback' => 'my_plugin_render_another_block',
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
add_action('init', 'my_plugin_register_blocks');
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Register block category
|
|
172
|
+
*/
|
|
173
|
+
function my_plugin_block_categories(array $categories): array {
|
|
174
|
+
return array_merge(
|
|
175
|
+
[
|
|
176
|
+
[
|
|
177
|
+
'slug' => 'my-plugin-blocks',
|
|
178
|
+
'title' => __('My Plugin Blocks', 'my-plugin'),
|
|
179
|
+
'icon' => 'wordpress',
|
|
180
|
+
],
|
|
181
|
+
],
|
|
182
|
+
$categories
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
add_filter('block_categories_all', 'my_plugin_block_categories');
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Static Block Development
|
|
191
|
+
|
|
192
|
+
### index.js (Entry Point)
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
/**
|
|
196
|
+
* WordPress dependencies
|
|
197
|
+
*/
|
|
198
|
+
import { registerBlockType } from '@wordpress/blocks';
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Internal dependencies
|
|
202
|
+
*/
|
|
203
|
+
import Edit from './edit';
|
|
204
|
+
import save from './save';
|
|
205
|
+
import metadata from './block.json';
|
|
206
|
+
import './style.scss';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Register block
|
|
210
|
+
*/
|
|
211
|
+
registerBlockType(metadata.name, {
|
|
212
|
+
edit: Edit,
|
|
213
|
+
save,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### edit.js (Editor Component)
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
/**
|
|
221
|
+
* WordPress dependencies
|
|
222
|
+
*/
|
|
223
|
+
import { __ } from '@wordpress/i18n';
|
|
224
|
+
import {
|
|
225
|
+
useBlockProps,
|
|
226
|
+
RichText,
|
|
227
|
+
InspectorControls,
|
|
228
|
+
BlockControls,
|
|
229
|
+
AlignmentToolbar,
|
|
230
|
+
MediaUpload,
|
|
231
|
+
MediaUploadCheck,
|
|
232
|
+
} from '@wordpress/block-editor';
|
|
233
|
+
import {
|
|
234
|
+
PanelBody,
|
|
235
|
+
ToggleControl,
|
|
236
|
+
TextControl,
|
|
237
|
+
SelectControl,
|
|
238
|
+
RangeControl,
|
|
239
|
+
Button,
|
|
240
|
+
} from '@wordpress/components';
|
|
241
|
+
import './editor.scss';
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Edit component
|
|
245
|
+
*
|
|
246
|
+
* @param {Object} props Block props
|
|
247
|
+
* @param {Object} props.attributes Block attributes
|
|
248
|
+
* @param {Function} props.setAttributes Function to update attributes
|
|
249
|
+
* @return {JSX.Element} Block edit component
|
|
250
|
+
*/
|
|
251
|
+
export default function Edit({ attributes, setAttributes }) {
|
|
252
|
+
const { content, alignment, showBorder, imageId, imageUrl, columns } = attributes;
|
|
253
|
+
|
|
254
|
+
const blockProps = useBlockProps({
|
|
255
|
+
className: `align-${alignment}${showBorder ? ' has-border' : ''}`,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const onSelectImage = (media) => {
|
|
259
|
+
setAttributes({
|
|
260
|
+
imageId: media.id,
|
|
261
|
+
imageUrl: media.url,
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const onRemoveImage = () => {
|
|
266
|
+
setAttributes({
|
|
267
|
+
imageId: undefined,
|
|
268
|
+
imageUrl: undefined,
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<>
|
|
274
|
+
<BlockControls>
|
|
275
|
+
<AlignmentToolbar
|
|
276
|
+
value={alignment}
|
|
277
|
+
onChange={(newAlignment) =>
|
|
278
|
+
setAttributes({ alignment: newAlignment })
|
|
279
|
+
}
|
|
280
|
+
/>
|
|
281
|
+
</BlockControls>
|
|
282
|
+
|
|
283
|
+
<InspectorControls>
|
|
284
|
+
<PanelBody title={__('Settings', 'my-plugin')} initialOpen={true}>
|
|
285
|
+
<ToggleControl
|
|
286
|
+
label={__('Show Border', 'my-plugin')}
|
|
287
|
+
checked={showBorder}
|
|
288
|
+
onChange={(value) => setAttributes({ showBorder: value })}
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
<RangeControl
|
|
292
|
+
label={__('Columns', 'my-plugin')}
|
|
293
|
+
value={columns}
|
|
294
|
+
onChange={(value) => setAttributes({ columns: value })}
|
|
295
|
+
min={1}
|
|
296
|
+
max={6}
|
|
297
|
+
/>
|
|
298
|
+
|
|
299
|
+
<SelectControl
|
|
300
|
+
label={__('Layout', 'my-plugin')}
|
|
301
|
+
value={attributes.layout}
|
|
302
|
+
options={[
|
|
303
|
+
{ label: __('Default', 'my-plugin'), value: 'default' },
|
|
304
|
+
{ label: __('Card', 'my-plugin'), value: 'card' },
|
|
305
|
+
{ label: __('Minimal', 'my-plugin'), value: 'minimal' },
|
|
306
|
+
]}
|
|
307
|
+
onChange={(value) => setAttributes({ layout: value })}
|
|
308
|
+
/>
|
|
309
|
+
</PanelBody>
|
|
310
|
+
|
|
311
|
+
<PanelBody title={__('Image', 'my-plugin')} initialOpen={false}>
|
|
312
|
+
<MediaUploadCheck>
|
|
313
|
+
<MediaUpload
|
|
314
|
+
onSelect={onSelectImage}
|
|
315
|
+
allowedTypes={['image']}
|
|
316
|
+
value={imageId}
|
|
317
|
+
render={({ open }) => (
|
|
318
|
+
<div className="editor-post-featured-image">
|
|
319
|
+
{imageUrl ? (
|
|
320
|
+
<>
|
|
321
|
+
<img src={imageUrl} alt="" />
|
|
322
|
+
<Button
|
|
323
|
+
onClick={onRemoveImage}
|
|
324
|
+
isDestructive
|
|
325
|
+
>
|
|
326
|
+
{__('Remove Image', 'my-plugin')}
|
|
327
|
+
</Button>
|
|
328
|
+
</>
|
|
329
|
+
) : (
|
|
330
|
+
<Button onClick={open} variant="secondary">
|
|
331
|
+
{__('Select Image', 'my-plugin')}
|
|
332
|
+
</Button>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
/>
|
|
337
|
+
</MediaUploadCheck>
|
|
338
|
+
</PanelBody>
|
|
339
|
+
</InspectorControls>
|
|
340
|
+
|
|
341
|
+
<div {...blockProps}>
|
|
342
|
+
{imageUrl && (
|
|
343
|
+
<img src={imageUrl} alt="" className="block-image" />
|
|
344
|
+
)}
|
|
345
|
+
<RichText
|
|
346
|
+
tagName="p"
|
|
347
|
+
value={content}
|
|
348
|
+
onChange={(value) => setAttributes({ content: value })}
|
|
349
|
+
placeholder={__('Enter content...', 'my-plugin')}
|
|
350
|
+
allowedFormats={['core/bold', 'core/italic', 'core/link']}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
</>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### save.js (Frontend Output)
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
/**
|
|
362
|
+
* WordPress dependencies
|
|
363
|
+
*/
|
|
364
|
+
import { useBlockProps, RichText } from '@wordpress/block-editor';
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Save component
|
|
368
|
+
*
|
|
369
|
+
* @param {Object} props Block props
|
|
370
|
+
* @param {Object} props.attributes Block attributes
|
|
371
|
+
* @return {JSX.Element} Block save component
|
|
372
|
+
*/
|
|
373
|
+
export default function save({ attributes }) {
|
|
374
|
+
const { content, alignment, showBorder, imageUrl } = attributes;
|
|
375
|
+
|
|
376
|
+
const blockProps = useBlockProps.save({
|
|
377
|
+
className: `align-${alignment}${showBorder ? ' has-border' : ''}`,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div {...blockProps}>
|
|
382
|
+
{imageUrl && (
|
|
383
|
+
<img src={imageUrl} alt="" className="block-image" />
|
|
384
|
+
)}
|
|
385
|
+
<RichText.Content tagName="p" value={content} />
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Dynamic Block Development
|
|
394
|
+
|
|
395
|
+
Dynamic blocks render on the server, useful for content that changes or requires PHP logic.
|
|
396
|
+
|
|
397
|
+
### block.json for Dynamic Block
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"$schema": "https://schemas.wp.org/trunk/block.json",
|
|
402
|
+
"apiVersion": 3,
|
|
403
|
+
"name": "my-plugin/recent-posts",
|
|
404
|
+
"title": "Recent Posts",
|
|
405
|
+
"category": "widgets",
|
|
406
|
+
"icon": "list-view",
|
|
407
|
+
"description": "Display recent posts with customizable options.",
|
|
408
|
+
"supports": {
|
|
409
|
+
"html": false,
|
|
410
|
+
"align": ["wide", "full"]
|
|
411
|
+
},
|
|
412
|
+
"attributes": {
|
|
413
|
+
"numberOfPosts": {
|
|
414
|
+
"type": "number",
|
|
415
|
+
"default": 5
|
|
416
|
+
},
|
|
417
|
+
"postType": {
|
|
418
|
+
"type": "string",
|
|
419
|
+
"default": "post"
|
|
420
|
+
},
|
|
421
|
+
"showExcerpt": {
|
|
422
|
+
"type": "boolean",
|
|
423
|
+
"default": true
|
|
424
|
+
},
|
|
425
|
+
"showFeaturedImage": {
|
|
426
|
+
"type": "boolean",
|
|
427
|
+
"default": true
|
|
428
|
+
},
|
|
429
|
+
"categories": {
|
|
430
|
+
"type": "array",
|
|
431
|
+
"default": []
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
"textdomain": "my-plugin",
|
|
435
|
+
"editorScript": "file:./index.js",
|
|
436
|
+
"style": "file:./style-index.css",
|
|
437
|
+
"render": "file:./render.php"
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### edit.js for Dynamic Block
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
/**
|
|
445
|
+
* WordPress dependencies
|
|
446
|
+
*/
|
|
447
|
+
import { __ } from '@wordpress/i18n';
|
|
448
|
+
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
|
|
449
|
+
import {
|
|
450
|
+
PanelBody,
|
|
451
|
+
RangeControl,
|
|
452
|
+
ToggleControl,
|
|
453
|
+
SelectControl,
|
|
454
|
+
Spinner,
|
|
455
|
+
} from '@wordpress/components';
|
|
456
|
+
import { useSelect } from '@wordpress/data';
|
|
457
|
+
import { store as coreStore } from '@wordpress/core-data';
|
|
458
|
+
import ServerSideRender from '@wordpress/server-side-render';
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Edit component for dynamic block
|
|
462
|
+
*/
|
|
463
|
+
export default function Edit({ attributes, setAttributes }) {
|
|
464
|
+
const { numberOfPosts, postType, showExcerpt, showFeaturedImage } = attributes;
|
|
465
|
+
|
|
466
|
+
const blockProps = useBlockProps();
|
|
467
|
+
|
|
468
|
+
// Fetch post types for select
|
|
469
|
+
const postTypes = useSelect((select) => {
|
|
470
|
+
const { getPostTypes } = select(coreStore);
|
|
471
|
+
const types = getPostTypes({ per_page: -1 });
|
|
472
|
+
return types?.filter((type) => type.viewable && type.rest_base) || [];
|
|
473
|
+
}, []);
|
|
474
|
+
|
|
475
|
+
// Fetch categories
|
|
476
|
+
const categories = useSelect((select) => {
|
|
477
|
+
const { getEntityRecords } = select(coreStore);
|
|
478
|
+
return getEntityRecords('taxonomy', 'category', { per_page: -1 }) || [];
|
|
479
|
+
}, []);
|
|
480
|
+
|
|
481
|
+
const postTypeOptions = postTypes.map((type) => ({
|
|
482
|
+
label: type.labels.singular_name,
|
|
483
|
+
value: type.slug,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
return (
|
|
487
|
+
<>
|
|
488
|
+
<InspectorControls>
|
|
489
|
+
<PanelBody title={__('Settings', 'my-plugin')}>
|
|
490
|
+
<RangeControl
|
|
491
|
+
label={__('Number of Posts', 'my-plugin')}
|
|
492
|
+
value={numberOfPosts}
|
|
493
|
+
onChange={(value) => setAttributes({ numberOfPosts: value })}
|
|
494
|
+
min={1}
|
|
495
|
+
max={20}
|
|
496
|
+
/>
|
|
497
|
+
|
|
498
|
+
{postTypeOptions.length > 0 && (
|
|
499
|
+
<SelectControl
|
|
500
|
+
label={__('Post Type', 'my-plugin')}
|
|
501
|
+
value={postType}
|
|
502
|
+
options={postTypeOptions}
|
|
503
|
+
onChange={(value) => setAttributes({ postType: value })}
|
|
504
|
+
/>
|
|
505
|
+
)}
|
|
506
|
+
|
|
507
|
+
<ToggleControl
|
|
508
|
+
label={__('Show Excerpt', 'my-plugin')}
|
|
509
|
+
checked={showExcerpt}
|
|
510
|
+
onChange={(value) => setAttributes({ showExcerpt: value })}
|
|
511
|
+
/>
|
|
512
|
+
|
|
513
|
+
<ToggleControl
|
|
514
|
+
label={__('Show Featured Image', 'my-plugin')}
|
|
515
|
+
checked={showFeaturedImage}
|
|
516
|
+
onChange={(value) => setAttributes({ showFeaturedImage: value })}
|
|
517
|
+
/>
|
|
518
|
+
</PanelBody>
|
|
519
|
+
</InspectorControls>
|
|
520
|
+
|
|
521
|
+
<div {...blockProps}>
|
|
522
|
+
<ServerSideRender
|
|
523
|
+
block="my-plugin/recent-posts"
|
|
524
|
+
attributes={attributes}
|
|
525
|
+
LoadingResponsePlaceholder={() => (
|
|
526
|
+
<div className="loading-placeholder">
|
|
527
|
+
<Spinner />
|
|
528
|
+
<p>{__('Loading...', 'my-plugin')}</p>
|
|
529
|
+
</div>
|
|
530
|
+
)}
|
|
531
|
+
EmptyResponsePlaceholder={() => (
|
|
532
|
+
<p>{__('No posts found.', 'my-plugin')}</p>
|
|
533
|
+
)}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
</>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### render.php (Server-Side Render)
|
|
542
|
+
|
|
543
|
+
```php
|
|
544
|
+
<?php
|
|
545
|
+
/**
|
|
546
|
+
* Server-side rendering for Recent Posts block
|
|
547
|
+
*
|
|
548
|
+
* @var array $attributes Block attributes
|
|
549
|
+
* @var string $content Block content
|
|
550
|
+
* @var WP_Block $block Block instance
|
|
551
|
+
*/
|
|
552
|
+
|
|
553
|
+
declare(strict_types=1);
|
|
554
|
+
|
|
555
|
+
defined('ABSPATH') || exit;
|
|
556
|
+
|
|
557
|
+
$number_of_posts = absint($attributes['numberOfPosts'] ?? 5);
|
|
558
|
+
$post_type = sanitize_key($attributes['postType'] ?? 'post');
|
|
559
|
+
$show_excerpt = (bool) ($attributes['showExcerpt'] ?? true);
|
|
560
|
+
$show_featured_image = (bool) ($attributes['showFeaturedImage'] ?? true);
|
|
561
|
+
$categories = array_map('absint', $attributes['categories'] ?? []);
|
|
562
|
+
|
|
563
|
+
$query_args = [
|
|
564
|
+
'post_type' => $post_type,
|
|
565
|
+
'posts_per_page' => $number_of_posts,
|
|
566
|
+
'post_status' => 'publish',
|
|
567
|
+
'orderby' => 'date',
|
|
568
|
+
'order' => 'DESC',
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
if (!empty($categories) && $post_type === 'post') {
|
|
572
|
+
$query_args['category__in'] = $categories;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
$posts_query = new WP_Query($query_args);
|
|
576
|
+
|
|
577
|
+
$wrapper_attributes = get_block_wrapper_attributes([
|
|
578
|
+
'class' => 'recent-posts-block',
|
|
579
|
+
]);
|
|
580
|
+
?>
|
|
581
|
+
|
|
582
|
+
<div <?php echo $wrapper_attributes; ?>>
|
|
583
|
+
<?php if ($posts_query->have_posts()) : ?>
|
|
584
|
+
<ul class="recent-posts-list">
|
|
585
|
+
<?php while ($posts_query->have_posts()) : $posts_query->the_post(); ?>
|
|
586
|
+
<li class="recent-posts-item">
|
|
587
|
+
<?php if ($show_featured_image && has_post_thumbnail()) : ?>
|
|
588
|
+
<div class="post-thumbnail">
|
|
589
|
+
<a href="<?php the_permalink(); ?>">
|
|
590
|
+
<?php the_post_thumbnail('thumbnail'); ?>
|
|
591
|
+
</a>
|
|
592
|
+
</div>
|
|
593
|
+
<?php endif; ?>
|
|
594
|
+
|
|
595
|
+
<div class="post-content">
|
|
596
|
+
<h3 class="post-title">
|
|
597
|
+
<a href="<?php the_permalink(); ?>">
|
|
598
|
+
<?php the_title(); ?>
|
|
599
|
+
</a>
|
|
600
|
+
</h3>
|
|
601
|
+
|
|
602
|
+
<time class="post-date" datetime="<?php echo esc_attr(get_the_date('c')); ?>">
|
|
603
|
+
<?php echo esc_html(get_the_date()); ?>
|
|
604
|
+
</time>
|
|
605
|
+
|
|
606
|
+
<?php if ($show_excerpt) : ?>
|
|
607
|
+
<div class="post-excerpt">
|
|
608
|
+
<?php the_excerpt(); ?>
|
|
609
|
+
</div>
|
|
610
|
+
<?php endif; ?>
|
|
611
|
+
</div>
|
|
612
|
+
</li>
|
|
613
|
+
<?php endwhile; ?>
|
|
614
|
+
</ul>
|
|
615
|
+
<?php else : ?>
|
|
616
|
+
<p class="no-posts"><?php esc_html_e('No posts found.', 'my-plugin'); ?></p>
|
|
617
|
+
<?php endif; ?>
|
|
618
|
+
|
|
619
|
+
<?php wp_reset_postdata(); ?>
|
|
620
|
+
</div>
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Block Patterns
|
|
626
|
+
|
|
627
|
+
### Registering Patterns in PHP
|
|
628
|
+
|
|
629
|
+
```php
|
|
630
|
+
<?php
|
|
631
|
+
/**
|
|
632
|
+
* Register block patterns
|
|
633
|
+
*/
|
|
634
|
+
function my_plugin_register_patterns(): void {
|
|
635
|
+
// Register pattern category
|
|
636
|
+
register_block_pattern_category('my-plugin-patterns', [
|
|
637
|
+
'label' => __('My Plugin Patterns', 'my-plugin'),
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
// Register pattern
|
|
641
|
+
register_block_pattern('my-plugin/hero-section', [
|
|
642
|
+
'title' => __('Hero Section', 'my-plugin'),
|
|
643
|
+
'description' => __('A full-width hero section with heading and CTA.', 'my-plugin'),
|
|
644
|
+
'categories' => ['my-plugin-patterns', 'featured'],
|
|
645
|
+
'keywords' => ['hero', 'banner', 'cta'],
|
|
646
|
+
'blockTypes' => ['core/template-part/header'],
|
|
647
|
+
'content' => '<!-- wp:cover {"dimRatio":60,"overlayColor":"black","align":"full"} -->
|
|
648
|
+
<div class="wp-block-cover alignfull">
|
|
649
|
+
<span class="wp-block-cover__background has-black-background-color has-background-dim-60"></span>
|
|
650
|
+
<div class="wp-block-cover__inner-container">
|
|
651
|
+
<!-- wp:heading {"textAlign":"center","level":1} -->
|
|
652
|
+
<h1 class="wp-block-heading has-text-align-center">' . esc_html__('Welcome', 'my-plugin') . '</h1>
|
|
653
|
+
<!-- /wp:heading -->
|
|
654
|
+
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
|
|
655
|
+
<div class="wp-block-buttons">
|
|
656
|
+
<!-- wp:button -->
|
|
657
|
+
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button">' . esc_html__('Get Started', 'my-plugin') . '</a></div>
|
|
658
|
+
<!-- /wp:button -->
|
|
659
|
+
</div>
|
|
660
|
+
<!-- /wp:buttons -->
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
<!-- /wp:cover -->',
|
|
664
|
+
]);
|
|
665
|
+
}
|
|
666
|
+
add_action('init', 'my_plugin_register_patterns');
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Pattern File (patterns/feature-grid.php)
|
|
670
|
+
|
|
671
|
+
```php
|
|
672
|
+
<?php
|
|
673
|
+
/**
|
|
674
|
+
* Title: Feature Grid
|
|
675
|
+
* Slug: my-plugin/feature-grid
|
|
676
|
+
* Categories: my-plugin-patterns
|
|
677
|
+
* Keywords: features, grid, cards
|
|
678
|
+
* Block Types: core/group
|
|
679
|
+
* Viewport Width: 1200
|
|
680
|
+
*/
|
|
681
|
+
|
|
682
|
+
declare(strict_types=1);
|
|
683
|
+
|
|
684
|
+
defined('ABSPATH') || exit;
|
|
685
|
+
?>
|
|
686
|
+
|
|
687
|
+
<!-- wp:group {"align":"wide","layout":{"type":"constrained"}} -->
|
|
688
|
+
<div class="wp-block-group alignwide">
|
|
689
|
+
<!-- wp:heading {"textAlign":"center"} -->
|
|
690
|
+
<h2 class="wp-block-heading has-text-align-center"><?php esc_html_e('Our Features', 'my-plugin'); ?></h2>
|
|
691
|
+
<!-- /wp:heading -->
|
|
692
|
+
|
|
693
|
+
<!-- wp:columns {"align":"wide"} -->
|
|
694
|
+
<div class="wp-block-columns alignwide">
|
|
695
|
+
<!-- wp:column -->
|
|
696
|
+
<div class="wp-block-column">
|
|
697
|
+
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|m","right":"var:preset|spacing|m","bottom":"var:preset|spacing|m","left":"var:preset|spacing|m"}},"border":{"radius":"8px"}},"backgroundColor":"base-alt"} -->
|
|
698
|
+
<div class="wp-block-group has-base-alt-background-color has-background">
|
|
699
|
+
<!-- wp:image {"width":"64px","sizeSlug":"full"} -->
|
|
700
|
+
<figure class="wp-block-image size-full is-resized" style="width:64px"><img src="<?php echo esc_url(get_template_directory_uri()); ?>/assets/images/icon-feature-1.svg" alt="" /></figure>
|
|
701
|
+
<!-- /wp:image -->
|
|
702
|
+
<!-- wp:heading {"level":3} -->
|
|
703
|
+
<h3 class="wp-block-heading"><?php esc_html_e('Feature One', 'my-plugin'); ?></h3>
|
|
704
|
+
<!-- /wp:heading -->
|
|
705
|
+
<!-- wp:paragraph -->
|
|
706
|
+
<p><?php esc_html_e('Description of the first feature goes here.', 'my-plugin'); ?></p>
|
|
707
|
+
<!-- /wp:paragraph -->
|
|
708
|
+
</div>
|
|
709
|
+
<!-- /wp:group -->
|
|
710
|
+
</div>
|
|
711
|
+
<!-- /wp:column -->
|
|
712
|
+
|
|
713
|
+
<!-- wp:column -->
|
|
714
|
+
<div class="wp-block-column">
|
|
715
|
+
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|m","right":"var:preset|spacing|m","bottom":"var:preset|spacing|m","left":"var:preset|spacing|m"}},"border":{"radius":"8px"}},"backgroundColor":"base-alt"} -->
|
|
716
|
+
<div class="wp-block-group has-base-alt-background-color has-background">
|
|
717
|
+
<!-- wp:image {"width":"64px","sizeSlug":"full"} -->
|
|
718
|
+
<figure class="wp-block-image size-full is-resized" style="width:64px"><img src="<?php echo esc_url(get_template_directory_uri()); ?>/assets/images/icon-feature-2.svg" alt="" /></figure>
|
|
719
|
+
<!-- /wp:image -->
|
|
720
|
+
<!-- wp:heading {"level":3} -->
|
|
721
|
+
<h3 class="wp-block-heading"><?php esc_html_e('Feature Two', 'my-plugin'); ?></h3>
|
|
722
|
+
<!-- /wp:heading -->
|
|
723
|
+
<!-- wp:paragraph -->
|
|
724
|
+
<p><?php esc_html_e('Description of the second feature goes here.', 'my-plugin'); ?></p>
|
|
725
|
+
<!-- /wp:paragraph -->
|
|
726
|
+
</div>
|
|
727
|
+
<!-- /wp:group -->
|
|
728
|
+
</div>
|
|
729
|
+
<!-- /wp:column -->
|
|
730
|
+
|
|
731
|
+
<!-- wp:column -->
|
|
732
|
+
<div class="wp-block-column">
|
|
733
|
+
<!-- wp:group {"style":{"spacing":{"padding":{"top":"var:preset|spacing|m","right":"var:preset|spacing|m","bottom":"var:preset|spacing|m","left":"var:preset|spacing|m"}},"border":{"radius":"8px"}},"backgroundColor":"base-alt"} -->
|
|
734
|
+
<div class="wp-block-group has-base-alt-background-color has-background">
|
|
735
|
+
<!-- wp:image {"width":"64px","sizeSlug":"full"} -->
|
|
736
|
+
<figure class="wp-block-image size-full is-resized" style="width:64px"><img src="<?php echo esc_url(get_template_directory_uri()); ?>/assets/images/icon-feature-3.svg" alt="" /></figure>
|
|
737
|
+
<!-- /wp:image -->
|
|
738
|
+
<!-- wp:heading {"level":3} -->
|
|
739
|
+
<h3 class="wp-block-heading"><?php esc_html_e('Feature Three', 'my-plugin'); ?></h3>
|
|
740
|
+
<!-- /wp:heading -->
|
|
741
|
+
<!-- wp:paragraph -->
|
|
742
|
+
<p><?php esc_html_e('Description of the third feature goes here.', 'my-plugin'); ?></p>
|
|
743
|
+
<!-- /wp:paragraph -->
|
|
744
|
+
</div>
|
|
745
|
+
<!-- /wp:group -->
|
|
746
|
+
</div>
|
|
747
|
+
<!-- /wp:column -->
|
|
748
|
+
</div>
|
|
749
|
+
<!-- /wp:columns -->
|
|
750
|
+
</div>
|
|
751
|
+
<!-- /wp:group -->
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## Interactivity API (WordPress 6.5+)
|
|
757
|
+
|
|
758
|
+
For client-side interactivity without custom JavaScript build processes.
|
|
759
|
+
|
|
760
|
+
### Interactive Block Example
|
|
761
|
+
|
|
762
|
+
```json
|
|
763
|
+
{
|
|
764
|
+
"$schema": "https://schemas.wp.org/trunk/block.json",
|
|
765
|
+
"apiVersion": 3,
|
|
766
|
+
"name": "my-plugin/counter",
|
|
767
|
+
"title": "Interactive Counter",
|
|
768
|
+
"supports": {
|
|
769
|
+
"interactivity": true
|
|
770
|
+
},
|
|
771
|
+
"textdomain": "my-plugin",
|
|
772
|
+
"editorScript": "file:./index.js",
|
|
773
|
+
"viewScriptModule": "file:./view.js",
|
|
774
|
+
"render": "file:./render.php"
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### render.php with Interactivity
|
|
779
|
+
|
|
780
|
+
```php
|
|
781
|
+
<?php
|
|
782
|
+
/**
|
|
783
|
+
* Interactive counter block render
|
|
784
|
+
*/
|
|
785
|
+
|
|
786
|
+
declare(strict_types=1);
|
|
787
|
+
|
|
788
|
+
$initial_count = absint($attributes['initialCount'] ?? 0);
|
|
789
|
+
|
|
790
|
+
wp_interactivity_state('my-plugin/counter', [
|
|
791
|
+
'count' => $initial_count,
|
|
792
|
+
]);
|
|
793
|
+
?>
|
|
794
|
+
|
|
795
|
+
<div
|
|
796
|
+
<?php echo get_block_wrapper_attributes(); ?>
|
|
797
|
+
data-wp-interactive="my-plugin/counter"
|
|
798
|
+
>
|
|
799
|
+
<button
|
|
800
|
+
data-wp-on--click="actions.decrement"
|
|
801
|
+
data-wp-bind--disabled="!state.canDecrement"
|
|
802
|
+
>
|
|
803
|
+
-
|
|
804
|
+
</button>
|
|
805
|
+
|
|
806
|
+
<span data-wp-text="state.count"></span>
|
|
807
|
+
|
|
808
|
+
<button data-wp-on--click="actions.increment">
|
|
809
|
+
+
|
|
810
|
+
</button>
|
|
811
|
+
</div>
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### view.js (Interactivity Store)
|
|
815
|
+
|
|
816
|
+
```javascript
|
|
817
|
+
/**
|
|
818
|
+
* WordPress dependencies
|
|
819
|
+
*/
|
|
820
|
+
import { store, getContext } from '@wordpress/interactivity';
|
|
821
|
+
|
|
822
|
+
store('my-plugin/counter', {
|
|
823
|
+
state: {
|
|
824
|
+
get canDecrement() {
|
|
825
|
+
const { count } = store('my-plugin/counter').state;
|
|
826
|
+
return count > 0;
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
actions: {
|
|
830
|
+
increment() {
|
|
831
|
+
const state = store('my-plugin/counter').state;
|
|
832
|
+
state.count++;
|
|
833
|
+
},
|
|
834
|
+
decrement() {
|
|
835
|
+
const state = store('my-plugin/counter').state;
|
|
836
|
+
if (state.count > 0) {
|
|
837
|
+
state.count--;
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## Best Practices
|
|
847
|
+
|
|
848
|
+
### Do
|
|
849
|
+
|
|
850
|
+
- Use `block.json` for all block metadata (API version 3)
|
|
851
|
+
- Leverage `useBlockProps` for proper block wrapper handling
|
|
852
|
+
- Use `InspectorControls` for sidebar settings
|
|
853
|
+
- Implement `example` in block.json for previews
|
|
854
|
+
- Use `ServerSideRender` for dynamic block previews
|
|
855
|
+
- Follow WordPress Coding Standards for PHP render callbacks
|
|
856
|
+
- Use CSS custom properties from theme.json
|
|
857
|
+
- Test blocks in isolation and within posts
|
|
858
|
+
- Support align wide/full when appropriate
|
|
859
|
+
- Use the Interactivity API for simple client-side logic
|
|
860
|
+
|
|
861
|
+
### Do Not
|
|
862
|
+
|
|
863
|
+
- Skip the `$schema` property in block.json
|
|
864
|
+
- Use deprecated block API versions (use apiVersion 3)
|
|
865
|
+
- Forget to escape output in render.php
|
|
866
|
+
- Hardcode styles (use theme.json presets)
|
|
867
|
+
- Create unnecessary server requests in edit components
|
|
868
|
+
- Ignore block validation warnings
|
|
869
|
+
- Skip internationalization for text strings
|
|
870
|
+
- Bundle React/WordPress packages (use externals)
|