Add preview handling
This commit is contained in:
193
STRAPI_PREVIEW_SETUP.md
Normal file
193
STRAPI_PREVIEW_SETUP.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Strapi Preview Implementation for Vike App
|
||||||
|
|
||||||
|
This document explains how to set up and use the Strapi Preview feature with this Vike application.
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
✅ **Basic Preview** - Preview draft and published content from Strapi admin
|
||||||
|
✅ **Dynamic Routes** - Support for articles, pages, and other content types
|
||||||
|
✅ **Preview Banner** - Visual indicator when in preview mode
|
||||||
|
✅ **Live Preview Ready** - Communication setup for real-time content updates
|
||||||
|
✅ **Security** - CSP headers for safe iframe embedding
|
||||||
|
✅ **Cache Control** - Preview content bypasses caching
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### 1. Frontend Configuration (Already Done)
|
||||||
|
|
||||||
|
The Vike app is now configured with:
|
||||||
|
|
||||||
|
- **Environment Variables** (`.env`):
|
||||||
|
```bash
|
||||||
|
VITE_CLIENT_URL=https://takerofnotes.com
|
||||||
|
VITE_PREVIEW_SECRET=preview-secret-key-change-this-in-production
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Preview Route**: `/pages/preview/+data.js` and `/pages/preview/+Page.vue`
|
||||||
|
- **Enhanced Strapi Client**: Preview-aware data fetching
|
||||||
|
- **UI Components**: Preview banner and Live Preview communication
|
||||||
|
- **CSP Headers**: Allow embedding from Strapi admin
|
||||||
|
|
||||||
|
### 2. Strapi Configuration (To Do)
|
||||||
|
|
||||||
|
1. **Copy the admin configuration** from `strapi-config-example.js` to your Strapi project at `config/admin.js`
|
||||||
|
|
||||||
|
2. **Add environment variables** to your Strapi `.env`:
|
||||||
|
```bash
|
||||||
|
CLIENT_URL=https://takerofnotes.com
|
||||||
|
PREVIEW_SECRET=preview-secret-key-change-this-in-production
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update the preview secret** in both frontend and Strapi to match
|
||||||
|
|
||||||
|
### 3. Content Type Setup
|
||||||
|
|
||||||
|
The implementation supports these content type patterns:
|
||||||
|
|
||||||
|
- **Global** (`api::global.global`) → `/` (homepage)
|
||||||
|
- **Pages** (`api::page.page`) → `/{slug}`
|
||||||
|
- **Articles** (`api::article.article`) → `/articles/{slug}`
|
||||||
|
|
||||||
|
Add more content types by:
|
||||||
|
1. Updating `getPreviewPathname()` in the Strapi config
|
||||||
|
2. Creating corresponding route files in `/pages/`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Preview Flow
|
||||||
|
|
||||||
|
1. **Strapi Admin** → User clicks "Open Preview" on content
|
||||||
|
2. **Preview Handler** → Generates preview URL with secret and parameters
|
||||||
|
3. **Preview API** → Validates secret and redirects to content page with preview flags
|
||||||
|
4. **Content Page** → Detects preview mode and fetches draft content
|
||||||
|
5. **Preview Banner** → Shows preview status with exit option
|
||||||
|
|
||||||
|
### URL Structure
|
||||||
|
|
||||||
|
Preview URLs follow this pattern:
|
||||||
|
```
|
||||||
|
/preview?secret=xxx&path=/articles/my-article&status=draft&documentId=123&uid=api::article.article
|
||||||
|
```
|
||||||
|
|
||||||
|
Which redirects to:
|
||||||
|
```
|
||||||
|
/articles/my-article?preview=true&status=draft&documentId=123&uid=api::article.article
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── pages/
|
||||||
|
│ ├── preview/ # Preview route handler
|
||||||
|
│ │ ├── +Page.vue
|
||||||
|
│ │ └── +data.js
|
||||||
|
│ ├── @slug/ # Dynamic pages route
|
||||||
|
│ │ ├── +Page.vue
|
||||||
|
│ │ └── +data.js
|
||||||
|
│ ├── articles/@slug/ # Dynamic articles route
|
||||||
|
│ │ ├── +Page.vue
|
||||||
|
│ │ └── +data.js
|
||||||
|
│ ├── index/+data.js # Homepage with preview support
|
||||||
|
│ └── +Layout.vue # Updated with preview components
|
||||||
|
├── components/
|
||||||
|
│ ├── PreviewBanner.vue # Preview mode indicator
|
||||||
|
│ └── LivePreview.vue # Live Preview communication
|
||||||
|
├── libs/
|
||||||
|
│ └── strapi.js # Enhanced with preview support
|
||||||
|
└── strapi-config-example.js # Strapi admin configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Content Types
|
||||||
|
|
||||||
|
To add preview support for a new content type:
|
||||||
|
|
||||||
|
1. **Update Strapi Config** (`config/admin.js`):
|
||||||
|
```js
|
||||||
|
case "api::my-content.my-content":
|
||||||
|
return `/my-content/${slug}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Vike Route**:
|
||||||
|
```
|
||||||
|
pages/my-content/@slug/
|
||||||
|
├── +Page.vue
|
||||||
|
└── +data.js
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Implement Data Fetching**:
|
||||||
|
```js
|
||||||
|
import { getBySlug, getByDocumentId } from '@/libs/strapi'
|
||||||
|
|
||||||
|
export const data = async (pageContext) => {
|
||||||
|
// Extract preview parameters
|
||||||
|
const isPreview = searchParams.preview === 'true'
|
||||||
|
const status = searchParams.status || 'published'
|
||||||
|
|
||||||
|
// Fetch with preview support
|
||||||
|
const response = await getBySlug('my-content', slug, {
|
||||||
|
isPreview,
|
||||||
|
status
|
||||||
|
})
|
||||||
|
|
||||||
|
return { content: response.data, isPreview, status }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- **Preview Secret**: Change the default secret in production
|
||||||
|
- **CSP Headers**: Only allow embedding from your Strapi domain
|
||||||
|
- **Environment Variables**: Use `VITE_` prefix for client-side access
|
||||||
|
- **Validation**: Preview API validates secret before redirecting
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Preview Mode
|
||||||
|
|
||||||
|
1. Add `?preview=true&status=draft` to any URL
|
||||||
|
2. Should see orange preview banner
|
||||||
|
3. Content should be fetched with draft status
|
||||||
|
|
||||||
|
### Test Preview Route
|
||||||
|
|
||||||
|
Visit: `/preview?secret=your-secret&path=/&status=draft`
|
||||||
|
Should redirect to: `/?preview=true&status=draft`
|
||||||
|
|
||||||
|
## Live Preview (Strapi Growth/Enterprise)
|
||||||
|
|
||||||
|
The implementation includes Live Preview communication:
|
||||||
|
|
||||||
|
- **Real-time Updates**: Content changes in Strapi trigger page refresh
|
||||||
|
- **Interactive Editing**: Double-click content to edit (with proper source maps)
|
||||||
|
- **Source Maps**: Enable with `strapi-encode-source-maps` header
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Preview Not Working
|
||||||
|
|
||||||
|
- Check environment variables match between frontend and Strapi
|
||||||
|
- Verify CSP headers allow iframe embedding
|
||||||
|
- Ensure Strapi admin config is properly deployed
|
||||||
|
|
||||||
|
### Content Not Loading
|
||||||
|
|
||||||
|
- Check Strapi API authentication
|
||||||
|
- Verify content type exists and has proper permissions
|
||||||
|
- Check browser network tab for failed requests
|
||||||
|
|
||||||
|
### Preview Banner Not Showing
|
||||||
|
|
||||||
|
- Verify URL contains `preview=true` parameter
|
||||||
|
- Check component import in Layout
|
||||||
|
- Ensure data is being passed correctly
|
||||||
|
|
||||||
|
## Development vs Production
|
||||||
|
|
||||||
|
### Development
|
||||||
|
- Use `http://localhost:3000` as CLIENT_URL
|
||||||
|
- Preview works locally during development
|
||||||
|
|
||||||
|
### Production
|
||||||
|
- Update CLIENT_URL to your production domain
|
||||||
|
- Ensure HTTPS for iframe embedding
|
||||||
|
- Update preview secret for security
|
||||||
64
components/LivePreview.vue
Normal file
64
components/LivePreview.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<!-- This component is invisible but handles Live Preview communication -->
|
||||||
|
<div style="display: none"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const STRAPI_ORIGIN = 'https://cms.takerofnotes.com'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle messages from Strapi's Live Preview
|
||||||
|
*/
|
||||||
|
const handleMessage = async (message) => {
|
||||||
|
const { origin, data } = message
|
||||||
|
|
||||||
|
// Only accept messages from our Strapi instance
|
||||||
|
if (origin !== STRAPI_ORIGIN) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'strapiUpdate':
|
||||||
|
// Content has been updated in Strapi, refresh the page to show changes
|
||||||
|
console.log('Strapi content updated, refreshing preview...')
|
||||||
|
window.location.reload()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'strapiScript':
|
||||||
|
// Inject the Live Preview script for interactive editing
|
||||||
|
console.log('Injecting Live Preview script...')
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.textContent = data.payload.script
|
||||||
|
document.head.appendChild(script)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown message type, ignore
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling Live Preview message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Add event listener for messages from Strapi
|
||||||
|
window.addEventListener('message', handleMessage)
|
||||||
|
|
||||||
|
// Signal to Strapi that we're ready to receive Live Preview scripts
|
||||||
|
try {
|
||||||
|
window.parent?.postMessage({ type: 'previewReady' }, STRAPI_ORIGIN)
|
||||||
|
console.log('Live Preview: Signaled ready to parent window')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error signaling Live Preview readiness:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Clean up event listener
|
||||||
|
window.removeEventListener('message', handleMessage)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
106
components/PreviewBanner.vue
Normal file
106
components/PreviewBanner.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="isPreview" class="preview-banner theme-dark">
|
||||||
|
<div class="preview-banner__content">
|
||||||
|
<div class="preview-banner__info">
|
||||||
|
<span class="preview-banner__label">Preview Mode</span>
|
||||||
|
<span class="preview-banner__status">{{ status }}</span>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
:href="exitPreviewUrl"
|
||||||
|
class="preview-banner__exit"
|
||||||
|
@click="exitPreview"
|
||||||
|
>
|
||||||
|
Exit Preview
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isPreview: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: 'published',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitPreviewUrl = computed(() => {
|
||||||
|
if (typeof window === 'undefined') return '/'
|
||||||
|
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
url.searchParams.delete('preview')
|
||||||
|
url.searchParams.delete('status')
|
||||||
|
url.searchParams.delete('documentId')
|
||||||
|
url.searchParams.delete('uid')
|
||||||
|
return url.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitPreview = (event) => {
|
||||||
|
// For better UX, we can use client-side navigation instead of full page reload
|
||||||
|
event.preventDefault()
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.href = exitPreviewUrl.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.preview-banner {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--theme-accent);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__exit {
|
||||||
|
color: var(--theme-bg);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
104
libs/strapi.js
104
libs/strapi.js
@@ -5,7 +5,105 @@ export const strapiClient = strapi({
|
|||||||
auth: import.meta.env.VITE_STRAPI_API_TOKEN,
|
auth: import.meta.env.VITE_STRAPI_API_TOKEN,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getGlobal = async () => {
|
/**
|
||||||
const globalManager = strapiClient.single('global')
|
* Enhanced fetch function with preview support
|
||||||
return await globalManager.find()
|
* @param {string} contentType - The Strapi content type (e.g., 'global', 'articles')
|
||||||
|
* @param {object} options - Fetch options including preview settings
|
||||||
|
* @param {boolean} options.isPreview - Whether this is a preview request
|
||||||
|
* @param {string} options.status - Content status ('draft' or 'published')
|
||||||
|
* @param {boolean} options.isSingle - Whether this is a single type or collection
|
||||||
|
* @param {object} options.params - Additional query parameters
|
||||||
|
* @returns {Promise} Strapi response
|
||||||
|
*/
|
||||||
|
export const fetchWithPreview = async (contentType, options = {}) => {
|
||||||
|
const {
|
||||||
|
isPreview = false,
|
||||||
|
status = 'published',
|
||||||
|
isSingle = false,
|
||||||
|
params = {},
|
||||||
|
...otherOptions
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const queryParams = { ...params }
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
|
// Add status parameter for draft content in preview mode
|
||||||
|
if (isPreview && status) {
|
||||||
|
queryParams.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable content source maps for Live Preview
|
||||||
|
if (isPreview) {
|
||||||
|
headers['strapi-encode-source-maps'] = 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable caching for preview requests
|
||||||
|
if (isPreview) {
|
||||||
|
headers['Cache-Control'] = 'no-cache'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isSingle) {
|
||||||
|
const manager = strapiClient.single(contentType)
|
||||||
|
return await manager.find(queryParams, { headers, ...otherOptions })
|
||||||
|
} else {
|
||||||
|
const manager = strapiClient.collection(contentType)
|
||||||
|
return await manager.find(queryParams, { headers, ...otherOptions })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching ${contentType}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch global content with preview support
|
||||||
|
*/
|
||||||
|
export const getGlobal = async (previewOptions = {}) => {
|
||||||
|
return await fetchWithPreview('global', {
|
||||||
|
isSingle: true,
|
||||||
|
...previewOptions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a collection item by slug with preview support
|
||||||
|
*/
|
||||||
|
export const getBySlug = async (contentType, slug, previewOptions = {}) => {
|
||||||
|
return await fetchWithPreview(contentType, {
|
||||||
|
isSingle: false,
|
||||||
|
params: {
|
||||||
|
filters: { slug: { $eq: slug } },
|
||||||
|
populate: '*',
|
||||||
|
},
|
||||||
|
...previewOptions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single item by document ID (useful for preview)
|
||||||
|
*/
|
||||||
|
export const getByDocumentId = async (
|
||||||
|
contentType,
|
||||||
|
documentId,
|
||||||
|
previewOptions = {},
|
||||||
|
) => {
|
||||||
|
const { isSingle = false } = previewOptions
|
||||||
|
|
||||||
|
if (isSingle) {
|
||||||
|
return await fetchWithPreview(contentType, {
|
||||||
|
isSingle: true,
|
||||||
|
params: { documentId },
|
||||||
|
...previewOptions,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return await fetchWithPreview(contentType, {
|
||||||
|
isSingle: false,
|
||||||
|
params: {
|
||||||
|
filters: { documentId: { $eq: documentId } },
|
||||||
|
populate: '*',
|
||||||
|
},
|
||||||
|
...previewOptions,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<!-- https://vike.dev/Head -->
|
<!-- https://vike.dev/Head -->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Allow the app to be embedded in iframes from Strapi admin -->
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="frame-ancestors 'self' https://cms.takerofnotes.com"
|
||||||
|
/>
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<lenis root>
|
<lenis root>
|
||||||
|
<preview-banner
|
||||||
|
v-if="data"
|
||||||
|
:is-preview="data.isPreview"
|
||||||
|
:status="data.status"
|
||||||
|
/>
|
||||||
<div :class="classes">
|
<div :class="classes">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<LivePreview v-if="data?.isPreview" />
|
||||||
</lenis>
|
</lenis>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -11,7 +17,12 @@ import '@/styles/main.scss'
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import loadFonts from '@fuzzco/font-loader'
|
import loadFonts from '@fuzzco/font-loader'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
|
import { useData } from 'vike-vue/useData'
|
||||||
import Lenis from '@/components/Lenis.vue'
|
import Lenis from '@/components/Lenis.vue'
|
||||||
|
import PreviewBanner from '@/components/PreviewBanner.vue'
|
||||||
|
import LivePreview from '@/components/LivePreview.vue'
|
||||||
|
|
||||||
|
const data = useData()
|
||||||
|
|
||||||
const { height } = useWindowSize()
|
const { height } = useWindowSize()
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import type { Config } from "vike/types";
|
import type { Config } from 'vike/types'
|
||||||
import vikeVue from "vike-vue/config";
|
import vikeVue from 'vike-vue/config'
|
||||||
|
|
||||||
// Default config (can be overridden by pages)
|
// Default config (can be overridden by pages)
|
||||||
// https://vike.dev/config
|
// https://vike.dev/config
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// https://vike.dev/head-tags
|
title: 'My Vike App',
|
||||||
title: "My Vike App",
|
description: 'Demo showcasing Vike',
|
||||||
description: "Demo showcasing Vike",
|
|
||||||
|
|
||||||
prerender: true,
|
prerender: true,
|
||||||
extends: [vikeVue],
|
extends: [vikeVue],
|
||||||
} as Config;
|
} as Config
|
||||||
|
|||||||
85
pages/@slug/+Page.vue
Normal file
85
pages/@slug/+Page.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<main class="page">
|
||||||
|
<article v-if="page" class="page__content">
|
||||||
|
<header v-if="page.title" class="page__header">
|
||||||
|
<h1 class="page__title">{{ page.title }}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="page.content" class="page__body">
|
||||||
|
<strapi-blocks :content="page.content" />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div v-else class="page-not-found">
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
<p>The requested page could not be found.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useData } from 'vike-vue/useData'
|
||||||
|
|
||||||
|
const { page } = useData()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--theme-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
:deep(h2) {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 2.5rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h3) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 2rem 0 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(p) {
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(ul, ol) {
|
||||||
|
margin: 1.25rem 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-not-found {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 0;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--theme-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--theme-fg-muted, #666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
pages/@slug/+data.js
Normal file
54
pages/@slug/+data.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { getBySlug, getByDocumentId } from '@/libs/strapi'
|
||||||
|
|
||||||
|
export const data = async (pageContext) => {
|
||||||
|
const { routeParams, urlParsed } = pageContext
|
||||||
|
const { slug } = routeParams
|
||||||
|
const searchParams = urlParsed.search || {}
|
||||||
|
|
||||||
|
// Extract preview parameters from URL
|
||||||
|
const isPreview = searchParams.preview === 'true'
|
||||||
|
const status = searchParams.status || 'published'
|
||||||
|
const documentId = searchParams.documentId
|
||||||
|
const uid = searchParams.uid
|
||||||
|
|
||||||
|
try {
|
||||||
|
let page = null
|
||||||
|
|
||||||
|
// If we have a documentId from preview, fetch by document ID
|
||||||
|
if (isPreview && documentId && uid === 'api::page.page') {
|
||||||
|
const response = await getByDocumentId('pages', documentId, {
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
isSingle: false
|
||||||
|
})
|
||||||
|
page = response.data?.[0] || null
|
||||||
|
} else {
|
||||||
|
// Normal fetch by slug
|
||||||
|
const response = await getBySlug('pages', slug, {
|
||||||
|
isPreview,
|
||||||
|
status
|
||||||
|
})
|
||||||
|
page = response.data?.[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
// Pass preview state to the component
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
documentId,
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching page:', error)
|
||||||
|
|
||||||
|
// Return null page but preserve preview state
|
||||||
|
return {
|
||||||
|
page: null,
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
documentId,
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
pages/articles/@slug/+Page.vue
Normal file
104
pages/articles/@slug/+Page.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<main class="article-page">
|
||||||
|
<article v-if="article" class="article">
|
||||||
|
<header class="article__header">
|
||||||
|
<h1 class="article__title">{{ article.title }}</h1>
|
||||||
|
<div v-if="article.publishedAt" class="article__meta">
|
||||||
|
<time :datetime="article.publishedAt">
|
||||||
|
{{ formatDate(article.publishedAt) }}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="article.content" class="article__content">
|
||||||
|
<strapi-blocks :content="article.content" />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div v-else class="article-not-found">
|
||||||
|
<h1>Article not found</h1>
|
||||||
|
<p>The requested article could not be found.</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useData } from 'vike-vue/useData'
|
||||||
|
|
||||||
|
const { article } = useData()
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
return new Date(dateString).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.article-page {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article {
|
||||||
|
&__header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: 1px solid var(--theme-border, #e0e0e0);
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--theme-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
color: var(--theme-fg-muted, #666);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
:deep(h2) {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin: 2rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h3) {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(p) {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(ul, ol) {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-not-found {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 0;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--theme-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--theme-fg-muted, #666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
pages/articles/@slug/+data.js
Normal file
55
pages/articles/@slug/+data.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { getBySlug, getByDocumentId } from '@/libs/strapi'
|
||||||
|
|
||||||
|
export const data = async (pageContext) => {
|
||||||
|
const { routeParams, urlParsed } = pageContext
|
||||||
|
const { slug } = routeParams
|
||||||
|
const searchParams = urlParsed.search || {}
|
||||||
|
|
||||||
|
// Extract preview parameters from URL
|
||||||
|
const isPreview = searchParams.preview === 'true'
|
||||||
|
const status = searchParams.status || 'published'
|
||||||
|
const documentId = searchParams.documentId
|
||||||
|
const uid = searchParams.uid
|
||||||
|
|
||||||
|
try {
|
||||||
|
let article = null
|
||||||
|
|
||||||
|
// If we have a documentId from preview, fetch by document ID
|
||||||
|
// This ensures we get the exact document being previewed
|
||||||
|
if (isPreview && documentId && uid === 'api::article.article') {
|
||||||
|
const response = await getByDocumentId('articles', documentId, {
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
isSingle: false
|
||||||
|
})
|
||||||
|
article = response.data?.[0] || null
|
||||||
|
} else {
|
||||||
|
// Normal fetch by slug
|
||||||
|
const response = await getBySlug('articles', slug, {
|
||||||
|
isPreview,
|
||||||
|
status
|
||||||
|
})
|
||||||
|
article = response.data?.[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
article,
|
||||||
|
// Pass preview state to the component
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
documentId,
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching article:', error)
|
||||||
|
|
||||||
|
// Return null article but preserve preview state for proper UI rendering
|
||||||
|
return {
|
||||||
|
article: null,
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
documentId,
|
||||||
|
uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,28 @@
|
|||||||
import { getGlobal } from '@/libs/strapi'
|
import { getGlobal } from '@/libs/strapi'
|
||||||
|
|
||||||
export const data = async () => {
|
export const data = async (pageContext) => {
|
||||||
const global = await getGlobal()
|
const { urlParsed } = pageContext
|
||||||
|
const searchParams = urlParsed.search || {}
|
||||||
|
|
||||||
return { global: global.data }
|
// Extract preview parameters from URL
|
||||||
|
const isPreview = searchParams.preview === 'true'
|
||||||
|
const status = searchParams.status || 'published'
|
||||||
|
const documentId = searchParams.documentId
|
||||||
|
const uid = searchParams.uid
|
||||||
|
|
||||||
|
// Fetch global content with preview support
|
||||||
|
const global = await getGlobal({
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
...(documentId && { params: { documentId } })
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
global: global.data,
|
||||||
|
// Pass preview state to the component
|
||||||
|
isPreview,
|
||||||
|
status,
|
||||||
|
documentId,
|
||||||
|
uid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
pages/preview-unauthorized/+Page.vue
Normal file
32
pages/preview-unauthorized/+Page.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<div class="unauthorized">
|
||||||
|
<h1>Unauthorized</h1>
|
||||||
|
<p>Invalid preview token. Please check your preview configuration.</p>
|
||||||
|
<p><a href="/">Return to homepage</a></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.unauthorized {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 50vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #dc3545;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
pages/preview/+Page.vue
Normal file
59
pages/preview/+Page.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="preview-redirect">
|
||||||
|
<h1>Preview Handler</h1>
|
||||||
|
|
||||||
|
<div v-if="error" class="error">
|
||||||
|
<h2>Debug Information</h2>
|
||||||
|
<p><strong>Error:</strong> {{ error }}</p>
|
||||||
|
<p v-if="pageContext"><strong>PageContext keys:</strong> {{ pageContext.join(', ') }}</p>
|
||||||
|
<pre v-if="urlParsed">{{ JSON.stringify(urlParsed, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="redirectUrl">
|
||||||
|
<p>Redirecting to preview...</p>
|
||||||
|
<p>If you are not redirected automatically, <a :href="redirectUrl">click here</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p>Processing preview request...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useData } from 'vike-vue/useData'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
|
const data = useData()
|
||||||
|
const { redirectUrl, error, pageContext, urlParsed } = data
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.href = redirectUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview-redirect {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 50vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
pages/preview/+data.js
Normal file
65
pages/preview/+data.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { redirect } from 'vike/abort'
|
||||||
|
|
||||||
|
export const data = async (pageContext) => {
|
||||||
|
try {
|
||||||
|
// Extract URL parameters safely
|
||||||
|
const { urlParsed } = pageContext
|
||||||
|
const searchParams = urlParsed?.search || {}
|
||||||
|
|
||||||
|
console.log('Preview route - pageContext keys:', Object.keys(pageContext))
|
||||||
|
console.log('Preview route - urlParsed:', urlParsed)
|
||||||
|
console.log('Preview route - searchParams:', searchParams)
|
||||||
|
|
||||||
|
const {
|
||||||
|
secret,
|
||||||
|
path = '/',
|
||||||
|
status = 'draft',
|
||||||
|
documentId,
|
||||||
|
uid
|
||||||
|
} = searchParams
|
||||||
|
|
||||||
|
// Validate preview secret
|
||||||
|
if (!secret || secret !== import.meta.env.VITE_PREVIEW_SECRET) {
|
||||||
|
console.log('Invalid secret:', secret, 'Expected:', import.meta.env.VITE_PREVIEW_SECRET)
|
||||||
|
throw redirect('/preview-unauthorized')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the preview URL path (not full URL since we're redirecting within same origin)
|
||||||
|
const previewPath = path || '/'
|
||||||
|
const previewUrl = new URL(previewPath, 'http://localhost')
|
||||||
|
|
||||||
|
// Add preview parameters
|
||||||
|
previewUrl.searchParams.set('preview', 'true')
|
||||||
|
previewUrl.searchParams.set('status', status)
|
||||||
|
|
||||||
|
// Optional: include document metadata for advanced preview functionality
|
||||||
|
if (documentId) {
|
||||||
|
previewUrl.searchParams.set('documentId', documentId)
|
||||||
|
}
|
||||||
|
if (uid) {
|
||||||
|
previewUrl.searchParams.set('uid', uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUrl = previewUrl.pathname + previewUrl.search
|
||||||
|
console.log('Redirecting to:', redirectUrl)
|
||||||
|
|
||||||
|
// Use Vike's redirect
|
||||||
|
throw redirect(redirectUrl)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// If it's a redirect, let it pass through
|
||||||
|
if (error.redirectTo) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Preview data error:', error)
|
||||||
|
|
||||||
|
// For debugging, let's not redirect on error, but show what went wrong
|
||||||
|
return {
|
||||||
|
redirectUrl: null,
|
||||||
|
error: error.message || 'Failed to process preview request',
|
||||||
|
pageContext: Object.keys(pageContext),
|
||||||
|
urlParsed: pageContext.urlParsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
strapi-config-example.js
Normal file
118
strapi-config-example.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Strapi Admin Configuration for Preview Feature
|
||||||
|
*
|
||||||
|
* This file should be placed in your Strapi project at: config/admin.js
|
||||||
|
*
|
||||||
|
* Environment Variables Required in Strapi .env:
|
||||||
|
* CLIENT_URL=https://takerofnotes.com
|
||||||
|
* PREVIEW_SECRET=preview-secret-key-change-this-in-production
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate preview pathname based on content type and document
|
||||||
|
* @param {string} uid - Content type identifier (e.g., 'api::article.article')
|
||||||
|
* @param {object} options - Options containing locale and document data
|
||||||
|
* @returns {string|null} - The preview pathname or null if no preview available
|
||||||
|
*/
|
||||||
|
const getPreviewPathname = (uid, { locale, document }) => {
|
||||||
|
const { slug } = document || {};
|
||||||
|
|
||||||
|
switch (uid) {
|
||||||
|
// Global single type - preview on homepage
|
||||||
|
case "api::global.global":
|
||||||
|
return "/";
|
||||||
|
|
||||||
|
// Generic pages with slugs
|
||||||
|
case "api::page.page":
|
||||||
|
if (!slug) {
|
||||||
|
return null; // No slug, no preview
|
||||||
|
}
|
||||||
|
return `/${slug}`;
|
||||||
|
|
||||||
|
// Articles/blog posts
|
||||||
|
case "api::article.article":
|
||||||
|
if (!slug) {
|
||||||
|
return "/articles"; // Could redirect to articles index
|
||||||
|
}
|
||||||
|
return `/articles/${slug}`;
|
||||||
|
|
||||||
|
// Products (example)
|
||||||
|
case "api::product.product":
|
||||||
|
if (!slug) {
|
||||||
|
return "/products";
|
||||||
|
}
|
||||||
|
return `/products/${slug}`;
|
||||||
|
|
||||||
|
// Add more content types as needed
|
||||||
|
default:
|
||||||
|
// Return null for content types that shouldn't have previews
|
||||||
|
// (e.g., configuration, metadata, components)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main Strapi admin configuration
|
||||||
|
export default ({ env }) => {
|
||||||
|
// Get environment variables
|
||||||
|
const clientUrl = env("CLIENT_URL");
|
||||||
|
const previewSecret = env("PREVIEW_SECRET");
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Other admin configurations can go here
|
||||||
|
// auth: { ... },
|
||||||
|
// apiToken: { ... },
|
||||||
|
|
||||||
|
preview: {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
// Allow preview iframe to load from your frontend domain
|
||||||
|
allowedOrigins: clientUrl,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview handler - generates preview URLs for content
|
||||||
|
* @param {string} uid - Content type UID
|
||||||
|
* @param {object} context - Preview context
|
||||||
|
* @param {string} context.documentId - Document ID being previewed
|
||||||
|
* @param {string} context.locale - Document locale
|
||||||
|
* @param {string} context.status - Document status ('draft' or 'published')
|
||||||
|
* @returns {string|null} - Preview URL or null to disable preview
|
||||||
|
*/
|
||||||
|
async handler(uid, { documentId, locale, status }) {
|
||||||
|
try {
|
||||||
|
// Fetch the complete document from Strapi
|
||||||
|
const document = await strapi.documents(uid).findOne({
|
||||||
|
documentId,
|
||||||
|
// Ensure we get the correct version based on status
|
||||||
|
...(status === 'draft' && { status: 'draft' })
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate the preview pathname
|
||||||
|
const pathname = getPreviewPathname(uid, { locale, document });
|
||||||
|
|
||||||
|
// If no pathname is generated, disable preview for this content type
|
||||||
|
if (!pathname) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build preview URL with all necessary parameters
|
||||||
|
const urlSearchParams = new URLSearchParams({
|
||||||
|
path: pathname,
|
||||||
|
secret: previewSecret,
|
||||||
|
status: status || 'draft',
|
||||||
|
documentId,
|
||||||
|
uid,
|
||||||
|
...(locale && { locale })
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return the complete preview URL pointing to your Vike app's preview page
|
||||||
|
return `${clientUrl}/preview?${urlSearchParams}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Preview handler error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
96
test-preview.html
Normal file
96
test-preview.html
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test Strapi Preview Implementation</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.test-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.test-link:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Strapi Preview Implementation Test</h1>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>1. Test Preview Route (Valid Secret)</h2>
|
||||||
|
<p>This should redirect to the homepage with preview parameters:</p>
|
||||||
|
<a href="/preview?secret=preview-secret-key-change-this-in-production&path=/&status=draft" class="test-link">
|
||||||
|
Test Valid Preview
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>2. Test Preview Route (Invalid Secret)</h2>
|
||||||
|
<p>This should show an unauthorized page:</p>
|
||||||
|
<a href="/preview?secret=wrong-secret&path=/&status=draft" class="test-link">
|
||||||
|
Test Invalid Preview
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>3. Test Preview Mode Directly</h2>
|
||||||
|
<p>This should show the homepage with preview banner:</p>
|
||||||
|
<a href="/?preview=true&status=draft" class="test-link">
|
||||||
|
Test Preview Mode
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>4. Test Article Preview (if you have articles)</h2>
|
||||||
|
<p>This should test dynamic route preview:</p>
|
||||||
|
<a href="/preview?secret=preview-secret-key-change-this-in-production&path=/articles/test-article&status=draft&documentId=123&uid=api::article.article" class="test-link">
|
||||||
|
Test Article Preview
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Expected Strapi Configuration</h2>
|
||||||
|
<p>For reference, your Strapi config should generate URLs like this:</p>
|
||||||
|
<pre><code>https://takerofnotes.com/preview?secret=preview-secret-key-change-this-in-production&path=/articles/my-article&status=draft&documentId=123&uid=api::article.article</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>Environment Variables Check</h2>
|
||||||
|
<p>Make sure these are set in your <code>.env</code> file:</p>
|
||||||
|
<pre>VITE_CLIENT_URL=https://takerofnotes.com
|
||||||
|
VITE_PREVIEW_SECRET=preview-secret-key-change-this-in-production</pre>
|
||||||
|
|
||||||
|
<p>And in your Strapi <code>.env</code> file:</p>
|
||||||
|
<pre>CLIENT_URL=https://takerofnotes.com
|
||||||
|
PREVIEW_SECRET=preview-secret-key-change-this-in-production</pre>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,6 +12,13 @@ export default defineConfig({
|
|||||||
'@': fileURLToPath(new URL('./', import.meta.url)),
|
'@': fileURLToPath(new URL('./', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
// Allow the app to be embedded in iframes from Strapi admin
|
||||||
|
'Content-Security-Policy':
|
||||||
|
"frame-ancestors 'self' https://cms.takerofnotes.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
|||||||
Reference in New Issue
Block a user