@jasonshimmy/vite-plugin-cer-app 0.7.0 → 0.9.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 (98) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/ROADMAP.md +278 -0
  3. package/commits.txt +1 -1
  4. package/dist/cli/commands/preview-isr.d.ts +6 -0
  5. package/dist/cli/commands/preview-isr.d.ts.map +1 -1
  6. package/dist/cli/commands/preview-isr.js +12 -0
  7. package/dist/cli/commands/preview-isr.js.map +1 -1
  8. package/dist/cli/commands/preview.d.ts.map +1 -1
  9. package/dist/cli/commands/preview.js +66 -6
  10. package/dist/cli/commands/preview.js.map +1 -1
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/plugin/dev-server.d.ts +1 -0
  14. package/dist/plugin/dev-server.d.ts.map +1 -1
  15. package/dist/plugin/dts-generator.d.ts.map +1 -1
  16. package/dist/plugin/dts-generator.js +4 -2
  17. package/dist/plugin/dts-generator.js.map +1 -1
  18. package/dist/plugin/index.d.ts.map +1 -1
  19. package/dist/plugin/index.js +30 -12
  20. package/dist/plugin/index.js.map +1 -1
  21. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  22. package/dist/plugin/transforms/auto-import.js +5 -4
  23. package/dist/plugin/transforms/auto-import.js.map +1 -1
  24. package/dist/plugin/virtual/routes.d.ts.map +1 -1
  25. package/dist/plugin/virtual/routes.js +7 -1
  26. package/dist/plugin/virtual/routes.js.map +1 -1
  27. package/dist/runtime/composables/define-middleware.d.ts +15 -0
  28. package/dist/runtime/composables/define-middleware.d.ts.map +1 -0
  29. package/dist/runtime/composables/define-middleware.js +16 -0
  30. package/dist/runtime/composables/define-middleware.js.map +1 -0
  31. package/dist/runtime/composables/index.d.ts +7 -1
  32. package/dist/runtime/composables/index.d.ts.map +1 -1
  33. package/dist/runtime/composables/index.js +4 -1
  34. package/dist/runtime/composables/index.js.map +1 -1
  35. package/dist/runtime/composables/use-cookie.d.ts +38 -0
  36. package/dist/runtime/composables/use-cookie.d.ts.map +1 -0
  37. package/dist/runtime/composables/use-cookie.js +104 -0
  38. package/dist/runtime/composables/use-cookie.js.map +1 -0
  39. package/dist/runtime/composables/use-runtime-config.d.ts +32 -14
  40. package/dist/runtime/composables/use-runtime-config.d.ts.map +1 -1
  41. package/dist/runtime/composables/use-runtime-config.js +42 -8
  42. package/dist/runtime/composables/use-runtime-config.js.map +1 -1
  43. package/dist/runtime/composables/use-seo-meta.d.ts +42 -0
  44. package/dist/runtime/composables/use-seo-meta.d.ts.map +1 -0
  45. package/dist/runtime/composables/use-seo-meta.js +58 -0
  46. package/dist/runtime/composables/use-seo-meta.js.map +1 -0
  47. package/dist/runtime/entry-server-template.d.ts +1 -1
  48. package/dist/runtime/entry-server-template.d.ts.map +1 -1
  49. package/dist/runtime/entry-server-template.js +15 -3
  50. package/dist/runtime/entry-server-template.js.map +1 -1
  51. package/dist/types/config.d.ts +14 -0
  52. package/dist/types/config.d.ts.map +1 -1
  53. package/dist/types/config.js.map +1 -1
  54. package/dist/types/index.d.ts +2 -2
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/dist/types/middleware.d.ts +8 -2
  57. package/dist/types/middleware.d.ts.map +1 -1
  58. package/docs/cli.md +5 -0
  59. package/docs/composables.md +165 -7
  60. package/docs/configuration.md +53 -3
  61. package/docs/middleware.md +53 -25
  62. package/e2e/cypress/e2e/cookie.cy.ts +68 -0
  63. package/e2e/cypress/e2e/middleware.cy.ts +45 -0
  64. package/e2e/cypress/e2e/preview-hardening.cy.ts +79 -0
  65. package/e2e/cypress/e2e/seo-meta.cy.ts +108 -0
  66. package/e2e/kitchen-sink/app/middleware/auth.ts +3 -7
  67. package/e2e/kitchen-sink/app/pages/cookie-test.ts +22 -0
  68. package/e2e/kitchen-sink/app/pages/seo-test.ts +23 -0
  69. package/package.json +1 -1
  70. package/src/__tests__/cli/preview-hardening.test.ts +175 -0
  71. package/src/__tests__/cli/preview-isr.test.ts +30 -0
  72. package/src/__tests__/plugin/cer-app-plugin.test.ts +50 -0
  73. package/src/__tests__/plugin/entry-server-template.test.ts +21 -0
  74. package/src/__tests__/plugin/resolve-config.test.ts +18 -0
  75. package/src/__tests__/plugin/transforms/auto-import.test.ts +39 -0
  76. package/src/__tests__/plugin/virtual/middleware.test.ts +15 -0
  77. package/src/__tests__/plugin/virtual/routes.test.ts +32 -0
  78. package/src/__tests__/runtime/define-middleware.test.ts +43 -0
  79. package/src/__tests__/runtime/use-cookie.test.ts +218 -0
  80. package/src/__tests__/runtime/use-runtime-config.test.ts +86 -2
  81. package/src/__tests__/runtime/use-seo-meta.test.ts +109 -0
  82. package/src/cli/commands/preview-isr.ts +14 -0
  83. package/src/cli/commands/preview.ts +78 -6
  84. package/src/index.ts +3 -1
  85. package/src/plugin/dev-server.ts +1 -1
  86. package/src/plugin/dts-generator.ts +4 -2
  87. package/src/plugin/index.ts +32 -11
  88. package/src/plugin/transforms/auto-import.ts +5 -4
  89. package/src/plugin/virtual/routes.ts +7 -1
  90. package/src/runtime/composables/define-middleware.ts +17 -0
  91. package/src/runtime/composables/index.ts +7 -1
  92. package/src/runtime/composables/use-cookie.ts +128 -0
  93. package/src/runtime/composables/use-runtime-config.ts +67 -11
  94. package/src/runtime/composables/use-seo-meta.ts +75 -0
  95. package/src/runtime/entry-server-template.ts +15 -3
  96. package/src/types/config.ts +15 -0
  97. package/src/types/index.ts +2 -2
  98. package/src/types/middleware.ts +8 -6
