strapi integration

This commit is contained in:
nicwands 2026-06-08 14:45:19 -04:00
parent ea19ac6189
commit 5c38cd9bdd
12 changed files with 264 additions and 144 deletions

View file

@ -1,7 +1,9 @@
<template> <template>
<lenis root :options="{ duration: 1 }"> <lenis root :options="{ duration: 1 }">
<div :class="classes" :style="styles" id="container"> <div :class="classes" :style="styles" id="container">
<layout /> <suspense>
<layout />
</suspense>
</div> </div>
</lenis> </lenis>
</template> </template>

3
src/components.d.ts vendored
View file

@ -11,6 +11,8 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Dl: typeof import('./components/Dl.vue')['default']
DlRow: typeof import('./components/DlRow.vue')['default']
InfiniteScrollContainer: typeof import('./components/InfiniteScrollContainer.vue')['default'] InfiniteScrollContainer: typeof import('./components/InfiniteScrollContainer.vue')['default']
InfiniteScrollDemo: typeof import('./components/InfiniteScrollDemo.vue')['default'] InfiniteScrollDemo: typeof import('./components/InfiniteScrollDemo.vue')['default']
Layout: typeof import('./components/Layout.vue')['default'] Layout: typeof import('./components/Layout.vue')['default']
@ -20,5 +22,6 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SiteHeader: typeof import('./components/site/Header.vue')['default'] SiteHeader: typeof import('./components/site/Header.vue')['default']
StrapiContent: typeof import('./components/strapi/Content.vue')['default']
} }
} }

28
src/components/DlRow.vue Normal file
View file

@ -0,0 +1,28 @@
<template>
<div v-if="row" class="dl-row">
<span class="label">{{ row.label }}:</span>
<strapi-content class="content" :content="row.content" />
</div>
</template>
<script setup>
const props = defineProps({ row: Object })
</script>
<style lang="scss">
.dl-row {
grid-template-columns: desktop-vw(40px) 1fr;
display: grid;
align-items: center;
padding-top: desktop-vw(8px);
border-top: 1px dashed currentColor;
.content {
p,
a {
@include h6;
}
}
}
</style>

View file

@ -1,6 +1,8 @@
<template> <template>
<div class="layout"> <div class="layout">
<site-header /> <suspense>
<site-header />
</suspense>
<main class="main-content"> <main class="main-content">
<infinite-scroll-demo /> <infinite-scroll-demo />
@ -9,14 +11,11 @@
</template> </template>
<script setup> <script setup>
import { useSeoMeta } from '@unhead/vue' import { useStrapiSingle } from '@/composables/useStrapi'
import useStrapiHead from '@/composables/useStrapiHead'
// SEO const { data } = await useStrapiSingle('home')
useSeoMeta({ useStrapiHead()
title: 'nicwands.com - Infinite Grid Gallery',
description:
'Interactive infinite scrolling grid gallery with bidirectional content generation',
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,25 +1,20 @@
<template> <template>
<header class="site-header"> <header v-if="data" class="site-header">
<img class="logo" src="/images/output_pixelated.gif" /> <img class="logo" src="/images/output_pixelated.gif" />
<div class="info theme-red"> <div class="info theme-red">
<p class="h6">Nic Wands</p> <strapi-content class="copy" :content="data.header_copy" />
<p class="description p-small">
Amet proin sit eleifend enim. Sit cursus nec vel donec odio
nullam placerat.
</p>
<hr /> <dl-row v-for="row in data.header_dl" :row="row" />
<dl>
<dt>Contact:</dt>
<dd>nic@nicwands.com</dd>
</dl>
</div> </div>
</header> </header>
</template> </template>
<script setup></script> <script setup>
import { useStrapiGlobal } from '@/composables/useStrapi'
const { data } = await useStrapiGlobal()
</script>
<style lang="scss"> <style lang="scss">
.site-header { .site-header {
@ -48,11 +43,16 @@
border-radius: desktop-vw(10px); border-radius: desktop-vw(10px);
padding: desktop-vw(12px) desktop-vw(10px); padding: desktop-vw(12px) desktop-vw(10px);
.description { .copy {
margin: desktop-vw(6px) 0 desktop-vw(18px); margin-bottom: desktop-vw(18px);
}
hr { strong {
border: 1px dashed currentColor; @include h6;
}
p {
margin: desktop-vw(6px) 0;
@include p-small;
}
} }
} }
} }

View file

@ -0,0 +1,24 @@
<template>
<div class="strapi-content">
<strapi-blocks :content="content" />
</div>
</template>
<script setup>
import { StrapiBlocks } from 'vue-strapi-blocks-renderer'
const props = defineProps({
content: Array,
})
</script>
<style lang="scss">
.strapi-content {
> *:first-child {
margin-top: 0 !important;
}
> *:last-child {
margin-bottom: 0 !important;
}
}
</style>

