eddev 2.3.12 → 2.3.13

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 (95) hide show
  1. package/dist/app/entry/MetaTags.d.ts.map +1 -1
  2. package/dist/app/entry/MetaTags.js +2 -0
  3. package/dist/app/lib/blocks/block-utils.d.ts +2 -2
  4. package/dist/app/lib/blocks/block-utils.d.ts.map +1 -1
  5. package/dist/app/lib/blocks/block-utils.js +10 -3
  6. package/dist/app/lib/blocks/editor/root-blocks.d.ts.map +1 -1
  7. package/dist/app/lib/blocks/editor/root-blocks.js +2 -2
  8. package/dist/app/lib/devtools/hooks/useTailwind.d.ts +45 -45
  9. package/dist/app/lib/hooks/query-hooks.d.ts +8 -8
  10. package/dist/app/lib/hooks/query-hooks.d.ts.map +1 -1
  11. package/dist/app/lib/hooks/query-hooks.js +2 -1
  12. package/dist/app/lib/runtime/apiConfig.d.ts +15 -0
  13. package/dist/app/lib/runtime/apiConfig.d.ts.map +1 -1
  14. package/dist/app/server/proxy-wp-admin.d.ts.map +1 -1
  15. package/dist/app/server/proxy-wp-admin.js +1 -0
  16. package/dist/app/server/server-context.d.ts +2 -2
  17. package/dist/app/server/server-context.d.ts.map +1 -1
  18. package/dist/app/utils/trpc-client.d.ts.map +1 -1
  19. package/dist/app/utils/trpc-client.js +19 -3
  20. package/dist/node/cli/version.d.ts +1 -1
  21. package/dist/node/cli/version.js +1 -1
  22. package/dist/node/compiler/dev-server.js +1 -1
  23. package/dist/node/compiler/get-vite-config.d.ts.map +1 -1
  24. package/dist/node/compiler/get-vite-config.js +1 -0
  25. package/dist/node/compiler/vinxi-app.d.ts +1 -1
  26. package/dist/node/compiler/vinxi-app.d.ts.map +1 -1
  27. package/dist/node/utils/fetch-wp.d.ts.map +1 -1
  28. package/dist/node/utils/fetch-wp.js +2 -1
  29. package/dist/node/utils/fs.d.ts +22 -19
  30. package/dist/node/utils/fs.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/skills/eddev/SKILL.md +156 -0
  33. package/skills/eddev/docs/acf/admin-panel-widgets.mdx +99 -0
  34. package/skills/eddev/docs/acf/custom-enums.mdx +75 -0
  35. package/skills/eddev/docs/acf/custom-fields.mdx +131 -0
  36. package/skills/eddev/docs/acf.mdx +31 -0
  37. package/skills/eddev/docs/blocks/block-definition.mdx +189 -0
  38. package/skills/eddev/docs/blocks/core-blocks.mdx +86 -0
  39. package/skills/eddev/docs/blocks/data-and-editing.mdx +219 -0
  40. package/skills/eddev/docs/blocks/editor-config.mdx +157 -0
  41. package/skills/eddev/docs/blocks/nested-blocks.mdx +129 -0
  42. package/skills/eddev/docs/blocks/overview.mdx +58 -0
  43. package/skills/eddev/docs/blocks/template-parts.mdx +131 -0
  44. package/skills/eddev/docs/config.mdx +200 -0
  45. package/skills/eddev/docs/design/color.mdx +185 -0
  46. package/skills/eddev/docs/design/favicons.mdx +103 -0
  47. package/skills/eddev/docs/design/grid.mdx +120 -0
  48. package/skills/eddev/docs/design/icons.mdx +197 -0
  49. package/skills/eddev/docs/design/responsive-scaling.mdx +312 -0
  50. package/skills/eddev/docs/design/type.mdx +125 -0
  51. package/skills/eddev/docs/devtool/cli.mdx +201 -0
  52. package/skills/eddev/docs/devtool/overlay.mdx +5 -0
  53. package/skills/eddev/docs/getting-started.mdx +53 -0
  54. package/skills/eddev/docs/graphql/extending.mdx +186 -0
  55. package/skills/eddev/docs/graphql/fragments.mdx +107 -0
  56. package/skills/eddev/docs/graphql/global-data.mdx +47 -0
  57. package/skills/eddev/docs/graphql/infinite-queries.mdx +95 -0
  58. package/skills/eddev/docs/graphql/mutation-hooks.mdx +111 -0
  59. package/skills/eddev/docs/graphql/query-hooks.mdx +122 -0
  60. package/skills/eddev/docs/graphql/tooling.mdx +50 -0
  61. package/skills/eddev/docs/graphql.mdx +97 -0
  62. package/skills/eddev/docs/guides/color-schemes.mdx +204 -0
  63. package/skills/eddev/docs/guides/integrations.mdx +3 -0
  64. package/skills/eddev/docs/guides/page-transitions.mdx +5 -0
  65. package/skills/eddev/docs/guides/seo.mdx +5 -0
  66. package/skills/eddev/docs/guides/state-management.mdx +5 -0
  67. package/skills/eddev/docs/infra/caching.mdx +9 -0
  68. package/skills/eddev/docs/infra/deployment.mdx +13 -0
  69. package/skills/eddev/docs/infra/local.mdx +5 -0
  70. package/skills/eddev/docs/infra/security.mdx +11 -0
  71. package/skills/eddev/docs/routing/api.mdx +731 -0
  72. package/skills/eddev/docs/routing/custom.mdx +123 -0
  73. package/skills/eddev/docs/routing/full-details.mdx +37 -0
  74. package/skills/eddev/docs/routing/wordpress.mdx +70 -0
  75. package/skills/eddev/docs/routing.mdx +18 -0
  76. package/skills/eddev/docs/serverless/functions.mdx +436 -0
  77. package/skills/eddev/docs/serverless.mdx +202 -0
  78. package/skills/eddev/docs/snippets/automated-block-layouts.mdx +97 -0
  79. package/skills/eddev/docs/snippets/custom-routes-and-urls.mdx +91 -0
  80. package/skills/eddev/docs/snippets/multiple-editable-zones.mdx +87 -0
  81. package/skills/eddev/docs/snippets/querying-specific-blocks.mdx +164 -0
  82. package/skills/eddev/docs/snippets/submitting-forms-to-rpc.mdx +91 -0
  83. package/skills/eddev/docs/snippets/svgs.mdx +38 -0
  84. package/skills/eddev/docs/snippets/type-safe-acf-dropdowns.mdx +72 -0
  85. package/skills/eddev/docs/snippets.mdx +19 -0
  86. package/skills/eddev/docs/software.mdx +19 -0
  87. package/skills/eddev/docs/stack/how-it-works.mdx +50 -0
  88. package/skills/eddev/docs/stack/overview.mdx +56 -0
  89. package/skills/eddev/docs/stack/spa-vs-ssr.mdx +52 -0
  90. package/skills/eddev/docs/views/app-view.mdx +97 -0
  91. package/skills/eddev/docs/views/page-templates.mdx +82 -0
  92. package/skills/eddev/docs/views/queries.mdx +116 -0
  93. package/skills/eddev/docs/views.mdx +63 -0
  94. package/skills/eddev/index.mdx +79 -0
  95. package/tsconfig.app.json +2 -2
