vite-wp 0.1.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/dist/astro.d.ts +5 -0
  4. package/dist/astro.js +27 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +40 -0
  7. package/dist/commands/dev.d.ts +1 -0
  8. package/dist/commands/dev.js +62 -0
  9. package/dist/commands/doctor.d.ts +6 -0
  10. package/dist/commands/doctor.js +139 -0
  11. package/dist/commands/init.d.ts +1 -0
  12. package/dist/commands/init.js +55 -0
  13. package/dist/commands/smoke.d.ts +1 -0
  14. package/dist/commands/smoke.js +138 -0
  15. package/dist/commands/types.d.ts +1 -0
  16. package/dist/commands/types.js +67 -0
  17. package/dist/config.d.ts +71 -0
  18. package/dist/config.js +74 -0
  19. package/dist/content.d.ts +44 -0
  20. package/dist/content.js +190 -0
  21. package/dist/dev-toolbar/vitewp-toolbar.d.ts +23 -0
  22. package/dist/dev-toolbar/vitewp-toolbar.js +144 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.js +2 -0
  25. package/dist/live.config.d.ts +64 -0
  26. package/dist/live.config.js +21 -0
  27. package/dist/runtime/astro.d.ts +4 -0
  28. package/dist/runtime/astro.js +69 -0
  29. package/dist/runtime/composer.d.ts +2 -0
  30. package/dist/runtime/composer.js +36 -0
  31. package/dist/runtime/php.d.ts +3 -0
  32. package/dist/runtime/php.js +79 -0
  33. package/dist/runtime/ports.d.ts +3 -0
  34. package/dist/runtime/ports.js +38 -0
  35. package/dist/runtime/process.d.ts +12 -0
  36. package/dist/runtime/process.js +51 -0
  37. package/dist/runtime/proxy.d.ts +6 -0
  38. package/dist/runtime/proxy.js +204 -0
  39. package/dist/runtime/wp-config.d.ts +3 -0
  40. package/dist/runtime/wp-config.js +82 -0
  41. package/dist/wordpress/client.d.ts +98 -0
  42. package/dist/wordpress/client.js +133 -0
  43. package/dist/wordpress/generated-types.d.ts +33 -0
  44. package/dist/wordpress/generated-types.js +2 -0
  45. package/dist/wordpress/menus.d.ts +24 -0
  46. package/dist/wordpress/menus.js +20 -0
  47. package/dist/wordpress/schemas.d.ts +58 -0
  48. package/dist/wordpress/schemas.js +39 -0
  49. package/dist/wordpress/templates.d.ts +19 -0
  50. package/dist/wordpress/templates.js +88 -0
  51. package/package.json +78 -0
  52. package/starter/.env.example +15 -0
  53. package/starter/astro.config.mjs +16 -0
  54. package/starter/composer.json +37 -0
  55. package/starter/src/env.d.ts +1 -0
  56. package/starter/src/live.config.ts +22 -0
  57. package/starter/src/pages/[...slug].astro +101 -0
  58. package/starter/src/templates/404.astro +11 -0
  59. package/starter/src/templates/pages/.gitkeep +0 -0
  60. package/starter/src/templates/pages/default.astro +12 -0
  61. package/starter/src/templates/pages/front-page.astro +13 -0
  62. package/starter/src/templates/partials/Header.astro +23 -0
  63. package/starter/src/templates/partials/Pagination.astro +21 -0
  64. package/starter/src/templates/post-types/.gitkeep +0 -0
  65. package/starter/src/templates/posts/.gitkeep +0 -0
  66. package/starter/src/templates/posts/archive.astro +25 -0
  67. package/starter/src/templates/posts/single.astro +12 -0
  68. package/starter/src/templates/search.astro +30 -0
  69. package/starter/src/templates/taxonomies/.gitkeep +0 -0
  70. package/starter/src/templates/taxonomies/taxonomy-[taxonomy].astro +28 -0
  71. package/starter/src/wordpress/client.ts +263 -0
  72. package/starter/src/wordpress/generated-types.ts +38 -0
  73. package/starter/src/wordpress/menus.ts +51 -0
  74. package/starter/src/wordpress/schemas.ts +44 -0
  75. package/starter/src/wordpress/templates.ts +113 -0
  76. package/starter/tsconfig.json +4 -0
  77. package/starter/vitewp.config.ts +29 -0
  78. package/starter/wordpress/content/mu-plugins/.gitkeep +0 -0
  79. package/starter/wordpress/content/mu-plugins/vitewp-bridge.php +560 -0
  80. package/starter/wordpress/content/themes/vitewp/functions.php +9 -0
  81. package/starter/wordpress/content/themes/vitewp/index.php +22 -0
  82. package/starter/wordpress/content/themes/vitewp/style.css +10 -0