View file

@ -0,0 +1,142 @@
import { inject, ref } from 'vue'
// Base composable to access the Strapi client
const getStrapiClient = () => {
const client = inject('strapi')
if (!client)
throw new Error(
'Strapi client not initialized. Did you install the plugin?',
)
return client
}
// Merge default and user options
const mergeOptions = (userOptions = {}) => {
return { pLevel: '5', ...userOptions }
}
// Singleton instance for global data
let globalInstance = null
/**
* Use the global singleton data with caching
* This composable ensures the global data is only fetched once,
* even when called from multiple places
* @param {object} options - optional query options
*/
export const useStrapiGlobal = async (options = {}) => {
// Return existing instance if already fetched
if (globalInstance) {
return globalInstance
}
// Create new instance
const data = ref(null)
const loading = ref(true)
const error = ref(null)
const client = getStrapiClient()
// Store the instance before making the request
globalInstance = { data, loading, error }
try {
const response = await client
.single('global')
.find(mergeOptions(options))
data.value = response.data
} catch (err) {
error.value = err
console.error(err)
// Reset instance on error so it can be retried
globalInstance = null
} finally {
loading.value = false
}
return globalInstance
}
/**
* Reset the global singleton cache
* Useful for forcing a refetch of global data
*/
export const resetStrapiGlobal = () => {
globalInstance = null
}
/**
* Use a singleton content type
* @param {string} uid - Strapi singleton UID
* @param {object} options - optional query options
*/
export const useStrapiSingle = async (uid, options = {}) => {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
const client = getStrapiClient()
try {
const response = await client.single(uid).find(mergeOptions(options))
data.value = response.data
} catch (err) {
error.value = err
console.error(err)
} finally {
loading.value = false
}
return { data, loading, error }
}
/**
* Use a single entry by ID
* @param {string} contentType
* @param {number|string} id
* @param {object} options - optional query options
*/
export const useStrapiOne = async (contentType, id, options = {}) => {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
const client = getStrapiClient()
try {
const response = await client
.entity(contentType)
.findOne({ id, ...mergeOptions(options) })
data.value = response.data
} catch (err) {
error.value = err
console.error(err)
} finally {
loading.value = false
}
return { data, loading, error }
}
/**
* Use a collection of entries
* @param {string} contentType
* @param {object} options - optional query options
*/
export const useStrapiMany = async (contentType, options = {}) => {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
const client = getStrapiClient()
try {
const response = await client
.entity(contentType)
.find(mergeOptions(options))
data.value = response.data
} catch (err) {
error.value = err
console.error(err)
} finally {
loading.value = false
}
return { data, loading, error }
}

View file

@ -0,0 +1,24 @@
import { computed } from 'vue'
import { useSeoMeta } from '@unhead/vue'
import { useStrapiGlobal } from '@/composables/useStrapi'
export default (pageSeo) => {
const { data: globalData } = useStrapiGlobal()
const globalSeo = computed(() => globalData?.value?.seo)
const title = computed(() => {
return pageSeo?.title || globalSeo.value?.title || ''
})
const description = computed(() => {
return pageSeo?.description || globalSeo.value?.description || ''
})
const image = computed(() => {
return pageSeo?.image || globalSeo.value?.image
})
useSeoMeta({
title: title.value,
description: description.value,
})
}

View file

@ -1,109 +0,0 @@
import { strapi } from '@strapi/client'
export const strapiClient = strapi({
baseURL: 'https://cms.nicwands.com/api',
auth: import.meta.env.VITE_STRAPI_API_TOKEN,
})
/**
* Enhanced fetch function with preview support
* @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,
})
}
}

View file

@ -1,7 +1,9 @@
import './styles/main.scss' import './styles/main.scss'
import App from './App.vue' import App from './App.vue'
import { ViteSSG } from 'vite-ssg/single-page' import { ViteSSG } from 'vite-ssg/single-page'
import strapi from './plugins/strapi'
export const createApp = ViteSSG(App, ({ app }) => { export const createApp = ViteSSG(App, ({ app }) => {
// Plugins // Plugins
app.use(strapi)
}) })

13
src/plugins/strapi.js Normal file
View file

@ -0,0 +1,13 @@
import { strapi } from '@strapi/client'
export default {
install: (app) => {
const client = strapi({
baseURL: 'https://cms.nicwands.com/api',
auth: import.meta.env.VITE_STRAPI_API_TOKEN,
})
// Also provide
app.provide('strapi', client)
},
}

View file

@ -61,14 +61,6 @@ code {
@include code; @include code;
} }
/* DL styling */
dl {
grid-template-columns: desktop-vw(40px) 1fr;
display: grid;
align-items: center;
padding-top: desktop-vw(8px);
}
#app { #app {
min-height: 100vh; min-height: 100vh;
} }