@@ -0,0 +1,202 @@
1
+ # Serverless (/docs/serverless)
2
+
3
+ **Run the public eddev frontend on Vercel while WordPress owns content and routing.**
4
+
5
+ eddev serverless mode runs the public React frontend on a JavaScript host such as Vercel, while WordPress remains the CMS and routing authority.
6
+
7
+ In production, the usual shape is:
8
+
9
+ * `cms.website.com` points at the WordPress origin, usually behind Cloudflare.
10
+ * `website.com` points at the Vercel deployment.
11
+ * Vercel receives public page requests, asks WordPress for the matched route data, then server-renders the React view.
12
+ * WordPress still resolves the route, executes the view/block GraphQL files, owns `/wp-admin`, and provides runtime query and mutation endpoints.
13
+
14
+ The Vercel app is not a separate router. It is a rendering and API layer in front of the WordPress origin.
15
+
16
+ ## Deploying To Vercel [#deploying-to-vercel]
17
+
18
+ For a normal project:
19
+
20
+ 1. Import the site's GitHub repository into Vercel.
21
+ 2. Add `SITE_URL` in Vercel project environment variables.
22
+ 3. Set `SITE_URL` to the WordPress origin, including the protocol, for example `https://cms.staging.sff.org.au/`.
23
+ 4. If the WordPress origin uses eddev Access Control, add `SITE_API_KEY` in Vercel as well.
24
+ 5. Deploy.
25
+
26
+ During the build, eddev reads `SITE_URL` and uses it as the origin for WordPress route data, global app data, named GraphQL query hooks, mutations, plugin assets, uploads, and proxied WordPress/admin requests.
27
+
28
+ <Callout type="warning">
29
+ Do not point `SITE_URL` at the Vercel frontend. It should point at the WordPress origin, usually the `cms.` host.
30
+ </Callout>
31
+
32
+ ## Request Flow [#request-flow]
33
+
34
+ When a visitor requests a page from the Vercel deployment:
35
+
36
+ 1. The serverless app normalizes the URL and asks WordPress for route data with `?_props=1` and `_ssr=1`.
37
+ 2. WordPress resolves the request through the normal template hierarchy, runs the paired view GraphQL file, and returns the JSON route payload.
38
+ 3. The serverless app replaces origin URLs where configured, fetches app data, and renders the matching React view.
39
+ 4. Browser navigation continues through eddev's route data endpoints instead of loading a whole WordPress HTML page.
40
+
41
+ Runtime GraphQL hooks follow the same split:
42
+
43
+ * browser-side query hooks on Vercel call `/_data/query/{queryName}`
44
+ * browser-side mutations on Vercel call `/_data/mutation/{mutationName}`
45
+ * the serverless app forwards those to the WordPress REST endpoints under `/wp-json/ed/v1/query/` and `/wp-json/ed/v1/mutation/`
46
+
47
+ Server routes from `server/routes/` are deployed with the serverless app. See [RPC Functions](./functions) for route authoring and React usage.
48
+
49
+ ## Basic Configuration [#basic-configuration]
50
+
51
+ Serverless behaviour is configured in `ed.config.json`. The `serverless` key controls deployment and routing behaviour, while the `cache` key controls how both the WordPress and JavaScript server sides cache data.
52
+
53
+ ```json filename="ed.config.json"
54
+ {
55
+ "$schema": ".ed.config.schema.json",
56
+ "version": "2",
57
+ "serverless": {
58
+ "enabled": true,
59
+ "uploads": "remote",
60
+ "plugins": "remote",
61
+ "admin": "hide",
62
+ "themeAssets": ["assets/**/*"],
63
+ "originProtection": {
64
+ "requireLogin": false
65
+ },
66
+ "endpoints": {
67
+ "cms.website.com": "website.com"
68
+ }
69
+ },
70
+ "cache": {
71
+ "*": {
72
+ "serverless": {
73
+ "isr": true,
74
+ "dataCache": "in-memory"
75
+ },
76
+ "wordpress": {
77
+ "cacheHeaders": true,
78
+ "transients": true
79
+ },
80
+ "pageDataTTL": 300,
81
+ "appDataTTL": 300,
82
+ "queryHooksTTL": 300
83
+ }
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### `serverless` [#serverless]
89
+
90
+ | Option | What It Does |
91
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
92
+ | `enabled` | Enables serverless deployment support. This is normally `true`. |
93
+ | `uploads` | Controls whether `/wp-content/uploads/**` URLs are left as remote origin URLs or proxied through the serverless deployment. Use `remote` by default. |
94
+ | `plugins` | Controls whether `/wp-content/plugins/**` URLs are left as remote origin URLs or proxied through the serverless deployment. Use `remote` by default. |
95
+ | `admin` | `proxy` forwards WordPress admin/API URLs through the deployment. `hide` returns a 404 for `/wp-admin`, `/wp-json`, `/wp-login.php`, GraphQL, and PHP-style URLs in production. |
96
+ | `originProtection.requireLogin` | Force-enables origin protection from config. This prevents non-logged-in visitors from accessing the WordPress origin frontend, while still allowing the serverless frontend to fetch with an API key. |
97
+ | `themeAssets` | Static theme asset folders to include in the serverless deployment. The starter theme uses `["assets/**/*"]`. |
98
+ | `endpoints` | Maps WordPress hostnames to serverless hostnames. This tells the WordPress-hosted SPA/admin where to call serverless endpoints such as RPC functions. |
99
+ | `cors.origins` | Additional allowed CORS origins. WordPress and serverless hosts from `endpoints` are already allowed. |
100
+ | `csp` | Content Security Policy settings for the serverless app, including autodetected origins, common origins, nonces, and directive values. |
101
+
102
+ `serverless.endpoints` values are hostnames, not full URLs:
103
+
104
+ ```json filename="ed.config.json"
105
+ {
106
+ "serverless": {
107
+ "endpoints": {
108
+ "cms.staging.sff.org.au": "sff-staging.vercel.app",
109
+ "cms.sff.org.au": "www.sff.org.au"
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ You can use `"*"` as a fallback key, but explicit hostnames are better for production and staging because they make the CMS-to-frontend relationship clear.
116
+
117
+ <Callout type="info">
118
+ The WordPress PHP layer injects `window.SERVERLESS_ENDPOINT` into the WordPress frontend and admin when it can match the current WordPress host in `serverless.endpoints`. The RPC client uses that value when the site is running in WordPress-hosted SPA mode, so incorrect endpoint mappings usually show up as RPC calls going to the wrong host.
119
+ </Callout>
120
+
121
+ ### `cache` [#cache]
122
+
123
+ The `cache` object is a map of WordPress origin hostnames to cache settings. Use `"*"` for the default, and add host-specific entries when staging and production need different behaviour.
124
+
125
+ Cache host keys are matched as hostnames. Ports are ignored, exact hostnames win over wildcard patterns, wildcard patterns like `"*.website.com"` are supported, and `"*"` is used as the fallback.
126
+
127
+ | Option | What It Does |
128
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129
+ | `pageDataTTL` | Seconds to cache route data and rendered page responses. |
130
+ | `appDataTTL` | Seconds to cache global app data from `views/_app.graphql`. |
131
+ | `queryHooksTTL` | Seconds to cache named runtime GraphQL query hook responses. |
132
+ | `serverless.isr` | Enables serverless response caching/ISR for rendered pages, route data, and named query data. Set it to `false` for dynamic, no-store serverless responses. |
133
+ | `serverless.dataCache` | Controls the serverless origin-data cache. Use `in-memory` for the normal LRU/SWR cache, or `none` to fetch WordPress every time the serverless function needs data. |
134
+ | `wordpress.cacheHeaders` | Allows WordPress data responses to emit cache headers. |
135
+ | `wordpress.transients` | Allows WordPress to cache data in transients. |
136
+
137
+ `serverless.isr` and `serverless.dataCache` control different cache layers:
138
+
139
+ | Configuration | Behaviour |
140
+ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `isr: true`, `dataCache: "in-memory"` | Normal production mode. Serverless responses can be cached, and serverless revalidation/data requests are protected by an in-memory data cache. |
142
+ | `isr: true`, `dataCache: "none"` | Serverless responses can still be cached, but every revalidation fetch goes back to WordPress. |
143
+ | `isr: false`, `dataCache: "in-memory"` | Serverless responses are dynamic/no-store, but repeated WordPress data fetches can still use the serverless in-memory cache. |
144
+ | `isr: false`, `dataCache: "none"` | Fully dynamic serverless mode. Responses are no-store and serverless data fetches go back to WordPress every time. |
145
+
146
+ Queries and mutations are handled differently:
147
+
148
+ * route data and app data use the page/app TTLs
149
+ * named query hooks use `queryHooksTTL`
150
+ * mutation responses are not cached
151
+ * requests with varied auth/session headers avoid the shared named-query data cache
152
+
153
+ Individual GraphQL query files can still set `# ttl: 300` or `# nocache`; see [GraphQL caching](/docs/graphql#caching).
154
+
155
+ ## Origin Access Control [#origin-access-control]
156
+
157
+ Use eddev's Access Control feature to protect the WordPress origin. Do not use WP Engine's password protection for this, because Vercel still needs to fetch WordPress route data, GraphQL results, uploads, and REST endpoints without receiving a human password prompt.
158
+
159
+ In WordPress:
160
+
161
+ 1. Open **Settings → Access Control**.
162
+ 2. Generate an API key.
163
+ 3. Enable API key protection if it is not force-enabled by config.
164
+ 4. Optionally enable simple password protection for people who should be able to preview the CMS-hosted frontend directly.
165
+ 5. Add the generated key to Vercel as `SITE_API_KEY`.
166
+
167
+ When `SITE_API_KEY` is set, eddev adds it to server-to-server WordPress requests as the `x-ed-api-key` header. The PHP origin checks that header before allowing access.
168
+
169
+ Access Control allows:
170
+
171
+ * logged-in WordPress users
172
+ * requests with a valid eddev API key
173
+ * users with the configured basic-auth credentials, when password protection is enabled
174
+
175
+ Everyone else receives an Access Denied page or a password prompt, depending on the settings.
176
+
177
+ <Callout type="info">
178
+ An API key only allows a request to reach the origin. It does not grant WordPress permissions or log the request in as a user.
179
+ </Callout>
180
+
181
+ For stricter projects, set `serverless.originProtection.requireLogin` to `true`. That force-enables API-key protection from committed config, so the WordPress origin cannot accidentally be left public through an admin setting.
182
+
183
+ ## Domain Setup [#domain-setup]
184
+
185
+ The common production setup is:
186
+
187
+ | Host | Points To | Purpose |
188
+ | --------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------- |
189
+ | `cms.website.com` | WP Engine or another WordPress host, often proxied through Cloudflare | WordPress admin, CMS preview, GraphQL, REST, uploads, and route data origin |
190
+ | `website.com` / `www.website.com` | Vercel | Public server-rendered frontend and serverless routes |
191
+
192
+ Keep the WordPress origin reachable by the Vercel deployment. If Cloudflare sits in front of `cms.website.com`, make sure it passes the `x-ed-api-key` header and does not add its own interactive challenge to server-to-server requests.
193
+
194
+ ## Deployment Checklist [#deployment-checklist]
195
+
196
+ * `SITE_URL` in Vercel points to the WordPress origin, for example `https://cms.website.com/`.
197
+ * `SITE_API_KEY` is set in Vercel when Access Control is enabled.
198
+ * The same API key exists in WordPress under **Settings → Access Control**.
199
+ * `serverless.endpoints` maps each WordPress hostname to the correct Vercel/public hostname.
200
+ * `serverless.admin` is set intentionally, usually `hide` for public deployments.
201
+ * `cache["*"]` or a host-specific cache entry sets realistic TTLs for route data, app data, and runtime query hooks.
202
+ * Cloudflare or host-level protection does not block Vercel's server-to-server origin requests.
@@ -0,0 +1,97 @@
1
+ # Automated Block Layouts (/docs/snippets/automated-block-layouts)
2
+
3
+ **Use block flags and wrapBlock to keep layout rules out of every block**
4
+
5
+ Use this when the page layout should automatically wrap blocks based on block metadata, rather than making every block render its own page grid.
6
+
7
+ For the underlying APIs, see [Block Flags](/docs/blocks/block-definition#block-flags), [Querying Blocks From Views](/docs/blocks/data-and-editing#querying-blocks-from-views), and [Nested Blocks](/docs/blocks/nested-blocks).
8
+
9
+ ## Put Layout Hints In Block Metadata [#put-layout-hints-in-block-metadata]
10
+
11
+ Keep flags small. They are copied into the block payload and are available to `ContentBlocks`.
12
+
13
+ ```tsx filename="blocks/content/accordion.tsx"
14
+ import { defineBlock, EditableText, InnerBlocks } from "eddev/blocks"
15
+
16
+ export const meta: BlockMeta = {
17
+ title: "Accordion",
18
+ icon: "info",
19
+ tags: ["#subpage"],
20
+ flags: {
21
+ contentWidth: true,
22
+ marginRule: "long-card",
23
+ },
24
+ }
25
+
26
+ export default defineBlock("content/accordion", () => {
27
+ return (
28
+ <section>
29
+ <EditableText as="h3" store="title" defaultValue="Enter a title" plainText />
30
+ <InnerBlocks allowedBlocks={["core/paragraph", "core/list", "content/button-row"]} />
31
+ </section>
32
+ )
33
+ })
34
+ ```
35
+
36
+ ```tsx filename="blocks/content/card-row.tsx"
37
+ import { defineBlock, InnerBlocks } from "eddev/blocks"
38
+
39
+ export const meta: BlockMeta = {
40
+ title: "Card Row",
41
+ category: "layouts",
42
+ icon: "slides",
43
+ tags: ["#subpage"],
44
+ flags: {
45
+ fullWidth: true,
46
+ marginRule: "featured-card",
47
+ },
48
+ }
49
+
50
+ export default defineBlock("content/card-row", () => {
51
+ return <InnerBlocks allowedBlocks={["content/card-row-item"]} />
52
+ })
53
+ ```
54
+
55
+ ## Wrap Blocks In One Place [#wrap-blocks-in-one-place]
56
+
57
+ Use `wrapBlock` on a shared `ContentBlocks` wrapper so page grid rules live in one component.
58
+
59
+ ```tsx filename="features/layout/SubpageContentBlocks.tsx"
60
+ import { BlocksContext, ContentBlocks } from "eddev/blocks"
61
+ import { ComponentProps, ReactNode } from "react"
62
+
63
+ function wrapBlock(children: ReactNode, ctx: BlocksContext) {
64
+ const marginClass = ctx.current.flags?.marginRule
65
+ ? `block-margins-${ctx.current.flags.marginRule}`
66
+ : ""
67
+
68
+ if (ctx.current.blockName === "core/rich-text" || ctx.current.flags?.contentWidth) {
69
+ return <div className={`col-subpage-content ${marginClass}`}>{children}</div>
70
+ }
71
+
72
+ if (ctx.current.flags?.fullWidth) {
73
+ return <div className={`full-width-block ${marginClass}`}>{children}</div>
74
+ }
75
+
76
+ return <div className={marginClass}>{children}</div>
77
+ }
78
+
79
+ type Props = Omit<ComponentProps<typeof ContentBlocks>, "wrapBlock">
80
+
81
+ export function SubpageContentBlocks(props: Props) {
82
+ return <ContentBlocks {...props} wrapBlock={wrapBlock} />
83
+ }
84
+ ```
85
+
86
+ ## Use The Wrapper In Views [#use-the-wrapper-in-views]
87
+
88
+ ```tsx filename="views/page.tsx"
89
+ import { SubpageContentBlocks } from "@features/layout/SubpageContentBlocks"
90
+ import { defineView } from "eddev/views"
91
+
92
+ export default defineView("page", (props) => {
93
+ return <SubpageContentBlocks blocks={props.page?.contentBlocks} />
94
+ })
95
+ ```
96
+
97
+ This works well when blocks need to stay reusable across different views, but each view has its own layout rules.
@@ -0,0 +1,91 @@
1
+ # Custom Routes And URLs (/docs/snippets/custom-routes-and-urls)
2
+
3
+ **Map fixed URLs to views and keep generated WordPress links aligned**
4
+
5
+ Use these when a route is not a normal WordPress single/archive URL, or when WordPress should generate a custom URL for a post type.
6
+
7
+ For the full routing docs, see [Custom Routes](/docs/routing/custom) and [WordPress Routing](/docs/routing/wordpress).
8
+
9
+ ## Fixed Route With A Query Variable [#fixed-route-with-a-query-variable]
10
+
11
+ ```php filename="backend/routes.php"
12
+ <?php
13
+
14
+ ED()->addCustomRoute('form/([0-9]+)/?$', [
15
+ 'template' => 'views/single-form.tsx',
16
+ 'title' => 'Get in touch',
17
+ 'queryVars' => [
18
+ 'formId' => '$1',
19
+ ],
20
+ ]);
21
+ ```
22
+
23
+ ```graphql filename="views/single-form.graphql"
24
+ query SingleForm($formId: Int!) {
25
+ gravityForm(id: $formId) {
26
+ title
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Route Slugs Back To A Post ID [#route-slugs-back-to-a-post-id]
32
+
33
+ Use `extraVars` when the URL contains slugs but the GraphQL view should receive the resolved WordPress post ID.
34
+
35
+ ```php filename="backend/routes.php"
36
+ <?php
37
+
38
+ ED()->addCustomRoute('program/event/([a-z0-9-]+)/?$', [
39
+ 'template' => 'views/single-film.tsx',
40
+ 'queryVars' => [
41
+ 'filmSlug' => '$1',
42
+ ],
43
+ 'extraVars' => function ($vars) {
44
+ global $post;
45
+ global $wp_query;
46
+
47
+ $film = get_page_by_path($vars['filmSlug'], OBJECT, 'film');
48
+ if (!$film) return [];
49
+
50
+ $wp_query->queried_object_id = $film->ID;
51
+ $wp_query->queried_object = $film;
52
+ $post = $film;
53
+
54
+ return [
55
+ 'postId' => $film->ID,
56
+ ];
57
+ },
58
+ 'is404' => function ($params) {
59
+ return empty($params['postId']);
60
+ },
61
+ ]);
62
+ ```
63
+
64
+ ## Generate Matching WordPress URLs [#generate-matching-wordpress-urls]
65
+
66
+ A custom route handles incoming URLs. It does not automatically change URLs generated by WordPress, WPGraphQL, menus, or blocks. Use `post_type_link` for that.
67
+
68
+ ```php filename="backend/post-types/services.php"
69
+ <?php
70
+
71
+ ED()->registerPostType('service', [
72
+ 'label' => 'Services',
73
+ 'rewrite' => false,
74
+ 'public' => false,
75
+ 'show_ui' => true,
76
+ 'publicly_queryable' => true,
77
+ 'show_in_graphql' => true,
78
+ 'graphql_single_name' => 'service',
79
+ 'graphql_plural_name' => 'services',
80
+ ]);
81
+
82
+ add_filter('post_type_link', function ($url, $post) {
83
+ if ($post->post_type === 'service') {
84
+ return '/services/#' . $post->post_name;
85
+ }
86
+
87
+ return $url;
88
+ }, 10, 2);
89
+ ```
90
+
91
+ For taxonomy URLs, use the matching WordPress filter, such as `term_link`.
@@ -0,0 +1,87 @@
1
+ # Multiple Editable Zones (/docs/snippets/multiple-editable-zones)
2
+
3
+ **Use SlotBlocks when one block needs more than one child area**
4
+
5
+ WordPress only allows one `InnerBlocks` area per block. Use `SlotBlocks` when a single block needs multiple independent editable zones, such as footer columns, mega menu regions, or a header with separate navigation and CTA areas.
6
+
7
+ For normal nested blocks, start with [Nested Blocks](/docs/blocks/nested-blocks). Reach for `SlotBlocks` only when one `InnerBlocks` area cannot model the layout.
8
+
9
+ ## Create Named Slots [#create-named-slots]
10
+
11
+ Each `SlotBlocks` call creates or renders a hidden `core/slot-group` for the given `id`.
12
+
13
+ ```tsx filename="blocks/parts/footer.tsx"
14
+ import { defineBlock, EditableText, SlotBlocks } from "eddev/blocks"
15
+
16
+ export const meta: BlockMeta = {
17
+ title: "Site Footer",
18
+ inserter: false,
19
+ templatePart: {
20
+ area: "footer",
21
+ slug: "siteFooter",
22
+ },
23
+ }
24
+
25
+ export default defineBlock("parts/footer", () => {
26
+ return (
27
+ <footer>
28
+ <section>
29
+ <EditableText
30
+ as="h2"
31
+ store="subscribe-heading"
32
+ defaultValue="Stay in the loop"
33
+ plainText
34
+ />
35
+ <SlotBlocks
36
+ id="subscribe-text"
37
+ title="Subscribe Text"
38
+ allowedBlocks={["core/paragraph"]}
39
+ appender={{ type: "button2" }}
40
+ orientation="vertical"
41
+ />
42
+ </section>
43
+
44
+ <nav>
45
+ <SlotBlocks
46
+ id="footer-primary-links"
47
+ title="Primary Footer Links"
48
+ allowedBlocks={["core/navigation-link"]}
49
+ appender={{ type: "button2" }}
50
+ orientation="vertical"
51
+ />
52
+ </nav>
53
+
54
+ <div>
55
+ <SlotBlocks
56
+ id="footer-legal"
57
+ title="Footer Legal"
58
+ allowedBlocks={["core/navigation-link"]}
59
+ appender={{ type: "button2" }}
60
+ orientation="horizontal"
61
+ />
62
+ </div>
63
+ </footer>
64
+ )
65
+ })
66
+ ```
67
+
68
+ Use stable slot IDs. Changing an `id` later creates a different editable region, so existing content will not appear in the renamed slot.
69
+
70
+ ## Add Editor Layout Classes [#add-editor-layout-classes]
71
+
72
+ Use `adminClassName` when the editor needs layout hints that are different from the frontend wrapper.
73
+
74
+ ```tsx filename="blocks/parts/footer.tsx"
75
+ <SlotBlocks
76
+ id="footer-columns"
77
+ title="Footer Columns"
78
+ allowedBlocks={["parts/footer-group-main"]}
79
+ adminClassName="grid grid-cols-1 md:grid-cols-3 gap-4"
80
+ appender={{ type: "button2" }}
81
+ orientation="vertical"
82
+ />
83
+ ```
84
+
85
+ ## When Not To Use It [#when-not-to-use-it]
86
+
87
+ Do not use slots for ordinary sections, accordions, cards, or button rows. A normal `InnerBlocks` container is simpler, easier to query, and should be the default. See [Nested Blocks](/docs/blocks/nested-blocks) for the simpler pattern.
@@ -0,0 +1,164 @@
1
+ # Querying Specific Blocks (/docs/snippets/querying-specific-blocks)
2
+
3
+ **Select, split, and render only the block content you need**
4
+
5
+ Use these patterns when a view or block needs a targeted block selection instead of every `contentBlocks` item.
6
+
7
+ For the broader data model, see [Block Data](/docs/blocks/data-and-editing), [View Data](/docs/views/queries), and [Template Parts](/docs/blocks/template-parts).
8
+
9
+ ## Pull One Block Type From Another Post [#pull-one-block-type-from-another-post]
10
+
11
+ Use `contentBlocks(include: [...])` when you want a specific block from related posts. Sol People uses this shape for a hero block that pulls only `services/service-card` blocks from Service posts.
12
+
13
+ ```graphql filename="blocks/hero/services-hero.graphql"
14
+ query ServicesHero {
15
+ block {
16
+ hero_services_hero {
17
+ theme
18
+ }
19
+ }
20
+ services(where: { orderby: [{ field: MENU_ORDER, order: ASC }] }) {
21
+ nodes {
22
+ contentBlocks(include: ["services/service-card"], limit: 1)
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ```tsx filename="blocks/hero/services-hero.tsx"
29
+ import { ContentBlocks, defineBlock } from "eddev/blocks"
30
+
31
+ export default defineBlock("hero/services-hero", (props) => {
32
+ const cards =
33
+ props.services?.nodes
34
+ .flatMap((service) => service?.contentBlocks ?? [])
35
+ .filter(Boolean) ?? []
36
+
37
+ return <ContentBlocks blocks={cards} />
38
+ })
39
+ ```
40
+
41
+ `include` accepts block names, tags, flags, and wildcards. Add `limit: 1` when you only expect one matching block per post.
42
+
43
+ ## Exclude A Block From Related Posts [#exclude-a-block-from-related-posts]
44
+
45
+ Use `exclude` when related posts should render their body content but skip the block that is used somewhere else.
46
+
47
+ ```graphql filename="views/archive-services.graphql"
48
+ query ServicesArchive($postId: ID!) {
49
+ page(id: $postId, idType: DATABASE_ID) {
50
+ contentBlocks
51
+ }
52
+ services(where: { orderby: [{ field: MENU_ORDER, order: ASC }] }) {
53
+ nodes {
54
+ contentBlocks(exclude: ["services/service-card"])
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ ```tsx filename="views/archive-services.tsx"
61
+ import { ContentBlocks } from "eddev/blocks"
62
+ import { defineView } from "eddev/views"
63
+
64
+ export default defineView("archive-services", (props) => {
65
+ return (
66
+ <>
67
+ <ContentBlocks blocks={props.page?.contentBlocks} />
68
+ {props.services?.nodes.map((service, index) => (
69
+ <ContentBlocks key={index} blocks={service?.contentBlocks} />
70
+ ))}
71
+ </>
72
+ )
73
+ })
74
+ ```
75
+
76
+ ## Pull One Page Header From A Known Page [#pull-one-page-header-from-a-known-page]
77
+
78
+ When an archive or app-like route has a fixed CMS page backing its intro content, query that page by URI and include just the header block.
79
+
80
+ ```graphql filename="views/archive-blog.graphql"
81
+ query ArchiveBlog {
82
+ blogPosts(first: 9999, where: { orderby: [{ field: DATE, order: DESC }] }) {
83
+ nodes {
84
+ title
85
+ uri
86
+ }
87
+ }
88
+ page(id: "blog", idType: URI) {
89
+ title
90
+ contentBlocks(include: ["page/page-header"], limit: 1)
91
+ }
92
+ }
93
+ ```
94
+
95
+ ```tsx filename="views/archive-blog.tsx"
96
+ import { ContentBlocks } from "eddev/blocks"
97
+ import { defineView } from "eddev/views"
98
+
99
+ export default defineView("archive-blog", (props) => {
100
+ return (
101
+ <>
102
+ <ContentBlocks blocks={props.page?.contentBlocks} />
103
+ {/* Render the listing UI below the page header. */}
104
+ </>
105
+ )
106
+ })
107
+ ```
108
+
109
+ ## Split A Hero From The Rest [#split-a-hero-from-the-rest]
110
+
111
+ If the current page stores the hero in normal `contentBlocks`, find it in React and render the remainder separately.
112
+
113
+ ```tsx filename="views/template-home.tsx"
114
+ import { ContentBlocks, SingleContentBlock } from "eddev/blocks"
115
+ import { defineView } from "eddev/views"
116
+
117
+ export default defineView("template-home", (props) => {
118
+ const blocks = props.page?.contentBlocks ?? []
119
+ const hero = blocks.find((block) => block.slug === "homepage/homepage-carousel")
120
+ const rest = blocks.filter((block) => block.slug !== "homepage/homepage-carousel")
121
+
122
+ return (
123
+ <>
124
+ {hero && <SingleContentBlock block={hero} />}
125
+ <ContentBlocks blocks={rest} />
126
+ </>
127
+ )
128
+ })
129
+ ```
130
+
131
+ Use this when the split is mostly presentational. If the split affects payload size or repeated related posts, prefer `include` and `exclude` in GraphQL.
132
+
133
+ ## Query A Template Part Subset [#query-a-template-part-subset]
134
+
135
+ Use aliases plus `contentBlocks` filters when `_app.graphql` needs both a whole template part and a small subset.
136
+
137
+ ```graphql filename="views/_app.graphql"
138
+ query CommonData {
139
+ templateParts {
140
+ siteHeader {
141
+ contentBlocks
142
+ }
143
+ headerButtons: siteHeader {
144
+ contentBlocks(flattenExcluded: true, include: ["parts/header-button"])
145
+ }
146
+ siteFooter {
147
+ contentBlocks
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ ```tsx filename="features/header/MobileMenu.tsx"
154
+ import { ContentBlocks } from "eddev/blocks"
155
+ import { useAppData } from "eddev/hooks"
156
+
157
+ export function MobileMenuButtons() {
158
+ const blocks = useAppData((data) => data.templateParts?.headerButtons?.contentBlocks)
159
+
160
+ return <ContentBlocks blocks={blocks} />
161
+ }
162
+ ```
163
+
164
+ `flattenExcluded: true` is useful when the matching blocks may sit inside layout wrappers that you do not want to render.