sanity-plugin-seofields 1.2.6 → 1.2.7
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/README.md +465 -0
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -658,6 +658,205 @@ export function SEO({seo}) {
|
|
|
658
658
|
}
|
|
659
659
|
```
|
|
660
660
|
|
|
661
|
+
## 🎯 Framework Integration Examples
|
|
662
|
+
|
|
663
|
+
### Remix (Loader + Action Approach)
|
|
664
|
+
|
|
665
|
+
Handle SEO metadata in Remix loaders for server-side rendering with JSON responses:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// routes/posts.$slug.tsx
|
|
669
|
+
import {json, type LoaderFunction} from '@remix-run/node'
|
|
670
|
+
import {useLoaderData} from '@remix-run/react'
|
|
671
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
|
|
672
|
+
|
|
673
|
+
export const loader: LoaderFunction = async ({params}) => {
|
|
674
|
+
// Fetch post with SEO fields from Sanity
|
|
675
|
+
const post = await sanityClient.fetch(
|
|
676
|
+
`*[_type == "post" && slug.current == $slug][0]{
|
|
677
|
+
title, content, seo, slug
|
|
678
|
+
}`,
|
|
679
|
+
{slug: params.slug},
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
// Use buildSeoMeta to generate meta tags
|
|
683
|
+
const seoMeta = buildSeoMeta(post.seo, {
|
|
684
|
+
defaultTitle: 'Blog',
|
|
685
|
+
siteUrl: 'https://example.com',
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
return json({post, seoMeta})
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
692
|
+
return data?.seoMeta || []
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export default function PostRoute() {
|
|
696
|
+
const {post} = useLoaderData<typeof loader>()
|
|
697
|
+
return <article>{post.title}</article>
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Nuxt 3 (Composable Approach)
|
|
702
|
+
|
|
703
|
+
Create a composable for SSR-friendly SEO management:
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// composables/useSanityMeta.ts
|
|
707
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
|
|
708
|
+
|
|
709
|
+
export const useSanityMeta = (seo: SEOFields, options = {}) => {
|
|
710
|
+
const {
|
|
711
|
+
defaultTitle = 'My Site',
|
|
712
|
+
siteUrl = 'https://example.com',
|
|
713
|
+
} = options
|
|
714
|
+
|
|
715
|
+
const meta = buildSeoMeta(seo, {defaultTitle, siteUrl})
|
|
716
|
+
|
|
717
|
+
// useHead() handles SSR + client-side rendering
|
|
718
|
+
useHead({
|
|
719
|
+
title: seo?.title || defaultTitle,
|
|
720
|
+
meta: meta.map(m => ({
|
|
721
|
+
name: m.name || m.property,
|
|
722
|
+
content: m.content,
|
|
723
|
+
})),
|
|
724
|
+
link: seo?.canonicalUrl
|
|
725
|
+
? [{rel: 'canonical', href: seo.canonicalUrl}]
|
|
726
|
+
: [],
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// pages/blog/[slug].vue
|
|
731
|
+
<script setup lang="ts">
|
|
732
|
+
const route = useRoute()
|
|
733
|
+
const {data: post} = await useFetch(`/api/posts/${route.params.slug}`)
|
|
734
|
+
|
|
735
|
+
useSanityMeta(post.value?.seo, {
|
|
736
|
+
siteUrl: 'https://example.com',
|
|
737
|
+
})
|
|
738
|
+
</script>
|
|
739
|
+
|
|
740
|
+
<template>
|
|
741
|
+
<article v-if="post">
|
|
742
|
+
<h1>{{ post.title }}</h1>
|
|
743
|
+
</article>
|
|
744
|
+
</template>
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Astro (Server-Side Rendering)
|
|
748
|
+
|
|
749
|
+
Leverage Astro's component-level SEO with static generation:
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
// src/pages/blog/[slug].astro
|
|
753
|
+
---
|
|
754
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/utils'
|
|
755
|
+
import Layout from '../../layouts/Layout.astro'
|
|
756
|
+
|
|
757
|
+
// Fetch from Sanity at build time
|
|
758
|
+
const {slug} = Astro.params
|
|
759
|
+
const post = await sanityClient.fetch(
|
|
760
|
+
`*[_type == "post" && slug.current == $slug][0]{
|
|
761
|
+
title, content, seo, slug
|
|
762
|
+
}`,
|
|
763
|
+
{slug},
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
// Generate meta tags for static HTML
|
|
767
|
+
const seoMeta = buildSeoMeta(post.seo, {
|
|
768
|
+
defaultTitle: 'Blog',
|
|
769
|
+
siteUrl: Astro.site,
|
|
770
|
+
})
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
<Layout
|
|
774
|
+
title={post.seo?.title}
|
|
775
|
+
meta={seoMeta}
|
|
776
|
+
canonicalUrl={post.seo?.canonicalUrl}
|
|
777
|
+
>
|
|
778
|
+
<article>
|
|
779
|
+
<h1>{post.title}</h1>
|
|
780
|
+
</article>
|
|
781
|
+
</Layout>
|
|
782
|
+
|
|
783
|
+
<!-- Astro layouts handle meta tag rendering -->
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### React SPA (Client-Side with Helmet)
|
|
787
|
+
|
|
788
|
+
For client-rendered React apps without SSR:
|
|
789
|
+
|
|
790
|
+
```typescript
|
|
791
|
+
// components/PostHead.tsx
|
|
792
|
+
import {Helmet} from 'react-helmet-async'
|
|
793
|
+
import type {SEOFields} from 'sanity-plugin-seofields'
|
|
794
|
+
|
|
795
|
+
interface PostHeadProps {
|
|
796
|
+
seo?: SEOFields
|
|
797
|
+
fallbackTitle: string
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function PostHead({seo, fallbackTitle}: PostHeadProps) {
|
|
801
|
+
return (
|
|
802
|
+
<Helmet>
|
|
803
|
+
{/* Basic Meta */}
|
|
804
|
+
<title>{seo?.title || fallbackTitle}</title>
|
|
805
|
+
<meta name="description" content={seo?.description || ''} />
|
|
806
|
+
|
|
807
|
+
{/* Open Graph - critical for social shares */}
|
|
808
|
+
<meta property="og:title" content={seo?.openGraph?.title} />
|
|
809
|
+
<meta property="og:description" content={seo?.openGraph?.description} />
|
|
810
|
+
{seo?.openGraph?.image?.url && (
|
|
811
|
+
<meta property="og:image" content={seo.openGraph.image.url} />
|
|
812
|
+
)}
|
|
813
|
+
|
|
814
|
+
{/* Robots */}
|
|
815
|
+
{seo?.robots?.noIndex && <meta name="robots" content="noindex" />}
|
|
816
|
+
|
|
817
|
+
{/* Canonical (limit crawl budget) */}
|
|
818
|
+
{seo?.canonicalUrl && (
|
|
819
|
+
<link rel="canonical" href={seo.canonicalUrl} />
|
|
820
|
+
)}
|
|
821
|
+
</Helmet>
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Usage in page component
|
|
826
|
+
// Note: Client-side rendering cannot inject meta tags pre-page-load.
|
|
827
|
+
// For public pages, use SSR or static generation instead.
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## 🚀 Migrating from Other SEO Plugins
|
|
833
|
+
|
|
834
|
+
Coming from **Yoast**, **All in One SEO**, or **RankMath**?
|
|
835
|
+
|
|
836
|
+
| Feature | Yoast | All in One SEO | RankMath | sanity-plugin-seofields |
|
|
837
|
+
|---------|-------|----------------|----------|------------------------|
|
|
838
|
+
| **Meta Title/Description** | ✅ | ✅ | ✅ | ✅ |
|
|
839
|
+
| **Open Graph Tags** | ✅ | ✅ | ✅ | ✅ |
|
|
840
|
+
| **Twitter Cards** | ⚠️ Limited | ✅ | ✅ | ✅ |
|
|
841
|
+
| **Readability Analysis** | ✅ | ✅ | ✅ | ❌ (Sanity-native focus) |
|
|
842
|
+
| **Keyword Density** | ✅ | ✅ | ✅ | ❌ (External tools) |
|
|
843
|
+
| **Custom Meta Attributes** | ⚠️ Limited | ✅ | ✅ | ✅ |
|
|
844
|
+
| **Robots/Canonical** | ✅ | ✅ | ✅ | ✅ |
|
|
845
|
+
| **Headless-First** | ❌ | ❌ | ❌ | ✅ Framework-agnostic |
|
|
846
|
+
| **SSR-Ready** | N/A | N/A | N/A | ✅ All frameworks |
|
|
847
|
+
|
|
848
|
+
### Migration Path
|
|
849
|
+
|
|
850
|
+
1. **Export existing metadata** from your old plugin (title, description, OG tags)
|
|
851
|
+
2. **Create a Sanity schema** matching your current fields — map to `seoFields` type
|
|
852
|
+
3. **Bulk import** using Sanity's API or migration scripts
|
|
853
|
+
4. **Update your frontend** to use `buildSeoMeta` utilities instead of plugin hooks
|
|
854
|
+
5. **Test meta rendering** in browsers DevTools and social preview tools
|
|
855
|
+
|
|
856
|
+
For detailed migration guides, see [Migration Guides](#) in our documentation.
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
661
860
|
## 📚 API Reference
|
|
662
861
|
|
|
663
862
|
### Main Export
|
|
@@ -675,6 +874,272 @@ import seofields from 'sanity-plugin-seofields'
|
|
|
675
874
|
- `metaAttribute` - Individual meta attribute
|
|
676
875
|
- `robots` - Search engine robots settings
|
|
677
876
|
|
|
877
|
+
## 🔧 Troubleshooting
|
|
878
|
+
|
|
879
|
+
### TypeScript auto-import not working
|
|
880
|
+
|
|
881
|
+
**Problem:** `buildSeoMeta` doesn't appear in IDE autocomplete
|
|
882
|
+
|
|
883
|
+
**Solution:**
|
|
884
|
+
|
|
885
|
+
1. Check your `package.json` exports field has a `"types"` condition:
|
|
886
|
+
```json
|
|
887
|
+
{
|
|
888
|
+
"exports": {
|
|
889
|
+
".": {
|
|
890
|
+
"types": "./dist/index.d.ts",
|
|
891
|
+
"default": "./dist/index.js"
|
|
892
|
+
},
|
|
893
|
+
"./next": {
|
|
894
|
+
"types": "./dist/next.d.ts",
|
|
895
|
+
"default": "./dist/next.js"
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
2. Verify your `tsconfig.json` has the correct `moduleResolution`:
|
|
902
|
+
```json
|
|
903
|
+
{
|
|
904
|
+
"compilerOptions": {
|
|
905
|
+
"moduleResolution": "bundler",
|
|
906
|
+
"resolveJsonModule": true
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
---
|
|
912
|
+
|
|
913
|
+
### "Cannot find module 'sanity-plugin-seofields/next'"
|
|
914
|
+
|
|
915
|
+
**Problem:** Runtime import error when trying to use Next.js utilities
|
|
916
|
+
|
|
917
|
+
**Solution:**
|
|
918
|
+
|
|
919
|
+
1. Ensure built files exist in `dist/next.js`:
|
|
920
|
+
```bash
|
|
921
|
+
npm run build
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
2. Clear and reinstall node_modules:
|
|
925
|
+
```bash
|
|
926
|
+
rm -rf node_modules package-lock.json
|
|
927
|
+
npm install
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
3. Verify `package.json` exports includes the next export:
|
|
931
|
+
```json
|
|
932
|
+
{
|
|
933
|
+
"exports": {
|
|
934
|
+
"./next": {
|
|
935
|
+
"types": "./dist/next.d.ts",
|
|
936
|
+
"default": "./dist/next.js"
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
### Type inference in generateMetadata()
|
|
945
|
+
|
|
946
|
+
**Problem:** `buildSeoMeta()` return type is not recognized as Next.js `Metadata`
|
|
947
|
+
|
|
948
|
+
**Solution:** Explicitly type the return value:
|
|
949
|
+
|
|
950
|
+
```tsx
|
|
951
|
+
import type {Metadata} from 'next'
|
|
952
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
953
|
+
|
|
954
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
955
|
+
const seoData = await fetchSeoData()
|
|
956
|
+
const metadata = buildSeoMeta(seoData)
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
title: metadata.title,
|
|
960
|
+
description: metadata.description,
|
|
961
|
+
openGraph: {
|
|
962
|
+
title: metadata.openGraph?.title,
|
|
963
|
+
description: metadata.openGraph?.description,
|
|
964
|
+
url: metadata.openGraph?.url,
|
|
965
|
+
},
|
|
966
|
+
twitter: {
|
|
967
|
+
card: metadata.twitter?.card as any,
|
|
968
|
+
site: metadata.twitter?.site,
|
|
969
|
+
creator: metadata.twitter?.creator,
|
|
970
|
+
},
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
### Dashboard not showing in Sanity Studio
|
|
978
|
+
|
|
979
|
+
**Problem:** SEO Health tool doesn't appear in the studio
|
|
980
|
+
|
|
981
|
+
**Solution:**
|
|
982
|
+
|
|
983
|
+
1. Ensure the plugin is added to `sanity.config.ts`:
|
|
984
|
+
```typescript
|
|
985
|
+
import seofields from 'sanity-plugin-seofields'
|
|
986
|
+
|
|
987
|
+
export default defineConfig({
|
|
988
|
+
// ... other config
|
|
989
|
+
plugins: [
|
|
990
|
+
seofields({
|
|
991
|
+
documentTypes: ['post', 'page', 'product'],
|
|
992
|
+
// other options
|
|
993
|
+
}),
|
|
994
|
+
],
|
|
995
|
+
})
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
2. Check that `documentTypes` array includes your document types:
|
|
999
|
+
```typescript
|
|
1000
|
+
seofields({
|
|
1001
|
+
documentTypes: ['post', 'page'], // Add your document types here
|
|
1002
|
+
})
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
3. Verify plugin config fieldVisibility is not hiding SEO fields:
|
|
1006
|
+
```typescript
|
|
1007
|
+
seofields({
|
|
1008
|
+
documentTypes: ['post'],
|
|
1009
|
+
fieldVisibility: {
|
|
1010
|
+
// Make sure SEO fields aren't set to hidden
|
|
1011
|
+
},
|
|
1012
|
+
})
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
### Image URLs not resolving
|
|
1018
|
+
|
|
1019
|
+
**Problem:** OG/Twitter images show as `undefined` in meta tags
|
|
1020
|
+
|
|
1021
|
+
**Solution:** Provide an `imageUrlResolver` function:
|
|
1022
|
+
|
|
1023
|
+
```tsx
|
|
1024
|
+
import imageUrlBuilder from '@sanity/image-url'
|
|
1025
|
+
import {client} from './sanity.client'
|
|
1026
|
+
|
|
1027
|
+
const imageBuilder = imageUrlBuilder(client)
|
|
1028
|
+
|
|
1029
|
+
export function buildImageUrl(source) {
|
|
1030
|
+
if (!source) return undefined
|
|
1031
|
+
return imageBuilder.image(source).url()
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// In your buildSeoMeta call:
|
|
1035
|
+
const metadata = buildSeoMeta({
|
|
1036
|
+
...seoData,
|
|
1037
|
+
imageUrlResolver: buildImageUrl,
|
|
1038
|
+
})
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Or use it in your Next.js layout:
|
|
1042
|
+
|
|
1043
|
+
```tsx
|
|
1044
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
1045
|
+
import imageUrlBuilder from '@sanity/image-url'
|
|
1046
|
+
|
|
1047
|
+
const imageBuilder = imageUrlBuilder(client)
|
|
1048
|
+
|
|
1049
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
1050
|
+
const seoData = await sanityFetch(SeoQuery)
|
|
1051
|
+
|
|
1052
|
+
const metadata = buildSeoMeta({
|
|
1053
|
+
...seoData,
|
|
1054
|
+
imageUrlResolver: (image) => imageBuilder.image(image).url(),
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
return metadata
|
|
1058
|
+
}
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
---
|
|
1062
|
+
|
|
1063
|
+
### generateMetadata() not finding Sanity data
|
|
1064
|
+
|
|
1065
|
+
**Problem:** Data is `undefined` when trying to fetch from Sanity in Next.js
|
|
1066
|
+
|
|
1067
|
+
**Solution:**
|
|
1068
|
+
|
|
1069
|
+
1. Ensure `sanityFetch` is properly awaited:
|
|
1070
|
+
```tsx
|
|
1071
|
+
import {sanityFetch} from '@/lib/sanity.client'
|
|
1072
|
+
|
|
1073
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
1074
|
+
try {
|
|
1075
|
+
const seoData = await sanityFetch(SeoQuery) // Don't forget await!
|
|
1076
|
+
return buildSeoMeta(seoData)
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
console.error('Failed to fetch SEO data:', error)
|
|
1079
|
+
return {title: 'Default Title'}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
2. Verify environment variables are set:
|
|
1085
|
+
```bash
|
|
1086
|
+
# .env.local
|
|
1087
|
+
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
|
|
1088
|
+
NEXT_PUBLIC_SANITY_DATASET=production
|
|
1089
|
+
SANITY_API_TOKEN=your_token (if using authenticated fetches)
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
3. Complete example with proper error handling:
|
|
1093
|
+
```tsx
|
|
1094
|
+
import type {Metadata} from 'next'
|
|
1095
|
+
import {buildSeoMeta} from 'sanity-plugin-seofields/next'
|
|
1096
|
+
import {sanityFetch} from '@/lib/sanity.client'
|
|
1097
|
+
|
|
1098
|
+
const SeoQuery = `*[_type == "post" && slug.current == $slug][0] {
|
|
1099
|
+
title,
|
|
1100
|
+
seo {
|
|
1101
|
+
title,
|
|
1102
|
+
description,
|
|
1103
|
+
openGraph {
|
|
1104
|
+
title,
|
|
1105
|
+
description,
|
|
1106
|
+
image,
|
|
1107
|
+
},
|
|
1108
|
+
twitter {
|
|
1109
|
+
card,
|
|
1110
|
+
site,
|
|
1111
|
+
creator,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
}`
|
|
1115
|
+
|
|
1116
|
+
export async function generateMetadata({
|
|
1117
|
+
params,
|
|
1118
|
+
}: {
|
|
1119
|
+
params: {slug: string}
|
|
1120
|
+
}): Promise<Metadata> {
|
|
1121
|
+
try {
|
|
1122
|
+
const doc = await sanityFetch(SeoQuery, {slug: params.slug})
|
|
1123
|
+
|
|
1124
|
+
if (!doc) {
|
|
1125
|
+
return {title: 'Post not found'}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return buildSeoMeta(doc.seo || {})
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.error('SEO metadata error:', error)
|
|
1131
|
+
return {title: 'Error loading page'}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
---
|
|
1137
|
+
|
|
1138
|
+
**Still stuck?** Check our:
|
|
1139
|
+
- 📖 [Full Documentation](./TYPES_SCHEMA_DOCS.md)
|
|
1140
|
+
- 🐛 [GitHub Issues](https://github.com/hardik-143/sanity-plugin-seofields/issues)
|
|
1141
|
+
- 📧 [Email Support](mailto:dhardik1430@gmail.com)
|
|
1142
|
+
|
|
678
1143
|
## 🤝 Contributing
|
|
679
1144
|
|
|
680
1145
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/next.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/next.ts","../src/helpers/seoMeta.ts","../src/helpers/SeoMetaTags.tsx"],"sourcesContent":["/**\n * Next.js App Router helpers — safe to import in Server Components.\n *\n * @example\n * import { buildSeoMeta, SeoMetaTags } from 'sanity-plugin-seofields/next'\n */\nexport {buildSeoMeta, sanitizeOGType, sanitizeTwitterCard} from './helpers/seoMeta'\n\nexport type {BuildSeoMetaOptions, SeoMetaDefaults, SeoMetadata} from './helpers/seoMeta'\n\nexport {SeoMetaTags} from './helpers/SeoMetaTags'\nexport type {SeoMetaTagsProps} from './helpers/SeoMetaTags'\n","/**\n * Headless CMS integration helpers for sanity-plugin-seofields\n *\n * Provides framework-agnostic SEO metadata utilities for use with:\n * - Next.js App Router → buildSeoMeta() inside generateMetadata()\n * - Next.js Pages Router → <SeoMetaTags> inside Next.js <Head>\n * - Nuxt / Remix / any SSR → <SeoMetaTags> inside your <head> slot\n */\n\nimport type {SanityImage, SanityImageWithAlt, SeoFields} from '../types'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** Structured metadata returned by buildSeoMeta(). Compatible with Next.js Metadata (App Router). */\nexport interface SeoMetadata {\n title?: string | null\n description?: string | null\n keywords?: string[]\n robots?: {\n index?: boolean\n follow?: boolean\n googleBot?: {\n index?: boolean\n follow?: boolean\n }\n }\n openGraph?: {\n type?: string\n url?: string\n title?: string\n description?: string\n siteName?: string\n images?: Array<{url: string; width?: number; height?: number; alt?: string}>\n }\n twitter?: {\n card?: string\n site?: string\n creator?: string\n title?: string\n description?: string\n images?: string[]\n }\n alternates?: {\n canonical?: string\n }\n /** Any custom meta attributes from seo.metaAttributes */\n other?: Record<string, string>\n}\n\n/** Default values used when SEO fields are missing. */\nexport interface SeoMetaDefaults {\n title?: string\n description?: string\n siteName?: string\n twitterSite?: string\n twitterCreator?: string\n /** Fallback image URL when no OG / Twitter image is set. */\n ogImage?: string\n}\n\n/**\n * Permissive image shape accepted by buildSeoMeta — compatible with both the\n * plugin's SanityImage and Sanity's code-generated image type (where `asset`\n * and `alt` are optional).\n */\ninterface SeoImageInput {\n _type?: string\n asset?: {_ref: string; _type: string; _weak?: boolean; [key: string]: unknown}\n hotspot?: unknown\n crop?: unknown\n alt?: string\n}\n\n/**\n * Input-compatible variant of SeoFields. Structurally matches Sanity's\n * code-generated types (where `asset`, `alt`, `key`, and `type` are all\n * optional), so you can pass `data.seo` from a sanityFetch result directly\n * without any `as any` or manual casting.\n */\nexport interface SeoFieldsInput {\n _type?: string\n robots?: {noIndex?: boolean | null; noFollow?: boolean | null} | null\n title?: string | null\n description?: string | null\n metaImage?: SeoImageInput | null\n metaAttributes?: Array<{_key?: string; key?: string; value?: string; type?: string}> | null\n keywords?: string[] | null\n canonicalUrl?: string | null\n openGraph?: {\n _type?: string\n url?: string | null\n title?: string | null\n description?: string | null\n siteName?: string | null\n type?: string | null\n imageType?: string | null\n image?: SeoImageInput | null\n imageUrl?: string | null\n } | null\n twitter?: {\n _type?: string\n card?: string | null\n site?: string | null\n creator?: string | null\n title?: string | null\n description?: string | null\n imageType?: string | null\n image?: SeoImageInput | null\n imageUrl?: string | null\n } | null\n}\n\n/** Options accepted by buildSeoMeta(). */\nexport interface BuildSeoMetaOptions {\n /**\n * The raw SEO object from Sanity (_type excluded or included — both work).\n * Pass `null` or `undefined` to fall back entirely to `defaults`.\n *\n * Accepts both the strict plugin `SeoFields` type and Sanity's code-generated\n * type (which has all nested fields optional) without any `as any` cast.\n */\n seo?: SeoFieldsInput | null\n\n /**\n * The base URL of your site, e.g. \"https://example.com\".\n * Used for canonical URL and OpenGraph URL construction.\n */\n baseUrl?: string\n\n /**\n * The path for the current page, e.g. \"/about\".\n * Combined with baseUrl to produce the canonical + OG url.\n * Defaults to \"\".\n */\n path?: string\n\n /**\n * Default values used when the Sanity SEO fields are empty / missing.\n */\n defaults?: SeoMetaDefaults\n\n /**\n * Resolve a Sanity image asset to a plain URL string.\n *\n * @example (using @sanity/image-url)\n * imageUrlResolver: (img) => urlFor(img).width(1200).url()\n */\n imageUrlResolver?: (image: SanityImage | SanityImageWithAlt) => string | null | undefined\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nconst VALID_OG_TYPES = [\n 'website',\n 'article',\n 'profile',\n 'book',\n 'music',\n 'video',\n 'product',\n] as const\ntype OGType = (typeof VALID_OG_TYPES)[number]\n\n/**\n * Coerce an arbitrary string to a valid OpenGraph type.\n * Falls back to \"website\" when the value is invalid.\n */\nexport function sanitizeOGType(value?: string): OGType {\n if (value && (VALID_OG_TYPES as readonly string[]).includes(value)) {\n return value as OGType\n }\n return 'website'\n}\n\nconst VALID_TWITTER_CARDS = ['summary', 'summary_large_image', 'app', 'player'] as const\ntype TwitterCard = (typeof VALID_TWITTER_CARDS)[number]\n\n/**\n * Coerce an arbitrary string to a valid Twitter card type.\n * Falls back to \"summary_large_image\" when the value is invalid.\n */\nexport function sanitizeTwitterCard(value?: string): TwitterCard {\n if (value && (VALID_TWITTER_CARDS as readonly string[]).includes(value)) {\n return value as TwitterCard\n }\n return 'summary_large_image'\n}\n\n// ─── Core builder ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a Sanity SEO object into a structured metadata object.\n *\n * The return value is structurally compatible with Next.js App Router's\n * `Metadata` type, so you can return it directly from `generateMetadata()`.\n *\n * @example Next.js App Router\n * ```ts\n * import { buildSeoMeta } from 'sanity-plugin-seofields'\n * import { urlFor } from '@/sanity/lib/image'\n *\n * export async function generateMetadata(): Promise<Metadata> {\n * const { seo } = await sanityFetch({ query: PAGE_SEO_QUERY })\n * return buildSeoMeta({\n * seo,\n * baseUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * path: '/about',\n * defaults: { title: 'My Site', siteName: 'My Site' },\n * imageUrlResolver: (img) => urlFor(img).width(1200).url(),\n * })\n * }\n * ```\n */\nexport function buildSeoMeta(options: BuildSeoMetaOptions): SeoMetadata {\n const {seo, baseUrl = '', path = '', defaults = {}, imageUrlResolver} = options\n\n const normalizedBase = baseUrl.replace(/\\/+$/, '') // remove trailing /\n const normalizedPath = path.replace(/^\\/+/, '') // remove leading /\n\n const fullUrl = [normalizedBase, normalizedPath].filter(Boolean).join('/')\n\n // ── OG image resolution ──\n let ogImageURL: string = defaults.ogImage || ''\n if (seo?.openGraph?.imageType === 'url' && seo.openGraph.imageUrl) {\n ogImageURL = seo.openGraph.imageUrl\n } else if (seo?.openGraph?.image && imageUrlResolver) {\n ogImageURL = imageUrlResolver(seo.openGraph.image as SanityImage) || ogImageURL\n }\n\n // ── Twitter image resolution ──\n let twitterImageURL: string = ogImageURL // reuse OG image as fallback\n if (seo?.twitter?.imageType === 'url' && seo.twitter.imageUrl) {\n twitterImageURL = seo.twitter.imageUrl\n } else if (seo?.twitter?.image && imageUrlResolver) {\n twitterImageURL = imageUrlResolver(seo.twitter.image as SanityImage) || twitterImageURL\n }\n\n // ── Custom meta attributes → `other` map ──\n const other: Record<string, string> = {}\n if (Array.isArray(seo?.metaAttributes)) {\n for (const attr of seo!.metaAttributes!) {\n if (attr.key && attr.value) {\n other[attr.key] = attr.value\n }\n }\n }\n\n const ogUrl = seo?.openGraph?.url || fullUrl\n\n return {\n title: seo?.title ?? defaults.title ?? null,\n description: seo?.description ?? defaults.description ?? null,\n keywords: seo?.keywords?.length ? (seo.keywords as string[]) : undefined,\n robots: {\n index: !seo?.robots?.noIndex,\n follow: !seo?.robots?.noFollow,\n googleBot: {\n index: !seo?.robots?.noIndex,\n follow: !seo?.robots?.noFollow,\n },\n },\n openGraph: {\n type: sanitizeOGType(seo?.openGraph?.type ?? undefined),\n url: ogUrl || undefined,\n title: seo?.openGraph?.title ?? defaults.title,\n description: seo?.openGraph?.description ?? defaults.description,\n siteName: seo?.openGraph?.siteName ?? defaults.siteName,\n images: ogImageURL ? [{url: ogImageURL}] : [],\n },\n twitter: {\n card: sanitizeTwitterCard(seo?.twitter?.card ?? undefined),\n site: seo?.twitter?.site ?? defaults.twitterSite,\n creator: seo?.twitter?.creator ?? defaults.twitterCreator,\n title: seo?.twitter?.title ?? defaults.title,\n description: seo?.twitter?.description ?? defaults.description,\n images: twitterImageURL ? [twitterImageURL] : [],\n },\n alternates: {\n canonical: fullUrl || undefined,\n },\n ...(Object.keys(other).length > 0 ? {other} : {}),\n }\n}\n","/**\n * <SeoMetaTags> — Framework-agnostic React SEO meta tag renderer.\n *\n * Renders all SEO meta tags as plain React elements.\n * Place it inside your framework's <Head> component:\n *\n * @example Next.js Pages Router\n * ```tsx\n * import Head from 'next/head'\n * import { SeoMetaTags } from 'sanity-plugin-seofields'\n *\n * export default function Page({ seo }) {\n * return (\n * <>\n * <Head>\n * <SeoMetaTags\n * data={seo}\n * baseUrl=\"https://example.com\"\n * path=\"/about\"\n * defaults={{ title: 'My Site', siteName: 'My Site' }}\n * imageUrlResolver={(img) => urlFor(img).width(1200).url()}\n * />\n * </Head>\n * <main>...</main>\n * </>\n * )\n * }\n * ```\n *\n * @example Nuxt 3 / generic SSR (inside <Head> slot)\n * ```tsx\n * <Head>\n * <SeoMetaTags data={seo} baseUrl=\"https://example.com\" path=\"/\" />\n * </Head>\n * ```\n */\nimport React from 'react'\n\nimport type {SanityImage, SanityImageWithAlt, SeoFields} from '../types'\nimport {buildSeoMeta, type BuildSeoMetaOptions} from './seoMeta'\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface SeoMetaTagsProps {\n /**\n * The raw SEO object from Sanity.\n * Pass `null` / `undefined` to render only the defaults.\n */\n data?: Partial<SeoFields> | null\n\n /**\n * Base URL of your site, e.g. \"https://example.com\".\n * Used for canonical link, og:url fallback.\n */\n baseUrl?: string\n\n /**\n * Current page path, e.g. \"/about\".\n * Defaults to \"\".\n */\n path?: string\n\n /**\n * Default values used when SEO fields are missing.\n */\n defaults?: BuildSeoMetaOptions['defaults']\n\n /**\n * Resolve a Sanity image asset reference to a full URL string.\n *\n * @example\n * imageUrlResolver={(img) => urlFor(img).width(1200).url()}\n */\n imageUrlResolver?: (image: SanityImage | SanityImageWithAlt) => string | null | undefined\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Renders all SEO meta tags for a page as plain React elements.\n * Intended to be placed inside your framework's <Head> / <head> component.\n *\n * Renders:\n * - `<title>`\n * - `<meta name=\"description\">`\n * - `<meta name=\"keywords\">`\n * - `<meta name=\"robots\">`\n * - OpenGraph meta tags (`og:*`)\n * - Twitter Card meta tags (`twitter:*`)\n * - Any custom `seo.metaAttributes` as `<meta name=\"...\" content=\"...\">`\n */\nexport function SeoMetaTags({data, baseUrl, path, defaults, imageUrlResolver}: SeoMetaTagsProps) {\n const meta = buildSeoMeta({seo: data, baseUrl, path, defaults, imageUrlResolver})\n\n const robotsContent = [\n meta.robots?.index === false ? 'noindex' : 'index',\n meta.robots?.follow === false ? 'nofollow' : 'follow',\n ].join(', ')\n\n return (\n <>\n {/* ── Title ── */}\n {meta.title && <title>{meta.title}</title>}\n\n {/* ── Basic meta ── */}\n {meta.description && <meta name=\"description\" content={meta.description} />}\n {meta.keywords?.length ? <meta name=\"keywords\" content={meta.keywords.join(', ')} /> : null}\n <meta name=\"robots\" content={robotsContent} />\n <meta name=\"googlebot\" content={robotsContent} />\n\n {/* ── Open Graph ── */}\n {meta.openGraph?.type && <meta property=\"og:type\" content={meta.openGraph.type} />}\n {meta.openGraph?.url && <meta property=\"og:url\" content={meta.openGraph.url} />}\n {meta.openGraph?.title && <meta property=\"og:title\" content={meta.openGraph.title} />}\n {meta.openGraph?.description && (\n <meta property=\"og:description\" content={meta.openGraph.description} />\n )}\n {meta.openGraph?.siteName && (\n <meta property=\"og:site_name\" content={meta.openGraph.siteName} />\n )}\n {meta.openGraph?.images?.map((img, i) => (\n <React.Fragment key={`og-img-${i}`}>\n <meta property=\"og:image\" content={img.url} />\n {img.width && <meta property=\"og:image:width\" content={String(img.width)} />}\n {img.height && <meta property=\"og:image:height\" content={String(img.height)} />}\n {img.alt && <meta property=\"og:image:alt\" content={img.alt} />}\n </React.Fragment>\n ))}\n\n {/* ── Twitter Card ── */}\n {meta.twitter?.card && <meta name=\"twitter:card\" content={meta.twitter.card} />}\n {meta.twitter?.site && <meta name=\"twitter:site\" content={meta.twitter.site} />}\n {meta.twitter?.creator && <meta name=\"twitter:creator\" content={meta.twitter.creator} />}\n {meta.twitter?.title && <meta name=\"twitter:title\" content={meta.twitter.title} />}\n {meta.twitter?.description && (\n <meta name=\"twitter:description\" content={meta.twitter.description} />\n )}\n {meta.twitter?.images?.map((url, i) => (\n <meta key={`tw-img-${i}`} name=\"twitter:image\" content={url} />\n ))}\n\n {/* ── Custom meta attributes ── */}\n {meta.other &&\n Object.entries(meta.other).map(([name, content]) => (\n <meta key={`custom-${name}`} name={name} content={content} />\n ))}\n\n {/* ── Canonical URL ── */}\n {meta.alternates?.canonical && <link rel=\"canonical\" href={meta.alternates.canonical} />}\n </>\n )\n}\n\nexport default SeoMetaTags\n"],"mappings":"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","names":["React"]}
|
|
1
|
+
{"version":3,"sources":["../src/next.ts","../src/helpers/seoMeta.ts","../src/helpers/SeoMetaTags.tsx"],"sourcesContent":["/**\n * Next.js App Router helpers — safe to import in Server Components.\n *\n * @example\n * import { buildSeoMeta, SeoMetaTags } from 'sanity-plugin-seofields/next'\n */\nexport type {\n BuildSeoMetaOptions,\n SeoFieldsInput,\n SeoMetadata,\n SeoMetaDefaults,\n} from './helpers/seoMeta'\nexport {buildSeoMeta, sanitizeOGType, sanitizeTwitterCard} from './helpers/seoMeta'\nexport type {SeoMetaTagsProps} from './helpers/SeoMetaTags'\nexport {SeoMetaTags} from './helpers/SeoMetaTags'\n","/**\n * Headless CMS integration helpers for sanity-plugin-seofields\n *\n * Provides framework-agnostic SEO metadata utilities for use with:\n * - Next.js App Router → buildSeoMeta() inside generateMetadata()\n * - Next.js Pages Router → <SeoMetaTags> inside Next.js <Head>\n * - Nuxt / Remix / any SSR → <SeoMetaTags> inside your <head> slot\n */\n\nimport type {SanityImage, SanityImageWithAlt, SeoFields} from '../types'\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** Structured metadata returned by buildSeoMeta(). Compatible with Next.js Metadata (App Router). */\nexport interface SeoMetadata {\n title?: string | null\n description?: string | null\n keywords?: string[]\n robots?: {\n index?: boolean\n follow?: boolean\n googleBot?: {\n index?: boolean\n follow?: boolean\n }\n }\n openGraph?: {\n type?: string\n url?: string\n title?: string\n description?: string\n siteName?: string\n images?: Array<{url: string; width?: number; height?: number; alt?: string}>\n }\n twitter?: {\n card?: string\n site?: string\n creator?: string\n title?: string\n description?: string\n images?: string[]\n }\n alternates?: {\n canonical?: string\n }\n /** Any custom meta attributes from seo.metaAttributes */\n other?: Record<string, string>\n}\n\n/** Default values used when SEO fields are missing. */\nexport interface SeoMetaDefaults {\n title?: string\n description?: string\n siteName?: string\n twitterSite?: string\n twitterCreator?: string\n /** Fallback image URL when no OG / Twitter image is set. */\n ogImage?: string\n}\n\n/**\n * Permissive image shape accepted by buildSeoMeta — compatible with both the\n * plugin's SanityImage and Sanity's code-generated image type (where `asset`\n * and `alt` are optional).\n */\ninterface SeoImageInput {\n _type?: string\n asset?: {_ref: string; _type: string; _weak?: boolean; [key: string]: unknown}\n hotspot?: unknown\n crop?: unknown\n alt?: string\n}\n\n/**\n * Input-compatible variant of SeoFields. Structurally matches Sanity's\n * code-generated types (where `asset`, `alt`, `key`, and `type` are all\n * optional), so you can pass `data.seo` from a sanityFetch result directly\n * without any `as any` or manual casting.\n */\nexport interface SeoFieldsInput {\n _type?: string\n robots?: {noIndex?: boolean | null; noFollow?: boolean | null} | null\n title?: string | null\n description?: string | null\n metaImage?: SeoImageInput | null\n metaAttributes?: Array<{_key?: string; key?: string; value?: string; type?: string}> | null\n keywords?: string[] | null\n canonicalUrl?: string | null\n openGraph?: {\n _type?: string\n url?: string | null\n title?: string | null\n description?: string | null\n siteName?: string | null\n type?: string | null\n imageType?: string | null\n image?: SeoImageInput | null\n imageUrl?: string | null\n } | null\n twitter?: {\n _type?: string\n card?: string | null\n site?: string | null\n creator?: string | null\n title?: string | null\n description?: string | null\n imageType?: string | null\n image?: SeoImageInput | null\n imageUrl?: string | null\n } | null\n}\n\n/** Options accepted by buildSeoMeta(). */\nexport interface BuildSeoMetaOptions {\n /**\n * The raw SEO object from Sanity (_type excluded or included — both work).\n * Pass `null` or `undefined` to fall back entirely to `defaults`.\n *\n * Accepts both the strict plugin `SeoFields` type and Sanity's code-generated\n * type (which has all nested fields optional) without any `as any` cast.\n */\n seo?: SeoFieldsInput | null\n\n /**\n * The base URL of your site, e.g. \"https://example.com\".\n * Used for canonical URL and OpenGraph URL construction.\n */\n baseUrl?: string\n\n /**\n * The path for the current page, e.g. \"/about\".\n * Combined with baseUrl to produce the canonical + OG url.\n * Defaults to \"\".\n */\n path?: string\n\n /**\n * Default values used when the Sanity SEO fields are empty / missing.\n */\n defaults?: SeoMetaDefaults\n\n /**\n * Resolve a Sanity image asset to a plain URL string.\n *\n * @example (using @sanity/image-url)\n * imageUrlResolver: (img) => urlFor(img).width(1200).url()\n */\n imageUrlResolver?: (image: SanityImage | SanityImageWithAlt) => string | null | undefined\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nconst VALID_OG_TYPES = [\n 'website',\n 'article',\n 'profile',\n 'book',\n 'music',\n 'video',\n 'product',\n] as const\ntype OGType = (typeof VALID_OG_TYPES)[number]\n\n/**\n * Coerce an arbitrary string to a valid OpenGraph type.\n * Falls back to \"website\" when the value is invalid.\n */\nexport function sanitizeOGType(value?: string): OGType {\n if (value && (VALID_OG_TYPES as readonly string[]).includes(value)) {\n return value as OGType\n }\n return 'website'\n}\n\nconst VALID_TWITTER_CARDS = ['summary', 'summary_large_image', 'app', 'player'] as const\ntype TwitterCard = (typeof VALID_TWITTER_CARDS)[number]\n\n/**\n * Coerce an arbitrary string to a valid Twitter card type.\n * Falls back to \"summary_large_image\" when the value is invalid.\n */\nexport function sanitizeTwitterCard(value?: string): TwitterCard {\n if (value && (VALID_TWITTER_CARDS as readonly string[]).includes(value)) {\n return value as TwitterCard\n }\n return 'summary_large_image'\n}\n\n// ─── Core builder ─────────────────────────────────────────────────────────────\n\n/**\n * Convert a Sanity SEO object into a structured metadata object.\n *\n * The return value is structurally compatible with Next.js App Router's\n * `Metadata` type, so you can return it directly from `generateMetadata()`.\n *\n * @example Next.js App Router\n * ```ts\n * import { buildSeoMeta } from 'sanity-plugin-seofields'\n * import { urlFor } from '@/sanity/lib/image'\n *\n * export async function generateMetadata(): Promise<Metadata> {\n * const { seo } = await sanityFetch({ query: PAGE_SEO_QUERY })\n * return buildSeoMeta({\n * seo,\n * baseUrl: process.env.NEXT_PUBLIC_SITE_URL,\n * path: '/about',\n * defaults: { title: 'My Site', siteName: 'My Site' },\n * imageUrlResolver: (img) => urlFor(img).width(1200).url(),\n * })\n * }\n * ```\n */\nexport function buildSeoMeta(options: BuildSeoMetaOptions): SeoMetadata {\n const {seo, baseUrl = '', path = '', defaults = {}, imageUrlResolver} = options\n\n const normalizedBase = baseUrl.replace(/\\/+$/, '') // remove trailing /\n const normalizedPath = path.replace(/^\\/+/, '') // remove leading /\n\n const fullUrl = [normalizedBase, normalizedPath].filter(Boolean).join('/')\n\n // ── OG image resolution ──\n let ogImageURL: string = defaults.ogImage || ''\n if (seo?.openGraph?.imageType === 'url' && seo.openGraph.imageUrl) {\n ogImageURL = seo.openGraph.imageUrl\n } else if (seo?.openGraph?.image && imageUrlResolver) {\n ogImageURL = imageUrlResolver(seo.openGraph.image as SanityImage) || ogImageURL\n }\n\n // ── Twitter image resolution ──\n let twitterImageURL: string = ogImageURL // reuse OG image as fallback\n if (seo?.twitter?.imageType === 'url' && seo.twitter.imageUrl) {\n twitterImageURL = seo.twitter.imageUrl\n } else if (seo?.twitter?.image && imageUrlResolver) {\n twitterImageURL = imageUrlResolver(seo.twitter.image as SanityImage) || twitterImageURL\n }\n\n // ── Custom meta attributes → `other` map ──\n const other: Record<string, string> = {}\n if (Array.isArray(seo?.metaAttributes)) {\n for (const attr of seo!.metaAttributes!) {\n if (attr.key && attr.value) {\n other[attr.key] = attr.value\n }\n }\n }\n\n const ogUrl = seo?.openGraph?.url || fullUrl\n\n return {\n title: seo?.title ?? defaults.title ?? null,\n description: seo?.description ?? defaults.description ?? null,\n keywords: seo?.keywords?.length ? (seo.keywords as string[]) : undefined,\n robots: {\n index: !seo?.robots?.noIndex,\n follow: !seo?.robots?.noFollow,\n googleBot: {\n index: !seo?.robots?.noIndex,\n follow: !seo?.robots?.noFollow,\n },\n },\n openGraph: {\n type: sanitizeOGType(seo?.openGraph?.type ?? undefined),\n url: ogUrl || undefined,\n title: seo?.openGraph?.title ?? defaults.title,\n description: seo?.openGraph?.description ?? defaults.description,\n siteName: seo?.openGraph?.siteName ?? defaults.siteName,\n images: ogImageURL ? [{url: ogImageURL}] : [],\n },\n twitter: {\n card: sanitizeTwitterCard(seo?.twitter?.card ?? undefined),\n site: seo?.twitter?.site ?? defaults.twitterSite,\n creator: seo?.twitter?.creator ?? defaults.twitterCreator,\n title: seo?.twitter?.title ?? defaults.title,\n description: seo?.twitter?.description ?? defaults.description,\n images: twitterImageURL ? [twitterImageURL] : [],\n },\n alternates: {\n canonical: fullUrl || undefined,\n },\n ...(Object.keys(other).length > 0 ? {other} : {}),\n }\n}\n","/**\n * <SeoMetaTags> — Framework-agnostic React SEO meta tag renderer.\n *\n * Renders all SEO meta tags as plain React elements.\n * Place it inside your framework's <Head> component:\n *\n * @example Next.js Pages Router\n * ```tsx\n * import Head from 'next/head'\n * import { SeoMetaTags } from 'sanity-plugin-seofields'\n *\n * export default function Page({ seo }) {\n * return (\n * <>\n * <Head>\n * <SeoMetaTags\n * data={seo}\n * baseUrl=\"https://example.com\"\n * path=\"/about\"\n * defaults={{ title: 'My Site', siteName: 'My Site' }}\n * imageUrlResolver={(img) => urlFor(img).width(1200).url()}\n * />\n * </Head>\n * <main>...</main>\n * </>\n * )\n * }\n * ```\n *\n * @example Nuxt 3 / generic SSR (inside <Head> slot)\n * ```tsx\n * <Head>\n * <SeoMetaTags data={seo} baseUrl=\"https://example.com\" path=\"/\" />\n * </Head>\n * ```\n */\nimport React from 'react'\n\nimport type {SanityImage, SanityImageWithAlt, SeoFields} from '../types'\nimport {buildSeoMeta, type BuildSeoMetaOptions} from './seoMeta'\n\n// ─── Props ────────────────────────────────────────────────────────────────────\n\nexport interface SeoMetaTagsProps {\n /**\n * The raw SEO object from Sanity.\n * Pass `null` / `undefined` to render only the defaults.\n */\n data?: Partial<SeoFields> | null\n\n /**\n * Base URL of your site, e.g. \"https://example.com\".\n * Used for canonical link, og:url fallback.\n */\n baseUrl?: string\n\n /**\n * Current page path, e.g. \"/about\".\n * Defaults to \"\".\n */\n path?: string\n\n /**\n * Default values used when SEO fields are missing.\n */\n defaults?: BuildSeoMetaOptions['defaults']\n\n /**\n * Resolve a Sanity image asset reference to a full URL string.\n *\n * @example\n * imageUrlResolver={(img) => urlFor(img).width(1200).url()}\n */\n imageUrlResolver?: (image: SanityImage | SanityImageWithAlt) => string | null | undefined\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * Renders all SEO meta tags for a page as plain React elements.\n * Intended to be placed inside your framework's <Head> / <head> component.\n *\n * Renders:\n * - `<title>`\n * - `<meta name=\"description\">`\n * - `<meta name=\"keywords\">`\n * - `<meta name=\"robots\">`\n * - OpenGraph meta tags (`og:*`)\n * - Twitter Card meta tags (`twitter:*`)\n * - Any custom `seo.metaAttributes` as `<meta name=\"...\" content=\"...\">`\n */\nexport function SeoMetaTags({data, baseUrl, path, defaults, imageUrlResolver}: SeoMetaTagsProps) {\n const meta = buildSeoMeta({seo: data, baseUrl, path, defaults, imageUrlResolver})\n\n const robotsContent = [\n meta.robots?.index === false ? 'noindex' : 'index',\n meta.robots?.follow === false ? 'nofollow' : 'follow',\n ].join(', ')\n\n return (\n <>\n {/* ── Title ── */}\n {meta.title && <title>{meta.title}</title>}\n\n {/* ── Basic meta ── */}\n {meta.description && <meta name=\"description\" content={meta.description} />}\n {meta.keywords?.length ? <meta name=\"keywords\" content={meta.keywords.join(', ')} /> : null}\n <meta name=\"robots\" content={robotsContent} />\n <meta name=\"googlebot\" content={robotsContent} />\n\n {/* ── Open Graph ── */}\n {meta.openGraph?.type && <meta property=\"og:type\" content={meta.openGraph.type} />}\n {meta.openGraph?.url && <meta property=\"og:url\" content={meta.openGraph.url} />}\n {meta.openGraph?.title && <meta property=\"og:title\" content={meta.openGraph.title} />}\n {meta.openGraph?.description && (\n <meta property=\"og:description\" content={meta.openGraph.description} />\n )}\n {meta.openGraph?.siteName && (\n <meta property=\"og:site_name\" content={meta.openGraph.siteName} />\n )}\n {meta.openGraph?.images?.map((img, i) => (\n <React.Fragment key={`og-img-${i}`}>\n <meta property=\"og:image\" content={img.url} />\n {img.width && <meta property=\"og:image:width\" content={String(img.width)} />}\n {img.height && <meta property=\"og:image:height\" content={String(img.height)} />}\n {img.alt && <meta property=\"og:image:alt\" content={img.alt} />}\n </React.Fragment>\n ))}\n\n {/* ── Twitter Card ── */}\n {meta.twitter?.card && <meta name=\"twitter:card\" content={meta.twitter.card} />}\n {meta.twitter?.site && <meta name=\"twitter:site\" content={meta.twitter.site} />}\n {meta.twitter?.creator && <meta name=\"twitter:creator\" content={meta.twitter.creator} />}\n {meta.twitter?.title && <meta name=\"twitter:title\" content={meta.twitter.title} />}\n {meta.twitter?.description && (\n <meta name=\"twitter:description\" content={meta.twitter.description} />\n )}\n {meta.twitter?.images?.map((url, i) => (\n <meta key={`tw-img-${i}`} name=\"twitter:image\" content={url} />\n ))}\n\n {/* ── Custom meta attributes ── */}\n {meta.other &&\n Object.entries(meta.other).map(([name, content]) => (\n <meta key={`custom-${name}`} name={name} content={content} />\n ))}\n\n {/* ── Canonical URL ── */}\n {meta.alternates?.canonical && <link rel=\"canonical\" href={meta.alternates.canonical} />}\n </>\n )\n}\n\nexport default SeoMetaTags\n"],"mappings":"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","names":["React"]}
|
package/dist/next.d.cts
CHANGED
|
@@ -238,4 +238,4 @@ interface SeoMetaTagsProps {
|
|
|
238
238
|
*/
|
|
239
239
|
declare function SeoMetaTags({ data, baseUrl, path, defaults, imageUrlResolver }: SeoMetaTagsProps): react_jsx_runtime.JSX.Element;
|
|
240
240
|
|
|
241
|
-
export { type BuildSeoMetaOptions, type SeoMetaDefaults, SeoMetaTags, type SeoMetaTagsProps, type SeoMetadata, buildSeoMeta, sanitizeOGType, sanitizeTwitterCard };
|
|
241
|
+
export { type BuildSeoMetaOptions, type SeoFieldsInput, type SeoMetaDefaults, SeoMetaTags, type SeoMetaTagsProps, type SeoMetadata, buildSeoMeta, sanitizeOGType, sanitizeTwitterCard };
|
package/dist/next.d.ts
CHANGED
|
@@ -238,4 +238,4 @@ interface SeoMetaTagsProps {
|
|
|
238
238
|
*/
|
|
239
239
|
declare function SeoMetaTags({ data, baseUrl, path, defaults, imageUrlResolver }: SeoMetaTagsProps): react_jsx_runtime.JSX.Element;
|
|
240
240
|
|
|
241
|
-
export { type BuildSeoMetaOptions, type SeoMetaDefaults, SeoMetaTags, type SeoMetaTagsProps, type SeoMetadata, buildSeoMeta, sanitizeOGType, sanitizeTwitterCard };
|
|
241
|
+
export { type BuildSeoMetaOptions, type SeoFieldsInput, type SeoMetaDefaults, SeoMetaTags, type SeoMetaTagsProps, type SeoMetadata, buildSeoMeta, sanitizeOGType, sanitizeTwitterCard };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-seofields",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "A Sanity Studio plugin to manage SEO fields like meta titles, descriptions, and Open Graph tags for structured, search-optimized content.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -52,8 +52,10 @@
|
|
|
52
52
|
"format": "prettier --write --cache --ignore-unknown .",
|
|
53
53
|
"lint": "eslint .",
|
|
54
54
|
"prepublishOnly": "npm run build",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "jest",
|
|
57
|
+
"test:watch": "jest --watch",
|
|
58
|
+
"test:coverage": "jest --coverage"
|
|
57
59
|
},
|
|
58
60
|
"sanityPlugin": {
|
|
59
61
|
"linkWatch": {
|
|
@@ -78,9 +80,13 @@
|
|
|
78
80
|
"devDependencies": {
|
|
79
81
|
"@sanity/codegen": "^4.9.0",
|
|
80
82
|
"@sanity/plugin-kit": "^4.0.20",
|
|
83
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
84
|
+
"@testing-library/react": "^16.3.2",
|
|
85
|
+
"@types/jest": "^30.0.0",
|
|
81
86
|
"@types/react": "^19.1.13",
|
|
82
87
|
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
|
83
88
|
"@typescript-eslint/parser": "^8.44.0",
|
|
89
|
+
"babel-jest": "^30.3.0",
|
|
84
90
|
"baseline-browser-mapping": "^2.10.8",
|
|
85
91
|
"eslint": "^8.57.1",
|
|
86
92
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -88,12 +94,18 @@
|
|
|
88
94
|
"eslint-plugin-prettier": "^5.5.4",
|
|
89
95
|
"eslint-plugin-react": "^7.37.5",
|
|
90
96
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
97
|
+
"husky": "^9.1.7",
|
|
98
|
+
"identity-obj-proxy": "^3.0.0",
|
|
99
|
+
"jest": "^30.3.0",
|
|
100
|
+
"jest-environment-jsdom": "^30.3.0",
|
|
101
|
+
"lint-staged": "^16.4.0",
|
|
91
102
|
"prettier": "^3.6.2",
|
|
92
103
|
"prettier-plugin-packagejson": "^2.5.19",
|
|
93
104
|
"react": "^19.1.1",
|
|
94
105
|
"react-dom": "^19.1.1",
|
|
95
106
|
"sanity": "^4.10.0",
|
|
96
107
|
"styled-components": "^6.1.19",
|
|
108
|
+
"ts-jest": "^29.4.6",
|
|
97
109
|
"ts-json-schema-generator": "^2.4.0",
|
|
98
110
|
"tsup": "^8.0.0",
|
|
99
111
|
"tsx": "^4.20.5",
|