@@ -210,7 +210,7 @@ Set any flag to `false` to opt out and manage imports manually.
210
210
 
211
211
  ## `runtimeConfig` options
212
212
 
213
- Expose typed, centralized public configuration to both server and client code via `useRuntimeConfig()`.
213
+ Expose typed, centralized configuration to your app. Public values are available everywhere; private values are server-only secrets resolved from environment variables at startup.
214
214
 
215
215
  ```ts
216
216
  export default defineConfig({
@@ -219,6 +219,10 @@ export default defineConfig({
219
219
  apiBase: process.env.VITE_API_BASE ?? 'https://api.example.com',
220
220
  appVersion: '1.0.0',
221
221
  },
222
+ private: {
223
+ dbUrl: '', // resolved from process.env.DB_URL at server startup
224
+ secretKey: '', // resolved from process.env.SECRET_KEY at server startup
225
+ },
222
226
  },
223
227
  })
224
228
  ```
@@ -230,9 +234,9 @@ export default defineConfig({
230
234
 
231
235
  Values placed here are serialized into `virtual:cer-app-config` at build time and accessible on both server and client via `useRuntimeConfig().public`.
232
236
 
233
- > **Security:** Only put values here that are safe to expose to the browser. Do not put secrets, tokens, or private keys in `public`. Those should be read directly from `process.env` inside server-only code (loaders, server middleware, API handlers).
237
+ > **Security:** Only put values here that are safe to expose to the browser. Do not put secrets, tokens, or private keys in `public`.
234
238
 
235
- > **Serialization:** Values must be JSON-serializable (strings, numbers, booleans, plain objects, arrays). Functions, class instances, `undefined`, and circular references are not supported and will be lost or throw during the build.
239
+ > **Serialization:** Values must be JSON-serializable (strings, numbers, booleans, plain objects, arrays). Functions, class instances, `undefined`, and circular references are not supported.
236
240
 
237
241
  ```ts
238
242
  // Any page, layout, component, or composable
@@ -252,6 +256,52 @@ import type { RuntimePublicConfig } from '@jasonshimmy/vite-plugin-cer-app/types
252
256
 
253
257
  ---
254
258
 
259
+ ### `runtimeConfig.private`
260
+
261
+ **Type:** `Record<string, string>`
262
+ **Default:** `{}`
263
+
264
+ Server-only secrets. Declare keys with empty-string defaults in `cer.config.ts` for typing purposes. **Private values are never included in the client bundle.**
265
+
266
+ **Environment variable resolution order** (at server startup, for each declared key):
267
+
268
+ 1. `process.env[key]` — exact case (e.g. `process.env.dbUrl`)
269
+ 2. `process.env[UPPER_SNAKE_CASE(key)]` — conventional env var form (e.g. `process.env.DB_URL`)
270
+ 3. The declared default value — used as a last-resort fallback
271
+
272
+ camelCase keys are automatically converted: `dbUrl` → `DB_URL`, `secretKey` → `SECRET_KEY`.
273
+
274
+ ```ts
275
+ // cer.config.ts
276
+ export default defineConfig({
277
+ runtimeConfig: {
278
+ private: {
279
+ dbUrl: '', // resolved from process.env.dbUrl or process.env.DB_URL
280
+ secretKey: '', // resolved from process.env.secretKey or process.env.SECRET_KEY
281
+ },
282
+ },
283
+ })
284
+ ```
285
+
286
+ ```ts
287
+ // app/pages/data.ts — loader (server-only)
288
+ export const loader = async () => {
289
+ const { private: priv } = useRuntimeConfig()
290
+ const rows = await db.query(priv!.dbUrl)
291
+ return { rows }
292
+ }
293
+ ```
294
+
295
+ > `useRuntimeConfig().private` is `undefined` on the client. Only access it in server-only contexts (loaders, server middleware, API handlers).
296
+
297
+ **TypeScript:** Import `RuntimePrivateConfig` to type your private config:
298
+
299
+ ```ts
300
+ import type { RuntimePrivateConfig } from '@jasonshimmy/vite-plugin-cer-app/types'
301
+ ```
302
+
303
+ ---
304
+
255
305
  ## Passing config to the Vite plugin directly
256
306
 
257
307
  When using `vite.config.ts` instead of (or alongside) `cer.config.ts`:
@@ -9,39 +9,35 @@ The framework has two kinds of middleware:
9
9
 
10
10
  ## Route middleware
11
11
 
12
- ### Defining global middleware
12
+ ### Defining middleware
13
13
 
14
- Create a file in `app/middleware/`. It runs before every route navigation:
14
+ Create a file in `app/middleware/`. Export a default `MiddlewareFn` using `defineMiddleware`:
15
15
 
16
16
  ```ts
17
17
  // app/middleware/auth.ts
18
- import type { RouteMiddleware } from '@jasonshimmy/vite-plugin-cer-app/types'
19
-
20
- const auth: RouteMiddleware = async (to, from, next) => {
18
+ export default defineMiddleware(async (to, from) => {
21
19
  const session = await getSession()
22
- if (!session) {
23
- next('/login') // redirect
24
- } else {
25
- next() // allow navigation
26
- }
27
- }
28
-
29
- export default auth
20
+ if (!session) return '/login' // redirect
21
+ return true // allow navigation
22
+ })
30
23
  ```
31
24
 
32
- ### `RouteMiddleware` signature
25
+ `defineMiddleware` is a no-op identity helper — it just provides TypeScript types without
26
+ any runtime overhead. It is auto-imported, so you don't need to import it manually.
27
+
28
+ ### `MiddlewareFn` signature
33
29
 
34
30
  ```ts
35
- type NextFunction = (redirectTo?: string) => void
31
+ type GuardResult = boolean | string | Promise<boolean | string>
36
32
 
37
- type RouteMiddleware = (
38
- to: Route,
39
- from: Route | null,
40
- next: NextFunction,
41
- ) => void | Promise<void>
33
+ type MiddlewareFn = (to: RouteState, from: RouteState | null) => GuardResult
42
34
  ```
43
35
 
44
- Call `next()` to allow navigation, or `next('/path')` to redirect.
36
+ | Return value | Effect |
37
+ |---|---|
38
+ | `true` | Allow navigation |
39
+ | `false` | Block navigation (stay on current route) |
40
+ | `string` | Redirect to that path |
45
41
 
46
42
  ---
47
43
 
@@ -56,28 +52,60 @@ export const meta = {
56
52
  }
57
53
  ```
58
54
 
59
- Named middleware runs in addition to any global middleware.
60
-
61
55
  ---
62
56
 
63
57
  ### Multiple middleware
64
58
 
59
+ Middleware runs in the order listed. The first non-`true` result wins:
60
+
65
61
  ```ts
66
62
  // app/pages/admin.ts
67
63
  export const meta = {
68
64
  middleware: ['auth', 'admin-role'],
69
- // Runs: auth → admin-role → page
65
+ // Runs: auth → admin-role → page render
70
66
  }
71
67
  ```
72
68
 
73
69
  ---
74
70
 
71
+ ### Execution order within a navigation
72
+
73
+ 1. `beforeEnter` fires on the matched route — runs all declared middleware in order
74
+ 2. Route state updates (component renders)
75
+ 3. `afterEnter` fires (analytics, logging)
76
+
77
+ Redirect loop protection: the router stops after 10 consecutive redirects.
78
+
79
+ ---
80
+
81
+ ### Error handling
82
+
83
+ If a middleware function throws (synchronously or asynchronously), navigation is **blocked** — the framework catches the error, logs it, and returns `false` to keep the user on the current route:
84
+
85
+ ```
86
+ [cer-app] Middleware "auth" threw an error: Error: session store unavailable
87
+ ```
88
+
89
+ This means a crashing middleware is always safe: the user stays put rather than landing on a broken page or being incorrectly redirected. Subsequent middleware in the same chain does not run.
90
+
91
+ ---
92
+
93
+ ### TypeScript types
94
+
95
+ `MiddlewareFn` and `GuardResult` are exported from the package if you need them outside of auto-imported files:
96
+
97
+ ```ts
98
+ import type { MiddlewareFn, GuardResult } from '@jasonshimmy/vite-plugin-cer-app/types'
99
+ ```
100
+
101
+ ---
102
+
75
103
  ### All middleware files
76
104
 
77
105
  All files in `app/middleware/` are registered and available by name. They are exported from `virtual:cer-middleware`:
78
106
 
79
107
  ```ts
80
- import middleware from 'virtual:cer-middleware'
108
+ import { middleware } from 'virtual:cer-middleware'
81
109
  // { auth: [Function], 'admin-role': [Function], ... }
82
110
  ```
83
111
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * useCookie() e2e tests — verifies isomorphic cookie read/write/remove
3
+ * works correctly in all rendering modes.
4
+ *
5
+ * Client-side path: set/remove via document.cookie.
6
+ * SSR path: reads from req.headers.cookie on initial request.
7
+ */
8
+
9
+ const mode = Cypress.env('mode') as 'spa' | 'ssr' | 'ssg'
10
+
11
+ // Helper: find an element inside the page-cookie-test shadow DOM (post-hydration).
12
+ const cookiePage = () =>
13
+ cy.get('cer-layout-view').shadow().find('page-cookie-test').shadow()
14
+
15
+ describe('useCookie() — client-side read/write/remove', () => {
16
+ beforeEach(() => {
17
+ cy.clearCookies()
18
+ })
19
+
20
+ it('shows "not set" when the cookie is absent', () => {
21
+ cy.visit('/cookie-test')
22
+ cookiePage().find('[data-cy=cookie-value]').should('contain', 'not set')
23
+ })
24
+
25
+ it('reads a cookie set via cy.setCookie()', () => {
26
+ cy.setCookie('ks-test-cookie', 'cypress-value')
27
+ cy.visit('/cookie-test')
28
+ cookiePage().find('[data-cy=cookie-value]').should('contain', 'cypress-value')
29
+ })
30
+
31
+ it('sets the cookie when the set button is clicked', () => {
32
+ cy.visit('/cookie-test')
33
+ cookiePage().find('[data-cy=set-cookie]').click({ force: true })
34
+ // After reload the SSR-rendered light DOM shows the new value
35
+ cy.get('[data-cy=cookie-value]').should('contain', 'hello-from-cer-app')
36
+ cy.getCookie('ks-test-cookie').should('have.property', 'value', 'hello-from-cer-app')
37
+ })
38
+
39
+ it('removes the cookie when the remove button is clicked', () => {
40
+ cy.setCookie('ks-test-cookie', 'to-remove')
41
+ cy.visit('/cookie-test')
42
+ cookiePage().find('[data-cy=cookie-value]').should('contain', 'to-remove')
43
+ cookiePage().find('[data-cy=remove-cookie]').click({ force: true })
44
+ // After reload the SSR-rendered light DOM shows "not set"
45
+ cy.get('[data-cy=cookie-value]').should('contain', 'not set')
46
+ })
47
+ })
48
+
49
+ if (mode === 'ssr') {
50
+ describe('useCookie() — server-side read (SSR)', () => {
51
+ it('reads the cookie from the request and renders its value', () => {
52
+ cy.setCookie('ks-test-cookie', 'ssr-cookie-value')
53
+ cy.visit('/cookie-test')
54
+ // The SSR-rendered HTML should include the cookie value server-side
55
+ cy.get('[data-cy=cookie-value]').should('contain', 'ssr-cookie-value')
56
+ })
57
+
58
+ it('initial HTML contains the cookie value when cookie is sent with the request', () => {
59
+ cy.setCookie('ks-test-cookie', 'in-html')
60
+ cy.request({
61
+ url: '/cookie-test',
62
+ headers: { Cookie: 'ks-test-cookie=in-html' },
63
+ }).then((response) => {
64
+ expect(response.body).to.include('in-html')
65
+ })
66
+ })
67
+ })
68
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * E2E tests for client-side route middleware (navigation guards).
3
+ *
4
+ * The kitchen-sink app ships an `auth` middleware that redirects unauthenticated
5
+ * users to /login. The /protected page declares `meta: { middleware: ['auth'] }`.
6
+ */
7
+
8
+ describe('Route middleware', () => {
9
+ beforeEach(() => {
10
+ // Ensure no stale auth token from a previous test
11
+ cy.clearLocalStorage()
12
+ })
13
+
14
+ context('unauthenticated navigation', () => {
15
+ it('redirects to /login when visiting /protected without a token', () => {
16
+ cy.visit('/protected')
17
+ cy.url().should('include', '/login')
18
+ })
19
+
20
+ it('shows the login page after redirect', () => {
21
+ cy.visit('/protected')
22
+ cy.get('[data-cy=login-heading]').should('contain', 'Login')
23
+ })
24
+ })
25
+
26
+ context('authenticated navigation', () => {
27
+ beforeEach(() => {
28
+ cy.visit('/')
29
+ cy.window().then((win) => {
30
+ win.localStorage.setItem('ks-token', '1')
31
+ })
32
+ })
33
+
34
+ it('allows navigation to /protected when a token is present', () => {
35
+ cy.visit('/protected')
36
+ cy.url().should('include', '/protected')
37
+ cy.get('[data-cy=protected-heading]').should('contain', 'Protected')
38
+ })
39
+
40
+ it('renders the protected page content', () => {
41
+ cy.visit('/protected')
42
+ cy.get('[data-cy=protected-note]').should('exist')
43
+ })
44
+ })
45
+ })
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Preview server hardening e2e tests — verifies security headers,
3
+ * Cache-Control values, and graceful shutdown behaviour.
4
+ *
5
+ * Security headers and Cache-Control are server-level concerns, so these
6
+ * tests only run in SSR and SSG modes (both use `cer-app preview`).
7
+ * SPA mode also runs through the preview server, so the tests apply there too.
8
+ */
9
+
10
+ describe('Preview server — security headers', () => {
11
+ it('responds with X-Content-Type-Options: nosniff', () => {
12
+ cy.request('/').then((response) => {
13
+ expect(response.headers['x-content-type-options']).to.eq('nosniff')
14
+ })
15
+ })
16
+
17
+ it('responds with X-Frame-Options: DENY', () => {
18
+ cy.request('/').then((response) => {
19
+ expect(response.headers['x-frame-options']).to.eq('DENY')
20
+ })
21
+ })
22
+
23
+ it('responds with Referrer-Policy: strict-origin-when-cross-origin', () => {
24
+ cy.request('/').then((response) => {
25
+ expect(response.headers['referrer-policy']).to.eq('strict-origin-when-cross-origin')
26
+ })
27
+ })
28
+
29
+ it('includes security headers on 404 responses', () => {
30
+ cy.request({ url: '/definitely-not-a-real-page-xyz', failOnStatusCode: false }).then((response) => {
31
+ expect(response.headers['x-content-type-options']).to.eq('nosniff')
32
+ })
33
+ })
34
+ })
35
+
36
+ describe('Preview server — path traversal protection', () => {
37
+ // HTTP clients (including Cypress/got) normalize `..` segments before sending,
38
+ // so a raw traversal like `/../../../../etc/passwd` arrives at the server as
39
+ // `/etc/passwd`. The `isPathBounded` guard is exercised via unit tests
40
+ // (src/__tests__/cli/preview-isr.test.ts). Here we verify the observable
41
+ // guarantee: files that do not exist inside dist/ are never served.
42
+ it('does not serve a file that does not exist inside dist/', () => {
43
+ cy.request({ url: '/etc/passwd', failOnStatusCode: false }).then((response) => {
44
+ // Either 404 (static) or 200 (SSR SPA fallback) — never a served file from
45
+ // outside the dist directory. Critically, the response body must NOT contain
46
+ // typical /etc/passwd content.
47
+ expect(response.body).not.to.include('root:x:')
48
+ expect(response.body).not.to.include('/bin/bash')
49
+ })
50
+ })
51
+
52
+ // NOTE: HTTP clients (including Cypress/got) normalize `..` segments before
53
+ // sending, so raw traversal sequences never arrive at the server unchanged.
54
+ // The isPathBounded() guard is exhaustively covered at the unit level in
55
+ // src/__tests__/cli/preview-isr.test.ts. No additional e2e assertion is needed.
56
+ })
57
+
58
+ describe('Preview server — Cache-Control', () => {
59
+ it('serves HTML with Cache-Control: no-cache', () => {
60
+ cy.request('/').then((response) => {
61
+ expect(response.headers['cache-control']).to.include('no-cache')
62
+ })
63
+ })
64
+
65
+ it('serves content-hashed assets with immutable Cache-Control', () => {
66
+ // Get the page to discover an actual asset URL (Vite hashes asset filenames)
67
+ cy.request('/').then((htmlResponse) => {
68
+ const assetMatch = htmlResponse.body.match(/\/assets\/[^"'\s]+\.js/)
69
+ if (!assetMatch) return // no JS asset found in this page, skip
70
+
71
+ const assetUrl = assetMatch[0]
72
+ cy.request(assetUrl).then((assetResponse) => {
73
+ const cc = assetResponse.headers['cache-control'] as string
74
+ expect(cc).to.include('max-age=31536000')
75
+ expect(cc).to.include('immutable')
76
+ })
77
+ })
78
+ })
79
+ })
@@ -0,0 +1,108 @@
1
+ /**
2
+ * useSeoMeta() e2e tests — verifies Open Graph, Twitter Card, canonical, and
3
+ * title/description tags are injected correctly in all modes.
4
+ *
5
+ * In SSR/SSG: tags must be present in the initial server-rendered HTML.
6
+ * In all modes: tags must be present after client hydration.
7
+ */
8
+
9
+ const mode = Cypress.env('mode') as 'spa' | 'ssr' | 'ssg'
10
+
11
+ describe('useSeoMeta() — title and description', () => {
12
+ it('sets document title', () => {
13
+ cy.visit('/seo-test')
14
+ cy.title().should('eq', 'SEO Test — Kitchen Sink')
15
+ })
16
+
17
+ it('sets meta description', () => {
18
+ cy.visit('/seo-test')
19
+ cy.get('meta[name="description"]').should('have.attr', 'content', 'A test page for useSeoMeta().')
20
+ })
21
+ })
22
+
23
+ describe('useSeoMeta() — Open Graph tags', () => {
24
+ it('sets og:title', () => {
25
+ cy.visit('/seo-test')
26
+ cy.get('meta[property="og:title"]').should('have.attr', 'content', 'SEO Test OG Title')
27
+ })
28
+
29
+ it('sets og:description', () => {
30
+ cy.visit('/seo-test')
31
+ cy.get('meta[property="og:description"]').should('have.attr', 'content', 'SEO Test OG description.')
32
+ })
33
+
34
+ it('sets og:image', () => {
35
+ cy.visit('/seo-test')
36
+ cy.get('meta[property="og:image"]').should('have.attr', 'content', 'https://example.com/og/seo-test.png')
37
+ })
38
+
39
+ it('sets og:url', () => {
40
+ cy.visit('/seo-test')
41
+ cy.get('meta[property="og:url"]').should('have.attr', 'content', 'https://example.com/seo-test')
42
+ })
43
+
44
+ it('sets og:type', () => {
45
+ cy.visit('/seo-test')
46
+ cy.get('meta[property="og:type"]').should('have.attr', 'content', 'website')
47
+ })
48
+
49
+ it('sets og:site_name', () => {
50
+ cy.visit('/seo-test')
51
+ cy.get('meta[property="og:site_name"]').should('have.attr', 'content', 'Kitchen Sink')
52
+ })
53
+ })
54
+
55
+ describe('useSeoMeta() — Twitter Card tags', () => {
56
+ it('sets twitter:card', () => {
57
+ cy.visit('/seo-test')
58
+ cy.get('meta[name="twitter:card"]').should('have.attr', 'content', 'summary_large_image')
59
+ })
60
+
61
+ it('sets twitter:title', () => {
62
+ cy.visit('/seo-test')
63
+ cy.get('meta[name="twitter:title"]').should('have.attr', 'content', 'SEO Test Twitter Title')
64
+ })
65
+
66
+ it('sets twitter:site', () => {
67
+ cy.visit('/seo-test')
68
+ cy.get('meta[name="twitter:site"]').should('have.attr', 'content', '@ks')
69
+ })
70
+ })
71
+
72
+ describe('useSeoMeta() — canonical link', () => {
73
+ it('sets canonical link element', () => {
74
+ cy.visit('/seo-test')
75
+ cy.get('link[rel="canonical"]').should('have.attr', 'href', 'https://example.com/seo-test')
76
+ })
77
+ })
78
+
79
+ if (mode !== 'spa') {
80
+ describe('useSeoMeta() — server-side injection (SSR/SSG)', () => {
81
+ it('title is in the initial HTML', () => {
82
+ cy.request('/seo-test').then((response) => {
83
+ expect(response.body).to.include('<title>SEO Test — Kitchen Sink</title>')
84
+ })
85
+ })
86
+
87
+ it('og:title meta is in the initial HTML', () => {
88
+ cy.request('/seo-test').then((response) => {
89
+ expect(response.body).to.include('og:title')
90
+ expect(response.body).to.include('SEO Test OG Title')
91
+ })
92
+ })
93
+
94
+ it('twitter:card meta is in the initial HTML', () => {
95
+ cy.request('/seo-test').then((response) => {
96
+ expect(response.body).to.include('twitter:card')
97
+ expect(response.body).to.include('summary_large_image')
98
+ })
99
+ })
100
+
101
+ it('canonical link is in the initial HTML', () => {
102
+ cy.request('/seo-test').then((response) => {
103
+ expect(response.body).to.include('canonical')
104
+ expect(response.body).to.include('https://example.com/seo-test')
105
+ })
106
+ })
107
+ })
108
+ }
@@ -1,13 +1,9 @@
1
1
  // Route middleware — redirects to /login if not authenticated.
2
2
  // Set localStorage.setItem('ks-token', '1') to simulate login.
3
- export default (to: any, _from: any, next: (path?: string) => void) => {
3
+ export default defineMiddleware((_to, _from) => {
4
4
  const isLoggedIn = typeof localStorage !== 'undefined'
5
5
  ? !!localStorage.getItem('ks-token')
6
6
  : false
7
7
 
8
- if (!isLoggedIn) {
9
- next('/login')
10
- } else {
11
- next()
12
- }
13
- }
8
+ return isLoggedIn ? true : '/login'
9
+ })
@@ -0,0 +1,22 @@
1
+ component('page-cookie-test', () => {
2
+ const testCookie = useCookie('ks-test-cookie')
3
+
4
+ function setCookie() {
5
+ testCookie.set('hello-from-cer-app')
6
+ window.location.reload()
7
+ }
8
+
9
+ function removeCookie() {
10
+ testCookie.remove()
11
+ window.location.reload()
12
+ }
13
+
14
+ return html`
15
+ <div>
16
+ <h1 data-cy="cookie-test-heading">Cookie Test</h1>
17
+ <p data-cy="cookie-value">Value: ${testCookie.value ?? 'not set'}</p>
18
+ <button data-cy="set-cookie" @click="${setCookie}">Set cookie</button>
19
+ <button data-cy="remove-cookie" @click="${removeCookie}">Remove cookie</button>
20
+ </div>
21
+ `
22
+ })
@@ -0,0 +1,23 @@
1
+ component('page-seo-test', () => {
2
+ useSeoMeta({
3
+ title: 'SEO Test — Kitchen Sink',
4
+ description: 'A test page for useSeoMeta().',
5
+ ogTitle: 'SEO Test OG Title',
6
+ ogDescription: 'SEO Test OG description.',
7
+ ogImage: 'https://example.com/og/seo-test.png',
8
+ ogUrl: 'https://example.com/seo-test',
9
+ ogType: 'website',
10
+ ogSiteName: 'Kitchen Sink',
11
+ twitterCard: 'summary_large_image',
12
+ twitterTitle: 'SEO Test Twitter Title',
13
+ twitterSite: '@ks',
14
+ canonical: 'https://example.com/seo-test',
15
+ })
16
+
17
+ return html`
18
+ <div>
19
+ <h1 data-cy="seo-test-heading">SEO Test</h1>
20
+ <p data-cy="seo-test-description">This page sets SEO tags via <code>useSeoMeta()</code>.</p>
21
+ </div>
22
+ `
23
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [