eddev 2.3.11-beta.8 → 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 (157) hide show
  1. package/dist/app/entry/MetaTags.d.ts +1 -1
  2. package/dist/app/entry/MetaTags.d.ts.map +1 -1
  3. package/dist/app/entry/MetaTags.js +2 -0
  4. package/dist/app/entry/hydration-script.js +1 -1
  5. package/dist/app/lib/blocks/BlockPropMutator.d.ts +1 -1
  6. package/dist/app/lib/blocks/BlockPropMutator.d.ts.map +1 -1
  7. package/dist/app/lib/blocks/SlotBlocks.d.ts +1 -1
  8. package/dist/app/lib/blocks/SlotBlocks.d.ts.map +1 -1
  9. package/dist/app/lib/blocks/SlotBlocks.js +2 -2
  10. package/dist/app/lib/blocks/block-debugger.d.ts +3 -3
  11. package/dist/app/lib/blocks/block-debugger.d.ts.map +1 -1
  12. package/dist/app/lib/blocks/block-debugger.js +1 -1
  13. package/dist/app/lib/blocks/block-utils.d.ts +2 -2
  14. package/dist/app/lib/blocks/block-utils.d.ts.map +1 -1
  15. package/dist/app/lib/blocks/block-utils.js +10 -3
  16. package/dist/app/lib/blocks/builtin-blocks.js +1 -1
  17. package/dist/app/lib/blocks/defineBlock.js +2 -2
  18. package/dist/app/lib/blocks/editor/EditorHighlights.js +1 -1
  19. package/dist/app/lib/blocks/editor/create-block.d.ts +1 -1
  20. package/dist/app/lib/blocks/editor/create-block.d.ts.map +1 -1
  21. package/dist/app/lib/blocks/editor/root-blocks.d.ts.map +1 -1
  22. package/dist/app/lib/blocks/editor/root-blocks.js +4 -4
  23. package/dist/app/lib/devtools/components/DevMenu.js +4 -4
  24. package/dist/app/lib/devtools/components/DevUI.js +5 -5
  25. package/dist/app/lib/devtools/components/panels/AppDataPanel.js +4 -4
  26. package/dist/app/lib/devtools/components/panels/QueriesPanel.js +1 -1
  27. package/dist/app/lib/devtools/components/panels/RoutePanel.d.ts +1 -1
  28. package/dist/app/lib/devtools/components/panels/RoutePanel.d.ts.map +1 -1
  29. package/dist/app/lib/devtools/components/panels/RoutePanel.js +8 -8
  30. package/dist/app/lib/devtools/components/panels/StructurePanel.js +8 -8
  31. package/dist/app/lib/devtools/components/panels/index.js +5 -5
  32. package/dist/app/lib/devtools/components/ui/JSONInspector.js +2 -2
  33. package/dist/app/lib/devtools/components/ui/Panel.js +1 -1
  34. package/dist/app/lib/devtools/components/ui/QueryMonitorDisplay.d.ts +1 -1
  35. package/dist/app/lib/devtools/components/ui/QueryMonitorDisplay.d.ts.map +1 -1
  36. package/dist/app/lib/devtools/components/ui/QueryMonitorDisplay.js +3 -3
  37. package/dist/app/lib/devtools/components/ui/TabBar.js +2 -2
  38. package/dist/app/lib/devtools/components/ui/Tabs.js +2 -2
  39. package/dist/app/lib/devtools/hooks/useTailwind.d.ts +5 -5
  40. package/dist/app/lib/dynamic/dynamic.js +1 -1
  41. package/dist/app/lib/hooks/query-hooks.d.ts +9 -9
  42. package/dist/app/lib/hooks/query-hooks.d.ts.map +1 -1
  43. package/dist/app/lib/hooks/query-hooks.js +2 -1
  44. package/dist/app/lib/integrations/gravityforms/createGravityFormComponent.d.ts +3 -3
  45. package/dist/app/lib/integrations/gravityforms/createGravityFormComponent.d.ts.map +1 -1
  46. package/dist/app/lib/integrations/gravityforms/createGravityFormComponent.js +1 -1
  47. package/dist/app/lib/integrations/gravityforms/field-types.d.ts +1 -1
  48. package/dist/app/lib/integrations/gravityforms/field-types.d.ts.map +1 -1
  49. package/dist/app/lib/legacy-stitches/createStitches.d.ts +1 -509
  50. package/dist/app/lib/legacy-stitches/createStitches.d.ts.map +1 -1
  51. package/dist/app/lib/routing/hooks/useRouteMeta.d.ts +1 -1
  52. package/dist/app/lib/routing/hooks/useRouteMeta.d.ts.map +1 -1
  53. package/dist/app/lib/runtime/apiConfig.d.ts +15 -0
  54. package/dist/app/lib/runtime/apiConfig.d.ts.map +1 -1
  55. package/dist/app/lib/runtime/errorHandling.d.ts +2 -2
  56. package/dist/app/lib/runtime/errorHandling.d.ts.map +1 -1
  57. package/dist/app/lib/runtime/index.d.ts +2 -2
  58. package/dist/app/lib/runtime/index.d.ts.map +1 -1
  59. package/dist/app/lib/runtime/index.js +2 -2
  60. package/dist/app/server/proxy-wp-admin.d.ts.map +1 -1
  61. package/dist/app/server/proxy-wp-admin.js +1 -0
  62. package/dist/app/server/rpc.d.ts +1 -1
  63. package/dist/app/server/rpc.d.ts.map +1 -1
  64. package/dist/app/server/server-context.d.ts +3 -3
  65. package/dist/app/server/server-context.d.ts.map +1 -1
  66. package/dist/app/server/server-context.js +5 -5
  67. package/dist/app/server/utils/content-security.d.ts +2 -2
  68. package/dist/app/server/utils/content-security.d.ts.map +1 -1
  69. package/dist/app/utils/trpc-client.d.ts.map +1 -1
  70. package/dist/app/utils/trpc-client.js +19 -3
  71. package/dist/app/utils/wp.d.ts +10 -10
  72. package/dist/app/utils/wp.d.ts.map +1 -1
  73. package/dist/node/cli/cli.js +3 -1
  74. package/dist/node/cli/version.d.ts +1 -1
  75. package/dist/node/cli/version.d.ts.map +1 -1
  76. package/dist/node/cli/version.js +1 -1
  77. package/dist/node/compiler/dev-server.js +1 -1
  78. package/dist/node/compiler/get-vite-config.d.ts.map +1 -1
  79. package/dist/node/compiler/get-vite-config.js +2 -0
  80. package/dist/node/compiler/vinxi-app.d.ts +1 -1
  81. package/dist/node/compiler/vinxi-app.d.ts.map +1 -1
  82. package/dist/node/compiler/vinxi-codegen.d.ts.map +1 -1
  83. package/dist/node/compiler/vinxi-codegen.js +2 -3
  84. package/dist/node/project/favicons.d.ts +1 -1
  85. package/dist/node/project/favicons.d.ts.map +1 -1
  86. package/dist/node/storybook/index.js +1 -1
  87. package/dist/node/utils/fetch-wp.d.ts.map +1 -1
  88. package/dist/node/utils/fetch-wp.js +2 -1
  89. package/dist/node/utils/fs.d.ts +22 -19
  90. package/dist/node/utils/fs.d.ts.map +1 -1
  91. package/package.json +2 -2
  92. package/skills/eddev/SKILL.md +156 -0
  93. package/skills/eddev/docs/acf/admin-panel-widgets.mdx +99 -0
  94. package/skills/eddev/docs/acf/custom-enums.mdx +75 -0
  95. package/skills/eddev/docs/acf/custom-fields.mdx +131 -0
  96. package/skills/eddev/docs/acf.mdx +31 -0
  97. package/skills/eddev/docs/blocks/block-definition.mdx +189 -0
  98. package/skills/eddev/docs/blocks/core-blocks.mdx +86 -0
  99. package/skills/eddev/docs/blocks/data-and-editing.mdx +219 -0
  100. package/skills/eddev/docs/blocks/editor-config.mdx +157 -0
  101. package/skills/eddev/docs/blocks/nested-blocks.mdx +129 -0
  102. package/skills/eddev/docs/blocks/overview.mdx +58 -0
  103. package/skills/eddev/docs/blocks/template-parts.mdx +131 -0
  104. package/skills/eddev/docs/config.mdx +200 -0
  105. package/skills/eddev/docs/design/color.mdx +185 -0
  106. package/skills/eddev/docs/design/favicons.mdx +103 -0
  107. package/skills/eddev/docs/design/grid.mdx +120 -0
  108. package/skills/eddev/docs/design/icons.mdx +197 -0
  109. package/skills/eddev/docs/design/responsive-scaling.mdx +312 -0
  110. package/skills/eddev/docs/design/type.mdx +125 -0
  111. package/skills/eddev/docs/devtool/cli.mdx +201 -0
  112. package/skills/eddev/docs/devtool/overlay.mdx +5 -0
  113. package/skills/eddev/docs/getting-started.mdx +53 -0
  114. package/skills/eddev/docs/graphql/extending.mdx +186 -0
  115. package/skills/eddev/docs/graphql/fragments.mdx +107 -0
  116. package/skills/eddev/docs/graphql/global-data.mdx +47 -0
  117. package/skills/eddev/docs/graphql/infinite-queries.mdx +95 -0
  118. package/skills/eddev/docs/graphql/mutation-hooks.mdx +111 -0
  119. package/skills/eddev/docs/graphql/query-hooks.mdx +122 -0
  120. package/skills/eddev/docs/graphql/tooling.mdx +50 -0
  121. package/skills/eddev/docs/graphql.mdx +97 -0
  122. package/skills/eddev/docs/guides/color-schemes.mdx +204 -0
  123. package/skills/eddev/docs/guides/integrations.mdx +3 -0
  124. package/skills/eddev/docs/guides/page-transitions.mdx +5 -0
  125. package/skills/eddev/docs/guides/seo.mdx +5 -0
  126. package/skills/eddev/docs/guides/state-management.mdx +5 -0
  127. package/skills/eddev/docs/infra/caching.mdx +9 -0
  128. package/skills/eddev/docs/infra/deployment.mdx +13 -0
  129. package/skills/eddev/docs/infra/local.mdx +5 -0
  130. package/skills/eddev/docs/infra/security.mdx +11 -0
  131. package/skills/eddev/docs/routing/api.mdx +731 -0
  132. package/skills/eddev/docs/routing/custom.mdx +123 -0
  133. package/skills/eddev/docs/routing/full-details.mdx +37 -0
  134. package/skills/eddev/docs/routing/wordpress.mdx +70 -0
  135. package/skills/eddev/docs/routing.mdx +18 -0
  136. package/skills/eddev/docs/serverless/functions.mdx +436 -0
  137. package/skills/eddev/docs/serverless.mdx +202 -0
  138. package/skills/eddev/docs/snippets/automated-block-layouts.mdx +97 -0
  139. package/skills/eddev/docs/snippets/custom-routes-and-urls.mdx +91 -0
  140. package/skills/eddev/docs/snippets/multiple-editable-zones.mdx +87 -0
  141. package/skills/eddev/docs/snippets/querying-specific-blocks.mdx +164 -0
  142. package/skills/eddev/docs/snippets/submitting-forms-to-rpc.mdx +91 -0
  143. package/skills/eddev/docs/snippets/svgs.mdx +38 -0
  144. package/skills/eddev/docs/snippets/type-safe-acf-dropdowns.mdx +72 -0
  145. package/skills/eddev/docs/snippets.mdx +19 -0
  146. package/skills/eddev/docs/software.mdx +19 -0
  147. package/skills/eddev/docs/stack/how-it-works.mdx +50 -0
  148. package/skills/eddev/docs/stack/overview.mdx +56 -0
  149. package/skills/eddev/docs/stack/spa-vs-ssr.mdx +52 -0
  150. package/skills/eddev/docs/views/app-view.mdx +97 -0
  151. package/skills/eddev/docs/views/page-templates.mdx +82 -0
  152. package/skills/eddev/docs/views/queries.mdx +116 -0
  153. package/skills/eddev/docs/views.mdx +63 -0
  154. package/skills/eddev/index.mdx +79 -0
  155. package/tsconfig.app.json +2 -2
  156. package/tsconfig.node.json +6 -3
  157. package/types.env.d.ts +1 -0