@@ -0,0 +1,15 @@
1
+ # ViteWP database settings
2
+ WP_DB_HOST=127.0.0.1
3
+ WP_DB_PORT=3306
4
+ WP_DB_NAME=vitewp
5
+ WP_DB_USER=root
6
+ WP_DB_PASSWORD=
7
+ WP_DB_TABLE_PREFIX=wp_
8
+
9
+ # ViteWP internal dev runtime listeners
10
+ # The site itself is served from wordpress.url in vitewp.config.ts.
11
+ # Leave these as auto unless you are debugging ViteWP internals.
12
+ VITEWP_PHP_HOST=127.0.0.1
13
+ VITEWP_PHP_PORT=auto
14
+ VITEWP_ASTRO_HOST=127.0.0.1
15
+ VITEWP_ASTRO_PORT=auto
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from 'astro/config';
2
+ import vitewp from 'vitewp/astro';
3
+
4
+ export default defineConfig({
5
+ output: 'server',
6
+ integrations: [vitewp()],
7
+ vite: {
8
+ server: {
9
+ ws: {
10
+ protocol: 'ws',
11
+ host: 'localhost',
12
+ clientPort: 3000,
13
+ },
14
+ },
15
+ },
16
+ });
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "vitewp/project",
3
+ "description": "Composer-managed WordPress runtime for a ViteWP project.",
4
+ "type": "project",
5
+ "require": {
6
+ "php": ">=8.2",
7
+ "johnpbloch/wordpress": "^7.0",
8
+ "composer/installers": "^2.3"
9
+ },
10
+ "repositories": [
11
+ {
12
+ "type": "composer",
13
+ "url": "https://wpackagist.org"
14
+ }
15
+ ],
16
+ "extra": {
17
+ "wordpress-install-dir": "wordpress/public",
18
+ "installer-paths": {
19
+ "wordpress/content/plugins/{$name}/": [
20
+ "type:wordpress-plugin"
21
+ ],
22
+ "wordpress/content/mu-plugins/{$name}/": [
23
+ "type:wordpress-muplugin"
24
+ ],
25
+ "wordpress/content/themes/{$name}/": [
26
+ "type:wordpress-theme"
27
+ ]
28
+ }
29
+ },
30
+ "config": {
31
+ "allow-plugins": {
32
+ "composer/installers": true,
33
+ "johnpbloch/wordpress-core-installer": true
34
+ },
35
+ "sort-packages": true
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="astro/client" />
@@ -0,0 +1,22 @@
1
+ import { defineLiveCollection } from 'astro/content/config';
2
+ import { wpMenuLoader, wpPostTypeLoader, wpRouteLoader } from 'vitewp/content';
3
+ import { wpContentItemSchema, wpMenuSchema, wpResolvedRouteSchema } from './wordpress/schemas.js';
4
+
5
+ export const collections = {
6
+ routes: defineLiveCollection({
7
+ loader: wpRouteLoader(),
8
+ schema: wpResolvedRouteSchema,
9
+ }),
10
+ posts: defineLiveCollection({
11
+ loader: wpPostTypeLoader({ postType: 'post', restBase: 'posts' }),
12
+ schema: wpContentItemSchema,
13
+ }),
14
+ pages: defineLiveCollection({
15
+ loader: wpPostTypeLoader({ postType: 'page', restBase: 'pages' }),
16
+ schema: wpContentItemSchema,
17
+ }),
18
+ menus: defineLiveCollection({
19
+ loader: wpMenuLoader(),
20
+ schema: wpMenuSchema,
21
+ }),
22
+ };
@@ -0,0 +1,101 @@
1
+ ---
2
+ export const prerender = false;
3
+
4
+ import { getLiveEntry } from 'astro:content';
5
+ import type { WpResolvedRoute } from '../wordpress/client.js';
6
+ import { createTemplateContext, templateCandidates } from '../wordpress/templates.js';
7
+
8
+ type TemplateModule = {
9
+ default: unknown;
10
+ };
11
+
12
+ const slug = Astro.params.slug ?? '';
13
+ const routePath = `${Astro.url.pathname}${Astro.url.search}`;
14
+ const routeResult = await getLiveEntry('routes', { path: routePath });
15
+
16
+ if (routeResult.error && routeResult.error.name !== 'LiveEntryNotFoundError') {
17
+ throw routeResult.error;
18
+ }
19
+
20
+ const route = routeResult.entry?.data as WpResolvedRoute | undefined;
21
+ let candidates: string[] = [];
22
+
23
+ if (!route) {
24
+ Astro.response.status = 404;
25
+ Astro.response.statusText = 'Not Found';
26
+ }
27
+
28
+ const templates = import.meta.glob<TemplateModule>('../templates/**/*.astro');
29
+ let Template: any = null;
30
+ let templatePath = '';
31
+ const showDebug = import.meta.env.DEV;
32
+ let context = null;
33
+
34
+ if (route) {
35
+ context = createTemplateContext(route);
36
+ candidates = templateCandidates(route);
37
+
38
+ for (const candidate of candidates) {
39
+ const path = `../templates/${candidate}`;
40
+ const loader = templates[path];
41
+
42
+ if (loader) {
43
+ Template = (await loader()).default;
44
+ templatePath = candidate;
45
+ break;
46
+ }
47
+ }
48
+ } else {
49
+ candidates = ['404.astro'];
50
+ const loader = templates['../templates/404.astro'];
51
+ if (loader) {
52
+ Template = (await loader()).default;
53
+ templatePath = '404.astro';
54
+ }
55
+ }
56
+
57
+ const toolbarRouteInfo = showDebug ? {
58
+ path: routePath,
59
+ matched: Boolean(route),
60
+ kind: context?.route.kind ?? '404',
61
+ liveCollection: routeResult.entry ? {
62
+ collection: 'routes',
63
+ entryId: routeResult.entry.id,
64
+ cacheHint: routeResult.cacheHint ?? null,
65
+ } : null,
66
+ postType: context?.postType ?? null,
67
+ slug: context?.slug ?? null,
68
+ page: context?.page ?? null,
69
+ totalPages: context?.totalPages ?? null,
70
+ template: templatePath || null,
71
+ candidateTemplates: candidates,
72
+ } : null;
73
+ const toolbarRouteInfoJson = toolbarRouteInfo
74
+ ? JSON.stringify(toolbarRouteInfo).replaceAll('<', '\\u003c')
75
+ : '';
76
+ ---
77
+ <html lang="en">
78
+ <head>
79
+ <meta charset="utf-8" />
80
+ <title>{context?.title ? `${context.title.replace(/<[^>]+>/g, '')} · ViteWP` : 'Not found · ViteWP'}</title>
81
+ {toolbarRouteInfoJson && (
82
+ <script is:inline set:html={`window.__VITEWP_ROUTE_INFO__ = ${toolbarRouteInfoJson}; window.dispatchEvent(new CustomEvent('vitewp:route-info', { detail: window.__VITEWP_ROUTE_INFO__ }));`} />
83
+ )}
84
+ </head>
85
+ <body>
86
+ <main>
87
+ {Template && context ? (
88
+ <>
89
+ <Template {...context} />
90
+ </>
91
+ ) : Template ? (
92
+ <Template slug={slug} />
93
+ ) : (
94
+ <article>
95
+ <h1>Not found</h1>
96
+ <p>No WordPress route matched <code>{Astro.url.pathname}</code>.</p>
97
+ </article>
98
+ )}
99
+ </main>
100
+ </body>
101
+ </html>
@@ -0,0 +1,11 @@
1
+ ---
2
+ interface Props {
3
+ slug?: string;
4
+ }
5
+
6
+ const { slug = '' } = Astro.props;
7
+ ---
8
+ <article>
9
+ <h1>Not found</h1>
10
+ <p>No WordPress route matched <code>/{slug}</code>.</p>
11
+ </article>
File without changes
@@ -0,0 +1,12 @@
1
+ ---
2
+ interface Props {
3
+ title?: string;
4
+ content?: string;
5
+ }
6
+
7
+ const { title = 'Untitled page', content = '' } = Astro.props;
8
+ ---
9
+ <article>
10
+ <h1>{title}</h1>
11
+ <div set:html={content} />
12
+ </article>
@@ -0,0 +1,13 @@
1
+ ---
2
+ interface Props {
3
+ title?: string;
4
+ content?: string;
5
+ }
6
+
7
+ const { title = 'Home', content = '' } = Astro.props;
8
+ ---
9
+ <article>
10
+ <p style="font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; color: #666; text-transform: uppercase; letter-spacing: .08em;">Front page</p>
11
+ <h1>{title}</h1>
12
+ <div set:html={content} />
13
+ </article>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import { getMenuByLocation, getMenuBySlug } from '../../wordpress/menus.js';
3
+
4
+ interface Props {
5
+ location?: string;
6
+ slug?: string;
7
+ }
8
+
9
+ const { location = 'primary', slug } = Astro.props;
10
+ const menu = slug ? await getMenuBySlug(slug) : await getMenuByLocation(location);
11
+ ---
12
+ <header>
13
+ <a href="/">ViteWP</a>
14
+ {menu && (
15
+ <nav aria-label={menu.name}>
16
+ <ul>
17
+ {menu.items.filter((item) => item.parent === 0).map((item) => (
18
+ <li><a href={item.url}>{item.title}</a></li>
19
+ ))}
20
+ </ul>
21
+ </nav>
22
+ )}
23
+ </header>
@@ -0,0 +1,21 @@
1
+ ---
2
+ interface Props {
3
+ page?: number;
4
+ totalPages?: number;
5
+ }
6
+
7
+ const { page = 1, totalPages = 1 } = Astro.props;
8
+ const currentPath = Astro.url.pathname.replace(/\/page\/\d+\/?$/, '').replace(/\/$/, '') || '/';
9
+ const query = Astro.url.search;
10
+ const pageHref = (nextPage: number) => {
11
+ const path = nextPage <= 1 ? currentPath : `${currentPath === '/' ? '' : currentPath}/page/${nextPage}`;
12
+ return `${path}${query}`;
13
+ };
14
+ ---
15
+ {totalPages > 1 && (
16
+ <nav aria-label="Pagination" style="display: flex; gap: 12px; margin-top: 32px;">
17
+ {page > 1 && <a href={pageHref(page - 1)}>Previous</a>}
18
+ <span>Page {page} of {totalPages}</span>
19
+ {page < totalPages && <a href={pageHref(page + 1)}>Next</a>}
20
+ </nav>
21
+ )}
File without changes
File without changes
@@ -0,0 +1,25 @@
1
+ ---
2
+ import Pagination from '../partials/Pagination.astro';
3
+ import type { WpContentItem } from '../../wordpress/client.js';
4
+
5
+ interface Props {
6
+ title?: string;
7
+ items?: WpContentItem[];
8
+ page?: number;
9
+ totalPages?: number;
10
+ }
11
+
12
+ const { title = 'Posts', items = [], page = 1, totalPages = 1 } = Astro.props;
13
+ ---
14
+ <section>
15
+ <h1>{title}</h1>
16
+ <div>
17
+ {items.map((item) => (
18
+ <article>
19
+ <h2><a href={new URL(item.link).pathname}>{item.title.rendered}</a></h2>
20
+ {item.excerpt?.rendered && <div set:html={item.excerpt.rendered} />}
21
+ </article>
22
+ ))}
23
+ </div>
24
+ <Pagination page={page} totalPages={totalPages} />
25
+ </section>
@@ -0,0 +1,12 @@
1
+ ---
2
+ interface Props {
3
+ title?: string;
4
+ content?: string;
5
+ }
6
+
7
+ const { title = 'Untitled post', content = '' } = Astro.props;
8
+ ---
9
+ <article>
10
+ <h1>{title}</h1>
11
+ <div set:html={content} />
12
+ </article>
@@ -0,0 +1,30 @@
1
+ ---
2
+ import Pagination from './partials/Pagination.astro';
3
+ import type { WpContentItem } from '../wordpress/client.js';
4
+
5
+ interface Props {
6
+ title?: string;
7
+ search?: string;
8
+ items?: WpContentItem[];
9
+ page?: number;
10
+ totalPages?: number;
11
+ }
12
+
13
+ const { title = 'Search', search = '', items = [], page = 1, totalPages = 1 } = Astro.props;
14
+ ---
15
+ <section>
16
+ <h1>{title}</h1>
17
+ <form action="/search" method="get" role="search">
18
+ <input type="search" name="s" value={search} placeholder="Search…" />
19
+ <button type="submit">Search</button>
20
+ </form>
21
+ <div>
22
+ {items.length === 0 ? <p>No results found.</p> : items.map((item) => (
23
+ <article>
24
+ <h2><a href={new URL(item.link).pathname}>{item.title.rendered}</a></h2>
25
+ {item.excerpt?.rendered && <div set:html={item.excerpt.rendered} />}
26
+ </article>
27
+ ))}
28
+ </div>
29
+ <Pagination page={page} totalPages={totalPages} />
30
+ </section>
File without changes
@@ -0,0 +1,28 @@
1
+ ---
2
+ import Pagination from '../partials/Pagination.astro';
3
+ import type { WpContentItem } from '../../wordpress/client.js';
4
+
5
+ interface Props {
6
+ title?: string;
7
+ taxonomy?: string;
8
+ termName?: string;
9
+ items?: WpContentItem[];
10
+ page?: number;
11
+ totalPages?: number;
12
+ }
13
+
14
+ const { title = 'Archive', taxonomy = '', termName = title, items = [], page = 1, totalPages = 1 } = Astro.props;
15
+ ---
16
+ <section>
17
+ <p style="font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; color: #666; text-transform: uppercase; letter-spacing: .08em;">{taxonomy}</p>
18
+ <h1>{termName}</h1>
19
+ <div>
20
+ {items.map((item) => (
21
+ <article>
22
+ <h2><a href={new URL(item.link).pathname}>{item.title.rendered}</a></h2>
23
+ {item.excerpt?.rendered && <div set:html={item.excerpt.rendered} />}
24
+ </article>
25
+ ))}
26
+ </div>
27
+ <Pagination page={page} totalPages={totalPages} />
28
+ </section>
@@ -0,0 +1,263 @@
1
+ export interface WpRenderedField {
2
+ rendered: string;
3
+ }
4
+
5
+ export interface WpContentItem {
6
+ id: number;
7
+ slug: string;
8
+ type: string;
9
+ link: string;
10
+ title: WpRenderedField;
11
+ content: WpRenderedField;
12
+ excerpt?: WpRenderedField;
13
+ date?: string;
14
+ modified?: string;
15
+ }
16
+
17
+ export interface WpArchivePayload {
18
+ items: WpContentItem[];
19
+ page: number;
20
+ perPage: number;
21
+ total: number;
22
+ totalPages: number;
23
+ }
24
+
25
+ interface BaseArchiveResolution {
26
+ found: true;
27
+ slug: string;
28
+ postType: string;
29
+ restBase: string;
30
+ title: string;
31
+ page?: number;
32
+ }
33
+
34
+ export type WpBridgeResolution =
35
+ | {
36
+ found: true;
37
+ kind: 'page' | 'single';
38
+ id: number;
39
+ slug: string;
40
+ postType: string;
41
+ restBase: string;
42
+ isFrontPage?: boolean;
43
+ isPostsPage?: boolean;
44
+ }
45
+ | (BaseArchiveResolution & {
46
+ kind: 'postsArchive' | 'postTypeArchive';
47
+ })
48
+ | (BaseArchiveResolution & {
49
+ kind: 'taxonomyArchive';
50
+ taxonomy: string;
51
+ taxonomyRestBase: string;
52
+ termId: number;
53
+ termName: string;
54
+ })
55
+ | (BaseArchiveResolution & {
56
+ kind: 'search';
57
+ search: string;
58
+ })
59
+ | {
60
+ found: false;
61
+ kind: 'notFound';
62
+ };
63
+
64
+ export type WpResolvedRoute =
65
+ | {
66
+ kind: 'page';
67
+ postType: 'page';
68
+ restBase: 'pages';
69
+ slug: string;
70
+ item: WpContentItem;
71
+ isFrontPage?: boolean;
72
+ }
73
+ | {
74
+ kind: 'single';
75
+ postType: string;
76
+ restBase: string;
77
+ slug: string;
78
+ item: WpContentItem;
79
+ }
80
+ | {
81
+ kind: 'postsArchive' | 'postTypeArchive' | 'search';
82
+ postType: string;
83
+ restBase: string;
84
+ slug: string;
85
+ title: string;
86
+ items: WpContentItem[];
87
+ page: number;
88
+ perPage: number;
89
+ total: number;
90
+ totalPages: number;
91
+ search?: string;
92
+ }
93
+ | {
94
+ kind: 'taxonomyArchive';
95
+ postType: string;
96
+ restBase: string;
97
+ slug: string;
98
+ title: string;
99
+ items: WpContentItem[];
100
+ page: number;
101
+ perPage: number;
102
+ total: number;
103
+ totalPages: number;
104
+ taxonomy: string;
105
+ taxonomyRestBase: string;
106
+ termId: number;
107
+ termName: string;
108
+ };
109
+
110
+ export async function resolveWordPressRoute(pathname: string): Promise<WpResolvedRoute | null> {
111
+ const resolution = await resolveViaWordPress(pathname);
112
+
113
+ if (!resolution.found) {
114
+ return null;
115
+ }
116
+
117
+ switch (resolution.kind) {
118
+ case 'postsArchive':
119
+ case 'postTypeArchive': {
120
+ const archive = await getArchive({
121
+ kind: resolution.kind,
122
+ postType: resolution.postType,
123
+ page: resolution.page ?? 1,
124
+ });
125
+ return {
126
+ kind: resolution.kind,
127
+ postType: resolution.postType,
128
+ restBase: resolution.restBase,
129
+ slug: resolution.slug,
130
+ title: resolution.title,
131
+ ...archive,
132
+ };
133
+ }
134
+ case 'taxonomyArchive': {
135
+ const archive = await getArchive({
136
+ kind: resolution.kind,
137
+ postType: resolution.postType,
138
+ taxonomy: resolution.taxonomy,
139
+ termId: resolution.termId,
140
+ page: resolution.page ?? 1,
141
+ });
142
+ return {
143
+ kind: 'taxonomyArchive',
144
+ postType: resolution.postType,
145
+ restBase: resolution.restBase,
146
+ slug: resolution.slug,
147
+ title: resolution.title,
148
+ taxonomy: resolution.taxonomy,
149
+ taxonomyRestBase: resolution.taxonomyRestBase,
150
+ termId: resolution.termId,
151
+ termName: resolution.termName,
152
+ ...archive,
153
+ };
154
+ }
155
+ case 'search': {
156
+ const archive = await getArchive({
157
+ kind: resolution.kind,
158
+ search: resolution.search,
159
+ page: resolution.page ?? 1,
160
+ });
161
+ return {
162
+ kind: 'search',
163
+ postType: resolution.postType,
164
+ restBase: resolution.restBase,
165
+ slug: resolution.slug,
166
+ title: resolution.title,
167
+ search: resolution.search,
168
+ ...archive,
169
+ };
170
+ }
171
+ case 'page':
172
+ case 'single':
173
+ break;
174
+ }
175
+
176
+ if (resolution.isPostsPage) {
177
+ const archive = await getArchive({ kind: 'postsArchive', postType: 'post', page: 1 });
178
+ return {
179
+ kind: 'postsArchive',
180
+ postType: 'post',
181
+ restBase: 'posts',
182
+ slug: resolution.slug,
183
+ title: '',
184
+ ...archive,
185
+ };
186
+ }
187
+
188
+ const item = await getById(resolution.restBase, resolution.id);
189
+
190
+ if (resolution.kind === 'page') {
191
+ return {
192
+ kind: 'page',
193
+ postType: 'page',
194
+ restBase: 'pages',
195
+ slug: resolution.slug,
196
+ item,
197
+ isFrontPage: resolution.isFrontPage,
198
+ };
199
+ }
200
+
201
+ return {
202
+ kind: 'single',
203
+ postType: resolution.postType,
204
+ restBase: resolution.restBase,
205
+ slug: resolution.slug,
206
+ item,
207
+ };
208
+ }
209
+
210
+ export function getWordPressApiBase() {
211
+ const base = getWordPressBaseUrl();
212
+ return `${base}/wp-json/wp/v2`;
213
+ }
214
+
215
+ export function getWordPressBaseUrl() {
216
+ return (process.env.VITEWP_PUBLIC_URL ?? process.env.WP_URL ?? 'http://localhost:3000').replace(/\/$/, '');
217
+ }
218
+
219
+ async function resolveViaWordPress(pathname: string) {
220
+ const url = new URL(`${getWordPressBaseUrl()}/wp-json/vitewp/v1/resolve`);
221
+ url.searchParams.set('path', pathname || '/');
222
+
223
+ const response = await fetch(url);
224
+
225
+ if (response.status === 404) {
226
+ return { found: false, kind: 'notFound' } satisfies WpBridgeResolution;
227
+ }
228
+
229
+ if (!response.ok) {
230
+ throw new Error(`ViteWP route resolution failed: ${response.status} ${response.statusText}`);
231
+ }
232
+
233
+ return response.json() as Promise<WpBridgeResolution>;
234
+ }
235
+
236
+ async function getById(restBase: string, id: number) {
237
+ const url = new URL(`${getWordPressApiBase()}/${restBase}/${id}`);
238
+ url.searchParams.set('_embed', '1');
239
+
240
+ return getJson<WpContentItem>(url);
241
+ }
242
+
243
+ async function getArchive(params: Record<string, string | number | undefined>) {
244
+ const url = new URL(`${getWordPressBaseUrl()}/wp-json/vitewp/v1/archive`);
245
+
246
+ for (const [key, value] of Object.entries(params)) {
247
+ if (value !== undefined) {
248
+ url.searchParams.set(key, String(value));
249
+ }
250
+ }
251
+
252
+ return getJson<WpArchivePayload>(url);
253
+ }
254
+
255
+ async function getJson<T>(url: URL): Promise<T> {
256
+ const response = await fetch(url);
257
+
258
+ if (!response.ok) {
259
+ throw new Error(`WordPress REST request failed: ${response.status} ${response.statusText}`);
260
+ }
261
+
262
+ return response.json() as Promise<T>;
263
+ }