@studio-fes/layer-strapi 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.
- package/.nuxtrc +1 -0
- package/CHANGELOG.md +7 -0
- package/README.md +69 -0
- package/app/components/StrapiImage.vue +18 -0
- package/app/components/StrapiRichText.vue +47 -0
- package/app/composables/useStrapiPage.ts +18 -0
- package/app/composables/useStrapiSEO.ts +49 -0
- package/app/types/StrapiRichText.d.ts +52 -0
- package/app/types/graphql-operations.ts +107 -0
- package/app/utils/check-page-data.ts +76 -0
- package/app/utils/parse-strapi-image.ts +25 -0
- package/app/utils/render-strapi-richtext.ts +97 -0
- package/eslint.config.js +74 -0
- package/graphql.config.ts +4 -0
- package/nuxt.config.ts +23 -0
- package/package.json +38 -0
- package/queries/.gitkeep +0 -0
- package/schema.graphql +0 -0
- package/server/graphqlMiddleware.serverOptions.ts +32 -0
- package/server/multiCache.serverOprtions.ts +22 -0
- package/tsconfig.json +3 -0
package/.nuxtrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
typescript.includeWorkspace = true
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Nuxt Layer Starter
|
|
2
|
+
|
|
3
|
+
Create Nuxt extendable layer with this GitHub template.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Make sure to install the dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Working on your layer
|
|
14
|
+
|
|
15
|
+
Your layer is at the root of this repository, it is exactly like a regular Nuxt project, except you can publish it on NPM.
|
|
16
|
+
|
|
17
|
+
## Distributing your layer
|
|
18
|
+
|
|
19
|
+
Your Nuxt layer is shaped exactly the same as any other Nuxt project, except you can publish it on NPM.
|
|
20
|
+
|
|
21
|
+
To do so, you only have to check if `files` in `package.json` are valid, then run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm publish --access public
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Once done, your users will only have to run:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --save your-layer
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then add the dependency to their `extends` in `nuxt.config`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
defineNuxtConfig({
|
|
37
|
+
extends: 'your-layer'
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Development Server
|
|
42
|
+
|
|
43
|
+
Start the development server on http://localhost:3000
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Production
|
|
50
|
+
|
|
51
|
+
Build the application for production:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or statically generate it with:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm generate
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Locally preview production build:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pnpm preview
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Checkout the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NuxtImgProps } from '@studio-fes/layer-base/app/components/Image.vue'
|
|
3
|
+
import type { StrapiFileFragment } from '~/types/graphql-operations'
|
|
4
|
+
|
|
5
|
+
interface CraftImageProps {
|
|
6
|
+
image?: StrapiFileFragment
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const props = defineProps<NuxtImgProps & CraftImageProps>()
|
|
10
|
+
const imageProps = computed(() => ({
|
|
11
|
+
...props,
|
|
12
|
+
...parseStrapiImage(props.image),
|
|
13
|
+
}))
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<Image v-if="imageProps" v-bind="imageProps" />
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type * as StrapiRichText from '~/types/StrapiRichText'
|
|
5
|
+
|
|
6
|
+
export type StrapiRichTextBlocks = StrapiRichText.BlockNode[]
|
|
7
|
+
|
|
8
|
+
export default defineComponent({
|
|
9
|
+
props: {
|
|
10
|
+
blocks: { type: Array as PropType<StrapiRichTextBlocks>, default: () => [] },
|
|
11
|
+
inline: { type: Boolean, default: false },
|
|
12
|
+
tag: { type: String, default: undefined },
|
|
13
|
+
},
|
|
14
|
+
setup(props) {
|
|
15
|
+
return () => {
|
|
16
|
+
const tag = props.tag || (props.inline ? 'p' : 'div')
|
|
17
|
+
const children = renderStrapiRichText(props.blocks, props.inline)
|
|
18
|
+
const selector = props.inline ? 'inline-richtext' : 'richtext'
|
|
19
|
+
return h(tag, {
|
|
20
|
+
class: selector,
|
|
21
|
+
}, children)
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
[class*='richtext'] {
|
|
29
|
+
&:not(.inline-richtext) {
|
|
30
|
+
display: block;
|
|
31
|
+
|
|
32
|
+
:deep(br) {
|
|
33
|
+
display: block;
|
|
34
|
+
height: 1em;
|
|
35
|
+
line-height: 1;
|
|
36
|
+
|
|
37
|
+
content: '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&.inline-richtext {
|
|
42
|
+
:deep(span) {
|
|
43
|
+
display: block;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function useStrapiPage<T>(queryName: never, variables?: Record<string, unknown>): Promise<T> {
|
|
2
|
+
const nuxtApp = useNuxtApp()
|
|
3
|
+
|
|
4
|
+
// @ts-expect-error i18n is not used yet
|
|
5
|
+
const locale = '$i18n' in nuxtApp ? [nuxtApp.$i18n?.locale?.value] : undefined
|
|
6
|
+
|
|
7
|
+
// @ts-expect-error queryName is not used yet
|
|
8
|
+
const response = await useAsyncGraphqlQuery(queryName, {
|
|
9
|
+
locale,
|
|
10
|
+
status: 'PUBLISHED',
|
|
11
|
+
graphqlCaching: { client: true },
|
|
12
|
+
...variables,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
checkPageData(response)
|
|
16
|
+
|
|
17
|
+
return response.data.value?.data as T
|
|
18
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MetaSocialFragment, SeoMetaFragment } from '~/types/graphql-operations'
|
|
2
|
+
|
|
3
|
+
export function useStrapiSEO(seoData?: MaybeRefOrGetter<SeoMetaFragment>) {
|
|
4
|
+
const nuxtApp = useNuxtApp()
|
|
5
|
+
const runtimeConfig = useRuntimeConfig()
|
|
6
|
+
|
|
7
|
+
const seo = computed(() => {
|
|
8
|
+
if (!seoData)
|
|
9
|
+
return {}
|
|
10
|
+
|
|
11
|
+
const { metaTitle, metaDescription, metaImage, metaSocial, metaRobots, keywords } = toValue(seoData)
|
|
12
|
+
|
|
13
|
+
const metaSocials = (metaSocial as MetaSocialFragment[]).map((social: MetaSocialFragment) => {
|
|
14
|
+
let key = null
|
|
15
|
+
|
|
16
|
+
switch (social.socialNetwork) {
|
|
17
|
+
case 'Facebook':
|
|
18
|
+
key = 'og'
|
|
19
|
+
break
|
|
20
|
+
case 'Twitter':
|
|
21
|
+
key = 'twitter'
|
|
22
|
+
break
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!key)
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
{ property: `${key}:title`, content: social.title || metaTitle },
|
|
30
|
+
{ property: `${key}:description`, content: social.description || metaDescription },
|
|
31
|
+
{ property: `${key}:image`, content: runtimeConfig.public.imageDomain + (social.image || metaImage)?.url },
|
|
32
|
+
{ property: `${key}:image:alt`, content: (social.image || metaImage)?.alternativeText || metaTitle },
|
|
33
|
+
]
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
title: metaTitle,
|
|
38
|
+
meta: [
|
|
39
|
+
{ name: 'description', content: metaDescription },
|
|
40
|
+
{ name: 'robots', content: metaRobots },
|
|
41
|
+
{ name: 'keywords', content: keywords },
|
|
42
|
+
{ property: 'twitter:card', content: 'summary_large_image' },
|
|
43
|
+
...metaSocials.flat(),
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
nuxtApp.runWithContext(() => useHead(seo.value))
|
|
49
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// import type { StrapiFileFragment } from '#graphql-operations'
|
|
2
|
+
import type { StrapiFileFragment } from '~/types/graphql-operations'
|
|
3
|
+
|
|
4
|
+
export interface HeadingNode {
|
|
5
|
+
type: 'heading'
|
|
6
|
+
level: 1 | 2 | 3 | 4 | 5 | 6
|
|
7
|
+
children: (TextNode | LinkNode)[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParagraphNode {
|
|
11
|
+
type: 'paragraph'
|
|
12
|
+
children: (TextNode | LinkNode)[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TextNode {
|
|
16
|
+
text: string
|
|
17
|
+
type: 'text'
|
|
18
|
+
bold?: boolean
|
|
19
|
+
underline?: boolean
|
|
20
|
+
italic?: boolean
|
|
21
|
+
strikethrough?: boolean
|
|
22
|
+
code?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LinkNode {
|
|
26
|
+
url: string
|
|
27
|
+
type: 'link'
|
|
28
|
+
children: TextNode[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ListNode {
|
|
32
|
+
type: 'list'
|
|
33
|
+
format: 'unordered' | 'ordered'
|
|
34
|
+
children: ListItemNode[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ListItemNode {
|
|
38
|
+
type: 'list-item'
|
|
39
|
+
children: (TextNode | LinkNode | ListNode)[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface QuoteNode {
|
|
43
|
+
type: 'quote'
|
|
44
|
+
children: (TextNode | LinkNode)[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ImageNode {
|
|
48
|
+
type: 'image'
|
|
49
|
+
image: StrapiFileFragment
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type BlockNode = HeadingNode | ParagraphNode | TextNode | LinkNode | ListNode | ListItemNode | QuoteNode | ImageNode
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @example
|
|
3
|
+
* ```graphql
|
|
4
|
+
* enum ENUM_COMPONENTSHAREDMETASOCIAL_SOCIALNETWORK {
|
|
5
|
+
* Facebook
|
|
6
|
+
* Twitter
|
|
7
|
+
* }
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export type EnumComponentsharedmetasocialSocialnetwork = 'Facebook' | 'Twitter'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```graphql
|
|
16
|
+
* fragment EntryInterfaceType {
|
|
17
|
+
* id
|
|
18
|
+
* __typename
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface EntryInterfaceTypeFragment {
|
|
23
|
+
__typename: string
|
|
24
|
+
/** The ID of the entity */
|
|
25
|
+
documentId?: string | number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```graphql
|
|
32
|
+
* fragment SeoMeta on ComponentSharedSeo {
|
|
33
|
+
* id
|
|
34
|
+
* metaTitle
|
|
35
|
+
* metaDescription
|
|
36
|
+
* metaImage {
|
|
37
|
+
* ...StrapiFile
|
|
38
|
+
* }
|
|
39
|
+
* metaSocial {
|
|
40
|
+
* ...MetaSocial
|
|
41
|
+
* }
|
|
42
|
+
* keywords
|
|
43
|
+
* metaRobots
|
|
44
|
+
* structuredData
|
|
45
|
+
* metaViewport
|
|
46
|
+
* canonicalURL
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export interface SeoMetaFragment {
|
|
51
|
+
canonicalURL?: string
|
|
52
|
+
id: string | number
|
|
53
|
+
keywords?: string
|
|
54
|
+
metaDescription: string
|
|
55
|
+
metaImage: StrapiFileFragment
|
|
56
|
+
metaRobots?: string
|
|
57
|
+
metaSocial?: (MetaSocialFragment | null)[]
|
|
58
|
+
metaTitle: string
|
|
59
|
+
metaViewport?: string
|
|
60
|
+
structuredData?: any
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```graphql
|
|
67
|
+
* fragment StrapiFile on UploadFile {
|
|
68
|
+
* alternativeText
|
|
69
|
+
* height
|
|
70
|
+
* width
|
|
71
|
+
* url
|
|
72
|
+
* mime
|
|
73
|
+
* ext
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export interface StrapiFileFragment {
|
|
78
|
+
alternativeText?: string
|
|
79
|
+
ext?: string
|
|
80
|
+
height?: number
|
|
81
|
+
mime: string
|
|
82
|
+
url: string
|
|
83
|
+
width?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```graphql
|
|
90
|
+
* fragment MetaSocial on ComponentSharedMetaSocial {
|
|
91
|
+
* description
|
|
92
|
+
* id
|
|
93
|
+
* socialNetwork
|
|
94
|
+
* title
|
|
95
|
+
* image {
|
|
96
|
+
* ...StrapiFile
|
|
97
|
+
* }
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export interface MetaSocialFragment {
|
|
102
|
+
description: string
|
|
103
|
+
id: string | number
|
|
104
|
+
image?: StrapiFileFragment
|
|
105
|
+
socialNetwork: EnumComponentsharedmetasocialSocialnetwork
|
|
106
|
+
title: string
|
|
107
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// import type { EntryQuery } from '#graphql-operations'
|
|
2
|
+
import type { GraphqlResponse } from '#nuxt-graphql-middleware/response'
|
|
3
|
+
import type { NuxtError } from 'nuxt/app'
|
|
4
|
+
import type { EntryInterfaceTypeFragment } from '~/types/graphql-operations'
|
|
5
|
+
|
|
6
|
+
import { createError } from 'nuxt/app'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Throws a 404 error with a specified message.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} [message] - The error message to be displayed.
|
|
12
|
+
* @throws {Error} Throws an error with a status code of 404.
|
|
13
|
+
* @returns {never} This function does not return a value; it always throws an error.
|
|
14
|
+
*/
|
|
15
|
+
export function throw404(
|
|
16
|
+
message: string = 'Page Not Found.',
|
|
17
|
+
): never {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 404,
|
|
20
|
+
statusMessage: message,
|
|
21
|
+
fatal: true,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Throws a 404 error with a specified message.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} [message] - The error message to be displayed.
|
|
29
|
+
* @throws {Error} Throws an error with a status code of 404.
|
|
30
|
+
* @returns {never} This function does not return a value; it always throws an error.
|
|
31
|
+
*/
|
|
32
|
+
export function throw500(
|
|
33
|
+
message: string = 'Server Error',
|
|
34
|
+
): never {
|
|
35
|
+
throw createError({
|
|
36
|
+
statusCode: 500,
|
|
37
|
+
statusMessage: message,
|
|
38
|
+
fatal: true,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checks for errors in an AsyncData response from a GraphQL query and throws appropriate HTTP errors.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* This function performs validation on the response of an asynchronous GraphQL query and throws
|
|
47
|
+
* specific errors based on the validation outcome. It checks for missing data, GraphQL errors,
|
|
48
|
+
* and server errors in a specific order.
|
|
49
|
+
*
|
|
50
|
+
* @param response - The AsyncData object containing either GraphQL response data or a NuxtError
|
|
51
|
+
*
|
|
52
|
+
* @throws {Error} Throws a 500 error if:
|
|
53
|
+
* - No response is provided
|
|
54
|
+
* - The response contains an error value
|
|
55
|
+
* - The GraphQL response contains errors in the errors array
|
|
56
|
+
*
|
|
57
|
+
* @throws {Error} Throws a 404 error if the response.data.value is undefined or null
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const response = await useAsyncData('query', () => fetchGraphQL());
|
|
62
|
+
* checkPageData(response);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function checkPageData(response: Awaited<ReturnType<typeof useAsyncData<GraphqlResponse<EntryInterfaceTypeFragment> | undefined, NuxtError | undefined>>>): void {
|
|
66
|
+
if (!response) {
|
|
67
|
+
throw500('No data returned from the server.')
|
|
68
|
+
}
|
|
69
|
+
if (response.error.value || response.data.value?.errors?.length) {
|
|
70
|
+
throw500(response.error.value?.statusMessage || response.error.value?.message || response.data.value?.errors?.map((e: any) => e.message).join(', ') || 'Unknown error occurred.')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!response.data.value?.data) {
|
|
74
|
+
throw404()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// import type { StrapiFileFragment } from '#graphql-operations'
|
|
2
|
+
import type { NuxtImgProps } from '@studio-fes/layer-base/app/components/Image.vue'
|
|
3
|
+
import type { StrapiFileFragment } from '~/types/graphql-operations'
|
|
4
|
+
|
|
5
|
+
export function parseStrapiImage(image: StrapiFileFragment | undefined): NuxtImgProps | null {
|
|
6
|
+
if (!image)
|
|
7
|
+
return null
|
|
8
|
+
|
|
9
|
+
const runtimeConfig = useRuntimeConfig()
|
|
10
|
+
const { width, height, url: src, alternativeText: alt } = image
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
src: runtimeConfig.public.imageDomain + src,
|
|
16
|
+
alt,
|
|
17
|
+
format: 'webp',
|
|
18
|
+
} as {
|
|
19
|
+
width: number
|
|
20
|
+
height: number
|
|
21
|
+
src: string
|
|
22
|
+
alt: string
|
|
23
|
+
format?: string
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type * as RichText from '~/types/StrapiRichText'
|
|
2
|
+
|
|
3
|
+
import { Comment, Fragment } from 'vue'
|
|
4
|
+
|
|
5
|
+
function renderParagraphBlock(block: RichText.ParagraphNode, inline?: boolean) {
|
|
6
|
+
const children = renderStrapiRichText(block.children, inline)
|
|
7
|
+
const tag = inline ? 'fragment' : 'p'
|
|
8
|
+
return children.length ? h(tag, children) : h('br')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function renderTextBlock(block: RichText.TextNode) {
|
|
12
|
+
const lines = block.text.split('\n')
|
|
13
|
+
const children = lines.reduce((acc, line, i) => {
|
|
14
|
+
if (i < lines.length - 1) {
|
|
15
|
+
acc.push(`${line} `.replace(/\s+/g, ' '), h('br'))
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
acc.push(line)
|
|
19
|
+
}
|
|
20
|
+
return acc
|
|
21
|
+
}, [] as (string | VNode)[])
|
|
22
|
+
|
|
23
|
+
if (block.bold)
|
|
24
|
+
return h('strong', children)
|
|
25
|
+
if (block.code)
|
|
26
|
+
return h('code', children)
|
|
27
|
+
if (block.italic)
|
|
28
|
+
return h('em', children)
|
|
29
|
+
if (block.strikethrough)
|
|
30
|
+
return h('span', { class: 'line-through' }, children)
|
|
31
|
+
if (block.underline)
|
|
32
|
+
return h('span', { class: 'underline' }, children)
|
|
33
|
+
return h(Fragment, children)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderInlineBlock(block: RichText.BlockNode): VNode {
|
|
37
|
+
switch (block.type) {
|
|
38
|
+
case 'heading':
|
|
39
|
+
case 'list-item':
|
|
40
|
+
case 'quote':
|
|
41
|
+
return h('span', renderStrapiRichText(block.children, true))
|
|
42
|
+
case 'image':
|
|
43
|
+
return h(Fragment)
|
|
44
|
+
case 'link':
|
|
45
|
+
return h('a', { href: block.url, target: '_blank', rel: 'noopener noreferrer' }, renderStrapiRichText(block.children, true))
|
|
46
|
+
case 'list':
|
|
47
|
+
return h(Fragment, renderStrapiRichText(block.children, true))
|
|
48
|
+
case 'paragraph':
|
|
49
|
+
return renderParagraphBlock(block, true)
|
|
50
|
+
case 'text':
|
|
51
|
+
return renderTextBlock(block)
|
|
52
|
+
default:
|
|
53
|
+
return h(Comment, 'Unknown block type')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderBlock(block: RichText.BlockNode): VNode {
|
|
58
|
+
switch (block.type) {
|
|
59
|
+
case 'heading':
|
|
60
|
+
return h(`h${block.level}`, renderStrapiRichText(block.children))
|
|
61
|
+
case 'image':
|
|
62
|
+
return h('img', { src: block.image.url, width: block.image.width, height: block.image.height, alt: block.image.alternativeText })
|
|
63
|
+
case 'link':
|
|
64
|
+
return h('a', { title: getPlainText(block.children), href: block.url, target: '_blank', rel: 'noopener noreferrer' }, h('span', renderStrapiRichText(block.children)))
|
|
65
|
+
case 'list':
|
|
66
|
+
return h(block.format === 'ordered' ? 'ol' : 'ul', renderStrapiRichText(block.children))
|
|
67
|
+
case 'list-item':
|
|
68
|
+
return h('li', renderStrapiRichText(block.children))
|
|
69
|
+
case 'paragraph':
|
|
70
|
+
return renderParagraphBlock(block)
|
|
71
|
+
case 'quote':
|
|
72
|
+
return h('blockquote', renderStrapiRichText(block.children))
|
|
73
|
+
case 'text':
|
|
74
|
+
return renderTextBlock(block)
|
|
75
|
+
default:
|
|
76
|
+
return h(Comment, 'Unknown block type')
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function renderStrapiRichText(blocks: RichText.BlockNode[], inline?: boolean): VNode[] {
|
|
81
|
+
const render = inline ? renderInlineBlock : renderBlock
|
|
82
|
+
return blocks.map(render)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getPlainText(block: RichText.BlockNode[]) {
|
|
86
|
+
if (!block)
|
|
87
|
+
return ''
|
|
88
|
+
|
|
89
|
+
const text: string = block.reduce((acc, node) => {
|
|
90
|
+
if (node.type === 'text') {
|
|
91
|
+
return acc + node.text
|
|
92
|
+
}
|
|
93
|
+
return acc + getPlainText(node.children)
|
|
94
|
+
}, '')
|
|
95
|
+
|
|
96
|
+
return text
|
|
97
|
+
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import antfu from '@antfu/eslint-config'
|
|
3
|
+
import graphqlPlugin from '@graphql-eslint/eslint-plugin'
|
|
4
|
+
import { globalIgnores } from 'eslint/config'
|
|
5
|
+
import withNuxt from './.nuxt/eslint.config.mjs'
|
|
6
|
+
|
|
7
|
+
// export default withNuxt(
|
|
8
|
+
export default withNuxt(
|
|
9
|
+
// Your custom configs here
|
|
10
|
+
antfu(
|
|
11
|
+
{
|
|
12
|
+
formatters: true,
|
|
13
|
+
vue: true,
|
|
14
|
+
pnpm: false,
|
|
15
|
+
unocss: true,
|
|
16
|
+
},
|
|
17
|
+
),
|
|
18
|
+
globalIgnores([
|
|
19
|
+
'dist',
|
|
20
|
+
'node_modules',
|
|
21
|
+
'.output',
|
|
22
|
+
'.nuxt',
|
|
23
|
+
'.storybook',
|
|
24
|
+
'storybook-static',
|
|
25
|
+
'.github',
|
|
26
|
+
'coverage',
|
|
27
|
+
'*.log',
|
|
28
|
+
'nuxt.d.ts',
|
|
29
|
+
'.output',
|
|
30
|
+
'.DS_Store',
|
|
31
|
+
'.vscode',
|
|
32
|
+
'*.md',
|
|
33
|
+
'package.json',
|
|
34
|
+
'package-lock.json',
|
|
35
|
+
'babel.config.js',
|
|
36
|
+
'graphql',
|
|
37
|
+
'types.ts',
|
|
38
|
+
'generated',
|
|
39
|
+
'components.d.ts',
|
|
40
|
+
'icons.d.ts',
|
|
41
|
+
'auto.d.ts',
|
|
42
|
+
'src-tauri',
|
|
43
|
+
'auto-imports.d.ts',
|
|
44
|
+
'schema.graphql',
|
|
45
|
+
]),
|
|
46
|
+
{
|
|
47
|
+
rules: {
|
|
48
|
+
'unocss/order': 'error', // or "error",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
// @ts-expect-error
|
|
52
|
+
{
|
|
53
|
+
files: ['**/*.graphql', '**/*.gql'],
|
|
54
|
+
languageOptions: {
|
|
55
|
+
parser: graphqlPlugin.parser,
|
|
56
|
+
},
|
|
57
|
+
plugins: {
|
|
58
|
+
'@graphql-eslint': graphqlPlugin,
|
|
59
|
+
},
|
|
60
|
+
rules: {
|
|
61
|
+
'@graphql-eslint/no-anonymous-operations': 'error',
|
|
62
|
+
'@graphql-eslint/naming-convention': [
|
|
63
|
+
'error',
|
|
64
|
+
{
|
|
65
|
+
OperationDefinition: {
|
|
66
|
+
style: 'PascalCase',
|
|
67
|
+
forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
|
|
68
|
+
forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
)
|
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
extends: ['@studio-fes/layer-base'],
|
|
4
|
+
|
|
5
|
+
modules: [
|
|
6
|
+
'nuxt-graphql-middleware',
|
|
7
|
+
'nuxt-multi-cache',
|
|
8
|
+
],
|
|
9
|
+
|
|
10
|
+
runtimeConfig: {
|
|
11
|
+
multiCacheEnabled: import.meta.env.NUXT_CACHE_ENABLED === 'true',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
graphqlMiddleware: {
|
|
15
|
+
downloadSchema: false,
|
|
16
|
+
graphqlEndpoint: 'https://example.com/graphql',
|
|
17
|
+
serverApiPrefix: '/gql',
|
|
18
|
+
clientCache: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
},
|
|
21
|
+
autoImportPatterns: ['./queries/**/*.gql'],
|
|
22
|
+
},
|
|
23
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@studio-fes/layer-strapi",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"main": "./nuxt.config.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "nuxi dev",
|
|
8
|
+
"build": "nuxi build",
|
|
9
|
+
"start": "nuxi preview",
|
|
10
|
+
"prepare": "nuxi prepare",
|
|
11
|
+
"generate": "nuxt generate",
|
|
12
|
+
"preview": "nuxt preview",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@nuxt/eslint": "latest",
|
|
18
|
+
"@studio-fes/layer-base": "workspace:*",
|
|
19
|
+
"@types/node": "^24.10.4",
|
|
20
|
+
"eslint": "^9.39.2",
|
|
21
|
+
"nuxt": "^4.2.2",
|
|
22
|
+
"nuxt-graphql-middleware": "^5.3.2",
|
|
23
|
+
"nuxt-multi-cache": "^4.0.3",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"vue": "latest"
|
|
26
|
+
},
|
|
27
|
+
"description": "Create Nuxt extendable layer with this GitHub template.",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/rondodevs/nuxt-layers.git"
|
|
31
|
+
},
|
|
32
|
+
"author": "studio fes",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/rondodevs/nuxt-layers/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/rondodevs/nuxt-layers#readme"
|
|
38
|
+
}
|
package/queries/.gitkeep
ADDED
|
File without changes
|
package/schema.graphql
ADDED
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FetchResponse } from 'ofetch'
|
|
2
|
+
import { defineGraphqlServerOptions } from 'nuxt-graphql-middleware/server-options'
|
|
3
|
+
|
|
4
|
+
export default defineGraphqlServerOptions({
|
|
5
|
+
async doGraphqlRequest(context) {
|
|
6
|
+
const { operationName, variables, operationDocument } = context
|
|
7
|
+
|
|
8
|
+
const config = useRuntimeConfig()
|
|
9
|
+
|
|
10
|
+
function doRequest(): Promise<FetchResponse<{ data: any, errors: any[] }>> {
|
|
11
|
+
return $fetch.raw(config.graphqlMiddleware.graphqlEndpoint, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
ignoreResponseError: true,
|
|
14
|
+
body: {
|
|
15
|
+
query: operationDocument,
|
|
16
|
+
variables,
|
|
17
|
+
operationName,
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const res = await doRequest()
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
data: res._data?.data,
|
|
26
|
+
errors: res._data?.errors?.map(e => ({
|
|
27
|
+
...e,
|
|
28
|
+
path: Array.isArray(e.path) ? e.path : [],
|
|
29
|
+
})) || [],
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { defineMultiCacheOptions } from 'nuxt-multi-cache/server-options'
|
|
4
|
+
import fsDriver from 'unstorage/drivers/fs'
|
|
5
|
+
import nullDriver from 'unstorage/drivers/null'
|
|
6
|
+
|
|
7
|
+
export default defineMultiCacheOptions(() => {
|
|
8
|
+
const config = useRuntimeConfig()
|
|
9
|
+
const { multiCacheEnabled } = config
|
|
10
|
+
|
|
11
|
+
const driver = multiCacheEnabled
|
|
12
|
+
? fsDriver({ base: '.nuxt-multi-cache' })
|
|
13
|
+
: nullDriver()
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
data: {
|
|
17
|
+
storage: {
|
|
18
|
+
driver,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
})
|
package/tsconfig.json
ADDED