@@ -0,0 +1,123 @@
1
+ # Custom Routes (/docs/routing/custom)
2
+
3
+ **Register fixed WordPress-backed routes for app-like screens.**
4
+
5
+ In cases where we need a custom route, which does not necessarily map to a content type, you can use the `ED()->addCustomRoute()` function.
6
+
7
+ This is mostly useful for sites which include app-like functionality — like login or checkout screens — where the URL should be fixed, and it shouldn't be removable by a client. It can also be useful for providing URLs to content which isn't a WordPress post or page, or for advanced customisation of routing.
8
+
9
+ The `ED()->addCustomRoute()` function takes two arguments:
10
+
11
+ 1. The route pattern (a string), which is a simple PHP regular expression. Parenthesised groups can be extracted using dollar sign formatting.
12
+ 2. Options for the custom route:
13
+ * `template` (string, required) — pointing to the `.tsx` template file to be used, starting with `views/`
14
+ * `queryVars` (array, optional) — allows you to map a URL segment to a GraphQL parameter.
15
+ * `extraVars` (function) — accepts a PHP function which takes the `queryVars` from the previous step, and can return additional GraphQL parameters. This can be useful if you need to search the database for an ID.
16
+ * `title` (string or function, required) — the page title to use when viewing this route. If you pass a function, you can use the first parameter to read the query var values — allowing you to look up a title if necessary.
17
+ * `is404` (function, optional) - An optional callback function, which will be passed the query parameters, and should return a truthy value if the page should be a 404.
18
+
19
+ Example:
20
+
21
+ ```php filename="/backend/routes.php"
22
+ <?php
23
+
24
+ // Adding a simple '/search' route, with an optional trailing slash.
25
+ ED()->addCustomRoute("search/?$", [
26
+ 'template' => 'views/search.tsx',
27
+ 'title' => function() {
28
+ return 'Search';
29
+ }
30
+ ]);
31
+
32
+ // Adding a custom route which uses a Regex pattern
33
+ // The pattern is made available to a paired GraphQL file via a 'listId' variable.
34
+ ED()->addCustomRoute("wishlist/([A-Za-z0-9\-\_]+)/?$", [
35
+ 'template' => 'views/user-wishlist.tsx',
36
+ 'queryVars' => [
37
+ 'listId' => '$1'
38
+ ],
39
+ 'title' => function($params) {
40
+ $wishlist = Wishlists::getWishlist($params['listId']);
41
+ return $wishlist->title;
42
+ },
43
+ 'is404' => function($params) {
44
+ return Wishlists::wishlistExists($params['listId']) == false;
45
+ }
46
+ ]);
47
+ ```
48
+
49
+ Inside `views/user-wishlist.graphql`, the `$listId` variable will be populated with the regular expression match from the custom route.
50
+
51
+ ```graphql filename="/views/user-wishlist.graphql"
52
+ query UserWishlist($listId: String!) {
53
+ wishlist(id: $listId) {
54
+ title
55
+ items {
56
+ title
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Generating WordPress URLs For Custom Routes [#generating-wordpress-urls-for-custom-routes]
63
+
64
+ A custom route only tells `eddev` how to handle an incoming URL. If WordPress also needs to generate that URL from `get_permalink()`, GraphQL `uri` fields, menus, or block data, remap the WordPress permalink separately.
65
+
66
+ For posts and custom post types, use the [`post_type_link`](https://developer.wordpress.org/reference/hooks/post_type_link/) filter. SFF uses this for Film posts: each Film still lives in WordPress as a `film` post, but generated links are rewritten based on the festival/program that owns the Film. The matching custom routes then resolve those paths back to `views/single-film.tsx`.
67
+
68
+ ```php filename="/backend/routes.php"
69
+ add_filter('post_type_link', function ($link, $post) {
70
+ if ($post->post_type === 'film') {
71
+ return Programs::getPrefixForProgram($post->post_parent) . $post->post_name . "/";
72
+ }
73
+
74
+ return $link;
75
+ }, 10, 2);
76
+ ```
77
+
78
+ For taxonomy or category URLs, use the matching WordPress term link filter, such as `term_link` or `category_link`, with the same idea: generate the public URL you want, then add a custom route that knows how to resolve that URL into view data.
79
+
80
+ ## Extra Vars [#extra-vars]
81
+
82
+ You can use the `extraVars` option if you need to calculate some additional GraphQL parameters based on the URL.
83
+
84
+ Say you want to use `/films/{festival}/{film}/` as the pathname for Films, where film slugs are only unique to that festival. The best way to accomplish this would be to:
85
+
86
+ * Disable the default item pages, using [`"rewrite" => false`](./wordpress#post-type-routing) on the Films post type
87
+ * Use the `post_type_link` filter to ensure that Film URLs are generated correctly.
88
+ * Add a custom route for the pattern, which handles 404s correctly.
89
+
90
+ Your custom route might look something like this:
91
+
92
+ ```php
93
+ ED()->addCustomRoute('films/([a-z0-9-]+)/([a-z0-9-]+)/?$', [
94
+ // Use the 'single-film' template by convention, even though it could be anything
95
+ 'template' => 'views/single-film.tsx',
96
+ // Map the URL params -> query vars
97
+ 'queryVars' => [
98
+ 'festivalSlug' => "$1",
99
+ 'filmSlug' => "$2",
100
+ ],
101
+ 'extraVars' => function ($vars) {
102
+ global $wp_query;
103
+ $post = get_post([/*...*/]);
104
+ if (!$post) return [];
105
+
106
+ // Optionally set the global queried_object_id, so that WordPress will handle the page title/SEO metadata as normal
107
+ $wp_query->queried_object_id = $post->ID;
108
+ $wp_query->queried_object = $post;
109
+
110
+ // Return the `postId` param
111
+ return [
112
+ 'postId' => $post->ID
113
+ ];
114
+ },
115
+ 'is404' => function ($vars) {
116
+ return empty($vars['postId']);
117
+ }
118
+ ]);
119
+ ```
120
+
121
+ <Callout>
122
+ TODO: OpenGraph Metadata
123
+ </Callout>
@@ -0,0 +1,37 @@
1
+ # How it Works (/docs/routing/full-details)
2
+
3
+ **Trace how eddev loads route data for SPA and serverless navigation.**
4
+
5
+ ## Route Data Handling [#route-data-handling]
6
+
7
+ When navigating to a new page, the frontend router makes a request to WordPress to fetch the JSON data needed by the current route. The request is handled by the `RouteLoader` (accessible via `useRouter().loader`).
8
+
9
+ The JSON data is available via `/{path}?_props=1` in SPA mode, and `/_data/route/{path}` in serverless mode.
10
+
11
+ | Mode | URL | JSON Page |
12
+ | ---------- | ------------------ | ------------------------------- |
13
+ | Serverless | `/work/my-project` | `/_data/route/work/my-project/` |
14
+ | SPA | `/work/my-project` | `/work/my-project/?_props=1` |
15
+
16
+ The *serverless* mode `/_data/route/{path}` version proxies the `/{path}/?_props=1` version from the WordPress origin, but allows for additional processing, caching and (potential future) static site generation.
17
+
18
+ On the WordPress side, `eddev-php` takes over, recognising that `?_props=1` is present in the URL. WordPress resolves the route that should be displayed using the [WordPress Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/) and any [Custom Routes](./custom), and identifies the `view`, or page template, that should be rendered.
19
+
20
+ Once the page template has been found, `eddev-php` then looks for a paired `.graphql` file with the same name. So if `views/single-project.tsx` is resolved as the template, then it'll search for a `views/single-project.graphql` file.
21
+
22
+ That GraphQL file is then executed, and the result is captured.
23
+
24
+ During this process, one of the following will happen:
25
+
26
+ * The route resolves to a 404 page, so the 404 view or `_error` view receives error details.
27
+ * The GraphQL query throws an error, so the view is set to `_error` and `props` is set to the error details.
28
+ * The GraphQL succeeds, and the process continues.
29
+ * No GraphQL file was present, so props is set to an empty object.
30
+
31
+ After this, `wp_head()`, `wp_footer()` are called, allowing WordPress (and plugins) to spit out meta tags and any other tags that should be present on the page. This HTML output is parsed, and included in the final payload.
32
+
33
+ The request is then resolved with the final JSON payload.
34
+
35
+ <Callout>
36
+ Most of this process also occurs when the page is loaded directly, but the JSON payload is instead printed in a `<script>window._PAGE_DATA = {...}</script>`, with app data also being included.
37
+ </Callout>
@@ -0,0 +1,70 @@
1
+ # WordPress Routing (/docs/routing/wordpress)
2
+
3
+ **Let WordPress resolve URLs while React renders the matched view.**
4
+
5
+ Rather than using file-based routing, we allow WordPress to serve as the 'authority' on what a given URL should display.
6
+
7
+ This results in a tighter coupling between the CMS and the frontend — with a single source of truth for getting/resolving URLs, with very minimal configuration — and allows us to serve the site as both an SPA on the WordPress server (no Node.js needed), as well as a standalone SSR frontend (deployed to Vercel).
8
+
9
+ The stack essentially allows the [Wordpress Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/) to unfold as normal, but it allows `.tsx` files to be used, instead of `.php` files. So, when WordPress attempts to look for `single-event.php`, it'll instead look for `views/single-event.tsx`.
10
+
11
+ ## Custom Routes [#custom-routes]
12
+
13
+ Custom routes can be registered using the `ED()->addCustomRoute()` method in PHP. [Read More](./custom)
14
+
15
+ ## Post Type Routing [#post-type-routing]
16
+
17
+ The code below would provide:
18
+
19
+ * a listing page at `/work`, which would use `views/archive-project.tsx` as the template.
20
+ * a page for each project at `/work/{slug}`, which would use `views/single-project.tsx` as the template.
21
+
22
+ ```php
23
+ ED()->registerPostType("projects", [
24
+ /* ... */
25
+ "has_archive" => true,
26
+ "rewrite" => [
27
+ "slug" => "work",
28
+ "with_front" => false
29
+ ]
30
+ ])
31
+ ```
32
+
33
+ <Callout type="info">
34
+ To disable the default listing page, just pass `"has_archive" => false`<br />
35
+ To disable the default post page, just pass `"rewrite" => false`
36
+ </Callout>
37
+
38
+ ## Posts URLs without pages [#posts-urls-without-pages]
39
+
40
+ Sometimes you might want to redirect a post type to another page. You can use the [post\_type\_link](https://developer.wordpress.org/reference/hooks/post_type_link/) filter to do this.
41
+
42
+ ```php
43
+ ED()->registerPostType("projects", [
44
+ /* ... */
45
+ // Disable the default route, to remove it from the router
46
+ "rewrite" => false
47
+ ])
48
+
49
+ // Return a custom URL for this post
50
+ add_filter('post_type_link', function ($url, $post) {
51
+ if ($post->post_type === 'job') {
52
+ return "/jobs?job=".$post->post_name;
53
+ }
54
+ return $url;
55
+ }, 10, 2);
56
+ ```
57
+
58
+ <Callout type="warning">
59
+ Note that this doesn't affect how routes are resolved — just how links are generated.
60
+ </Callout>
61
+
62
+ <Callout type="info">
63
+ The same functionality can be applied to Taxonomy terms too, via [term\_link](https://developer.wordpress.org/reference/hooks/term_link/).
64
+ </Callout>
65
+
66
+ ## Rule Flushing Hack [#rule-flushing-hack]
67
+
68
+ WordPress caches the routing rules for performance reasons, which can sometimes result in routing updates not applying.
69
+
70
+ The stack tracks custom route and post-type declarations, and automatically calls [flush\_rewrite\_rules](https://developer.wordpress.org/reference/functions/flush_rewrite_rules/) when changes are detected. If you're doing something more advanced with WordPress routing, you may need to flush the router manually. Just navigate to *Settings -> Permalinks* and simply hit *Save Changes*.
@@ -0,0 +1,18 @@
1
+ # Routing Overview (/docs/routing)
2
+
3
+ **Understand the WordPress and frontend sides of eddev routing.**
4
+
5
+ There are two sides to routing in our stack:
6
+
7
+ 1. **WordPress/PHP** — We allow WordPress to act as the authority on routing, and the stack provides extra functions for extending this.
8
+ 2. **Frontend API** — The `eddev` library provides JavaScript APIs for handling client-side navigation, and for preloading data and components.
9
+
10
+ <Cards>
11
+ <Card title="WordPress Routing" href="/routing/wordpress">
12
+ Let WordPress resolve URLs while React renders the matched template.
13
+ </Card>
14
+
15
+ <Card title="Frontend APIs" href="/routing/api">
16
+ Use eddev link and router APIs for client-side navigation.
17
+ </Card>
18
+ </Cards>
@@ -0,0 +1,436 @@
1
+ # RPC Functions (/docs/serverless/functions)
2
+
3
+ **Define TypeScript RPC routes for serverless eddev projects.**
4
+
5
+ Serverless functions are TypeScript HTTP routes that allow you to build REST endpoints.
6
+
7
+ To get started:
8
+
9
+ * You'll need to create a `server/routes/` folder
10
+ * You'll also need to install [`zod`](https://zod.dev/) via `yarn add zod`. Zod is used for validation of RPC inputs.
11
+
12
+ The `server/routes` folder contains a set of `.ts` files, and uses file-based routing. You can also create an optional `server/_context.ts` file, which allows you to parse HTTP headers for things like auth, and pass additional information to your API handlers.
13
+
14
+ Each file must have a `default` export, and export either a [query](#queries), [mutation](#mutations), or a [router](#routers).
15
+
16
+ You can also add additional helper code anywhere in the `server` folder, and it'll be ignored — as long as it's not in the `server/routes` folder. Of course, you can also import code from elsewhere in your project.
17
+
18
+ An example folder structure:
19
+
20
+ <Files>
21
+ <Folder name="server/">
22
+ <File name="_context.ts" />
23
+
24
+ <Folder name="routes/">
25
+ <File name="api.pokemon.ts" />
26
+
27
+ <File name="api.echo.ts" />
28
+
29
+ <Folder name="api/">
30
+ <Folder name="users/">
31
+ <File name="auth.ts" />
32
+
33
+ <File name="prefs.ts" />
34
+ </Folder>
35
+ </Folder>
36
+ </Folder>
37
+
38
+ <Folder name="lib/">
39
+ <File name="whatever.ts" />
40
+ </Folder>
41
+ </Folder>
42
+ </Files>
43
+
44
+ <Callout type="info">
45
+ Under the hood, the stack uses [tRPC](https://trpc.io) to define server-side procedures and routers, with custom eddev integration. You should familiarize yourself with the core concepts of procedures, routing and context in tRPC, but examples are shown on this page. You don't need to install tRPC, as it's included in the stack.
46
+ </Callout>
47
+
48
+ <Callout type="warning">
49
+ RPC functions require a Vercel deployment. The [`useRPCQuery()`](#userpcquery) and [`useRPCMutation()`](#userpcmutation) hooks will work correctly in WordPress admin and the SPA frontend, as long as the `serverless.endpoints` object in `ed.config.json` is configured correctly — it must contain a mapping of the WordPress origin to the serverless endpoint, for example `"cms.ed.studio": "ed.studio"`.
50
+ </Callout>
51
+
52
+ ## Defining Routes [#defining-routes]
53
+
54
+ Routes are defined using file-based routing. You can use a combination of folders and filenames with `.` so `api/hello.ts` and `api.hello.ts` do the same thing! It's recommended that you use `api.hello.ts` so that your filenames are easier to search for and recognise in your code editor.
55
+
56
+ Below are some examples of how the file-based routing is resolved:
57
+
58
+ <table>
59
+ <thead>
60
+ <tr>
61
+ <th>
62
+ File
63
+ </th>
64
+
65
+ <th>
66
+ URL Path
67
+ </th>
68
+
69
+ <th>
70
+ Notes
71
+ </th>
72
+ </tr>
73
+ </thead>
74
+
75
+ <tbody>
76
+ <tr>
77
+ <td>
78
+ `/routes/demo1.ts`
79
+ </td>
80
+
81
+ <td>
82
+ `/demo1`
83
+ </td>
84
+
85
+ <td>
86
+ Straight-up file -> URL
87
+ </td>
88
+ </tr>
89
+
90
+ <tr>
91
+ <td>
92
+ `/routes/api/tickets.ts`
93
+ </td>
94
+
95
+ <td>
96
+ `/api/tickets/`
97
+ </td>
98
+
99
+ <td>
100
+ Using folders
101
+ </td>
102
+ </tr>
103
+
104
+ <tr>
105
+ <td>
106
+ `/routes/api.tickets.ts`
107
+ </td>
108
+
109
+ <td>
110
+ `/api/tickets/`
111
+ </td>
112
+
113
+ <td>
114
+ Using filenames only with `.` instead of folders
115
+ </td>
116
+ </tr>
117
+
118
+ <tr>
119
+ <td>
120
+ `/routes/api/hubspot.forms.ts`
121
+ </td>
122
+
123
+ <td>
124
+ `/api/hubspot/forms/`
125
+ </td>
126
+
127
+ <td>
128
+ Using a combination of folders + `.` syntax
129
+ </td>
130
+ </tr>
131
+ </tbody>
132
+ </table>
133
+
134
+ ## Procedures [#procedures]
135
+
136
+ "Procedures" are either "queries" or "mutations". Queries are GET requests, and mutations are POST requests. Both have a nearly identical syntax, but are called in different ways.
137
+
138
+ Multiple procedures can be contained within a single file by exporting a ["router"](#routers).
139
+
140
+ ### Queries [#queries]
141
+
142
+ Query procedures use the `GET` HTTP method.
143
+
144
+ ```typescript filename="/server/routes/api.getPokemon.ts"
145
+ import { rpc } from "eddev/server"
146
+ import { z } from "zod"
147
+
148
+ export default rpc.procedure
149
+ .input(
150
+ z.object({
151
+ name: z.string().min(2),
152
+ }),
153
+ )
154
+ .query(async ({ ctx, input }) => {
155
+ const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${input.name}`)
156
+ const data = await response.json()
157
+
158
+ return {
159
+ pokemonInfo: data,
160
+ }
161
+ })
162
+ ```
163
+
164
+ #### `useRPCQuery()` [#userpcquery]
165
+
166
+ In React, you can then call `useRPCQuery` with the name `"api.getPokemon"` and type-safe arguments, and observe the result.
167
+
168
+ ```tsx filename="PokemonInfo.tsx"
169
+ import { useRPCQuery } from "eddev/hooks"
170
+
171
+ type Props = {
172
+ name: string
173
+ }
174
+
175
+ export function Pokemon(props: Props) {
176
+ const pokemon = useRPCQuery("api.getPokemon", { name: props.name })
177
+
178
+ if (pokemon.isSuccess) {
179
+ return (
180
+ <div>
181
+ Pokemon data: <pre>{JSON.stringify(pokemon.data.pokemonInfo, null, 2)}</pre>
182
+ </div>
183
+ )
184
+ } else if (pokemon.isLoading) {
185
+ return <div>Loading...</div>
186
+ } else if (pokemon.isError) {
187
+ return <div>Error: {pokemon.error.message}</div>
188
+ }
189
+ }
190
+ ```
191
+
192
+ <Callout>
193
+ The return result of `useRPCQuery` is the same as our generated GraphQL hooks. It also accepts a third argument, the query options, where you can pass options like `refetchInterval` to have the hook auto-refresh. See [TanStack docs for useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)
194
+ </Callout>
195
+
196
+ #### `rpcClient.query()` [#rpcclientquery]
197
+
198
+ You can also call queries procedurally using `rpcClient.query()`, like so:
199
+
200
+ ```typescript
201
+ const bulbasaur = await rpcClient.query("api.getPokemon", { name: "bulbasaur" })
202
+ console.log("Bulbasaur is", bulbasaur)
203
+ ```
204
+
205
+ ### Mutations [#mutations]
206
+
207
+ Use `mutation` procedures for actions that should be triggered when a user completes an action, like submitting a form.
208
+
209
+ Mutations will be called from React with `const mutation = useRPCMutation("api.submit")` and `mutation.mutate(args)`
210
+
211
+ ```typescript filename="/server/routes/api.subscribe.ts"
212
+ import { rpc } from "eddev/server"
213
+ import { z } from "zod"
214
+
215
+ export default rpc.procedure
216
+ .input(
217
+ z.object({
218
+ email: z.string(),
219
+ listId: z.string()
220
+ }),
221
+ )
222
+ .mutation(async ({ input }) => {
223
+ const params = new URLSearchParams()
224
+ params.append("EMAIL", input.email)
225
+ params.append("id", input.listId)
226
+ const response = await fetch(`${env.CAMPAIGN_MONITOR_URL}&${params}`, {
227
+ method: "GET",
228
+ })
229
+ if (response.ok) {
230
+ return { success: true }
231
+ } else {
232
+ return { success: false }
233
+ }
234
+ })
235
+ ```
236
+
237
+ #### `useRPCMutation()` [#userpcmutation]
238
+
239
+ In React, you can then call `useRPCMutation` with the name `"api.subscribe"` and type-safe arguments, and observe the result.
240
+
241
+ ```tsx filename="SignupForm.tsx"
242
+ import { useRPCMutation } from "eddev/hooks"
243
+ import { useState } from "react"
244
+
245
+ type Props = {
246
+ listId: string
247
+ }
248
+
249
+ export function SignupForm(props: Props) {
250
+ const [email, setEmail] = useState("")
251
+ const subscribe = useRPCMutation("api.subscribe")
252
+
253
+ if (subscribe.isSuccess) {
254
+ console.log("Result is", subscribe.data)
255
+ return <div>Thank you for signing up!</div>
256
+ }
257
+
258
+ return <form onSubmit={e => {
259
+ e.preventDefault()
260
+ subscribe.mutate({ email: email, listId: props.listId })
261
+ }}>
262
+ {/* Error message */}
263
+ {subscribe.isError && <p>{subscribe.error.message}</p>}
264
+ {/* Disable input when submitting */}
265
+ <input type="email" disabled={subscribe.isPending} value={email} onChange={e => setEmail(e.currentTarget.value)} />
266
+ <button type="submit">Submit</button>
267
+ </form>
268
+ }
269
+ ```
270
+
271
+ <Callout>
272
+ The return result of `useRPCMutation` is the same as our generated GraphQL hooks. It also accepts a second argument, the mutation options, where you can pass options like `onSuccess` to trigger functions to occur after successful submission. See [TanStack docs for useMutation](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation)
273
+ </Callout>
274
+
275
+ #### `rpcClient.mutation()` [#rpcclientmutation]
276
+
277
+ You can also call mutations procedurally using `rpcClient.mutation()`, like so:
278
+
279
+ ```typescript
280
+ const result = await rpcClient.mutation("api.subscribe", { email: "hello@internet.com", listId: "12345" })
281
+ console.log("Sign up result", result)
282
+ ```
283
+
284
+ ## Routers [#routers]
285
+
286
+ Multiple procedures can be exported from a single file by exporting a "router". A router mounts multiple endpoints at once. Use `rpc.router()` when you have a group of highly-related API functions.
287
+
288
+ ```typescript filename="/server/routes/api.math.ts"
289
+ import { rpc } from "eddev/server"
290
+ import z from "zod"
291
+
292
+ export default rpc.router({
293
+ sum: rpc.procedure
294
+ .input(
295
+ z.object({
296
+ a: z.number(),
297
+ b: z.number(),
298
+ }),
299
+ )
300
+ .query(({ input }) => {
301
+ return input.a + input.b
302
+ }),
303
+ random: rpc.procedure.query(() => {
304
+ return Math.random()
305
+ }),
306
+ pi: rpc.procedure.query(() => {
307
+ return Math.PI
308
+ }),
309
+ })
310
+ ```
311
+
312
+ Procedures defined in a router can be called using [`useRPCQuery()`](#userpcquery) and [`useRPCMutation()`](#userpcmutation), like any other procedure.
313
+
314
+ ```tsx
315
+ import { useRPCQuery } from "eddev/hooks"
316
+
317
+ export function Demo() {
318
+ const pi = useRPCQuery("api.math.pi")
319
+ const random = useRPCQuery("api.math.random")
320
+
321
+ const sum = useRPCQuery(
322
+ "api.math.sum",
323
+ { a: pi.data ?? 0, b: random.data ?? 0 },
324
+ {
325
+ enabled: pi.isSuccess && random.isSuccess,
326
+ },
327
+ )
328
+
329
+ return (
330
+ <div>
331
+ <div>Pi: {pi.data}</div>
332
+ <div>Random: {random.data}</div>
333
+ <div>Sum: {sum.data}</div>
334
+ </div>
335
+ )
336
+
337
+ }
338
+ ```
339
+
340
+ ## Context Middleware [#context-middleware]
341
+
342
+ The `/server/_context.ts` file is optional, but can be defined to extract common metadata from a request. You can use it to parse Auth headers or cookies, and the returned values will be available in the `ctx` argument of your procedures.
343
+
344
+ ```typescript filename="/server/_context.ts"
345
+ import { defineServerContext } from "eddev/server"
346
+
347
+ export default defineServerContext((ctx) => {
348
+ const headers = ctx.req.headers
349
+ return {
350
+ ...ctx,
351
+ user: {
352
+ ip: headers.get("x-real-ip") ?? headers.get("x-real-ip") ?? "",
353
+ },
354
+ }
355
+ })
356
+ ```
357
+
358
+ You'll need to run `eddev dev` to ensure that the TypeScript types are generated at least once. In any procedure, you can now access `ctx.user.ip`.
359
+
360
+ ```typescript filename="/server/api.ip.ts"
361
+ import { rpc } from "eddev/server"
362
+
363
+ export default rpc.procedure.query(async ({ ctx }) => {
364
+ return {
365
+ ip: "Your IP is " + ctx.user.ip,
366
+ }
367
+ })
368
+ ```
369
+
370
+ ## Connecting to WordPress [#connecting-to-wordpress]
371
+
372
+ [Generated GraphQL hooks](/docs/graphql/query-hooks) can be called using `.fetch()` and `.mutate()`, with arguments.
373
+
374
+ ```typescript
375
+ import { rpc } from "eddev/server"
376
+
377
+ export default rpc.procedure
378
+ .query(async ({ ctx, input }) => {
379
+ const data = useLatestNews.fetch({ category: "design" })
380
+
381
+ return {
382
+ latest: data.news.nodes[0]
383
+ }
384
+ })
385
+ ```
386
+
387
+ ## Non-RPC Responses [#non-rpc-responses]
388
+
389
+ RPC queries typically return JSON payloads with a `Content-Type: application/json` header, however procedures can also be used as generic HTTP endpoints, by simply returning a native JavaScript `Response` object. Note that this will only work on Vercel-deployed domains.
390
+
391
+ ```typescript
392
+ import { rpc } from "eddev/server"
393
+
394
+ export default rpc.procedure.query(async () => {
395
+ return new Response("Hello world", {
396
+ status: 200,
397
+ headers: {
398
+ "Content-Type": "text/plain",
399
+ "Cache-Control": "max-age=3600, s-maxage=3600, public",
400
+ },
401
+ })
402
+ })
403
+ ```
404
+
405
+ <Callout>
406
+ Note that when returning a `Response` object, the procedure may no longer be callable with `useRPCQuery()` and `useRPCMutation()`, since it may no longer conform to tRPC's return protocol.
407
+ </Callout>
408
+
409
+ There are also some helper methods for force-rendering pages from other routes.
410
+
411
+ The `renderPage()` function can be used to display a page with a specific route. This should be avoided in the majority of cases.
412
+
413
+ ```typescript
414
+ import { rpc, renderPage } from "eddev/server"
415
+
416
+ export default rpc.procedure.query(async () => {
417
+ return renderPage({
418
+ pathname: "/",
419
+ })
420
+ })
421
+ ```
422
+
423
+ Similarly, the `renderErrorPage()` function can be used to display the site's standard error page.
424
+
425
+ ```typescript
426
+ import { renderErrorPage, rpc } from "eddev/server"
427
+
428
+ export default rpc.procedure.query(async () => {
429
+ const result = await renderErrorPage({
430
+ title: "Bad Request",
431
+ statusCode: 400,
432
+ userMessage: "Something went wrong",
433
+ })
434
+ return result
435
+ })
436
+ ```