├── .eslintignore ├── .eslintrc ├── .prettierignore ├── app ├── globals.css ├── favicon.ico ├── (sanity) │ ├── icon.ico │ ├── icon.png │ ├── apple-icon.png │ ├── studio │ │ └── [[...tool]] │ │ │ └── page.tsx │ ├── layout.tsx │ └── icon.svg ├── (blog) │ ├── date.tsx │ ├── actions.ts │ ├── cover-image.tsx │ ├── avatar.tsx │ ├── portable-text.tsx │ ├── alert-banner.tsx │ ├── more-stories.tsx │ ├── onboarding.tsx │ ├── layout.tsx │ ├── posts │ │ └── [slug] │ │ │ └── page.tsx │ └── page.tsx └── api │ └── draft │ └── route.tsx ├── sanity-typegen.json ├── images ├── og.png ├── screenshot.png └── deploy-to-vercel.png ├── postcss.config.js ├── languages.json ├── .env.example ├── next.config.js ├── sanity ├── lib │ ├── token.ts │ ├── client.ts │ ├── utils.ts │ ├── api.ts │ ├── demo.ts │ ├── fetch.ts │ └── queries.ts ├── schemas │ ├── singletons │ │ ├── localizedString.ts │ │ └── settings.tsx │ └── documents │ │ ├── comment.ts │ │ ├── author.ts │ │ ├── user.ts │ │ ├── tag.ts │ │ ├── group.ts │ │ ├── appType.ts │ │ ├── guide.ts │ │ ├── category.ts │ │ ├── submission.ts │ │ ├── application.ts │ │ ├── post.ts │ │ └── product.ts ├── defaultDocumentNode.ts └── plugins │ ├── locate.ts │ ├── settings.tsx │ └── assist.ts ├── sanity.cli.ts ├── tailwind.config.ts ├── languages.js ├── .gitignore ├── tsconfig.json ├── LICENSE.md ├── sanity-patch.js ├── sanity-fix.js ├── package.json ├── sanity.config.ts ├── README.md └── sanity.types.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignoring generated files 2 | ./sanity.types.ts 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "root": true 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignoring generated files 2 | ./sanity.types.ts 3 | ./schema.json 4 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /sanity-typegen.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "'./{app,sanity}/**/*.{ts,tsx,js,jsx}'" 3 | } 4 | -------------------------------------------------------------------------------- /images/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/images/og.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/(sanity)/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/app/(sanity)/icon.ico -------------------------------------------------------------------------------- /app/(sanity)/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/app/(sanity)/icon.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /app/(sanity)/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/app/(sanity)/apple-icon.png -------------------------------------------------------------------------------- /images/deploy-to-vercel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javayhu/free-directory-sanity/HEAD/images/deploy-to-vercel.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n": { 3 | "languages": [ 4 | { "id": "en", "title": "English (US)", "isDefault": true }, 5 | { "id": "zh", "title": "简体中文" } 6 | ], 7 | "base": "en" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # Sanity 3 | # ----------------------------------------------------------------------------- 4 | NEXT_PUBLIC_SANITY_PROJECT_ID= 5 | NEXT_PUBLIC_SANITY_DATASET= 6 | SANITY_API_READ_TOKEN= 7 | -------------------------------------------------------------------------------- /app/(blog)/date.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | export default function DateComponent({ dateString }: { dateString: string }) { 4 | return ( 5 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/(sanity)/studio/[[...tool]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { NextStudio } from "next-sanity/studio"; 2 | 3 | import config from "@/sanity.config"; 4 | 5 | export const dynamic = "force-static"; 6 | 7 | export default function StudioPage() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | experimental: { 4 | // Used to guard against accidentally leaking SANITY_API_READ_TOKEN to the browser 5 | taint: true, 6 | }, 7 | logging: { 8 | fetches: { fullUrl: false }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /app/(blog)/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { draftMode } from "next/headers"; 4 | 5 | export async function disableDraftMode() { 6 | "use server"; 7 | await Promise.allSettled([ 8 | draftMode().disable(), 9 | // Simulate a delay to show the loading state 10 | new Promise((resolve) => setTimeout(resolve, 1000)), 11 | ]); 12 | } 13 | -------------------------------------------------------------------------------- /sanity/lib/token.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { experimental_taintUniqueValue } from "react"; 4 | 5 | export const token = process.env.SANITY_API_READ_TOKEN; 6 | 7 | if (!token) { 8 | throw new Error("Missing SANITY_API_READ_TOKEN"); 9 | } 10 | 11 | experimental_taintUniqueValue( 12 | "Do not pass the sanity API read token to the client.", 13 | process, 14 | token, 15 | ); 16 | -------------------------------------------------------------------------------- /sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import { loadEnvConfig } from "@next/env"; 2 | import { defineCliConfig } from "sanity/cli"; 3 | 4 | const dev = process.env.NODE_ENV !== "production"; 5 | loadEnvConfig(__dirname, dev, { info: () => null, error: console.error }); 6 | 7 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID; 8 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET; 9 | 10 | export default defineCliConfig({ api: { projectId, dataset } }); 11 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import typography from "@tailwindcss/typography"; 3 | 4 | export default { 5 | content: ["./app/**/*.{ts,tsx}", "./sanity/**/*.{ts,tsx}"], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | sans: ["var(--font-inter)"], 10 | }, 11 | }, 12 | }, 13 | future: { 14 | hoverOnlyWhenSupported: true, 15 | }, 16 | plugins: [typography], 17 | } satisfies Config; 18 | -------------------------------------------------------------------------------- /languages.js: -------------------------------------------------------------------------------- 1 | const languages = [ 2 | {id: 'en', title: 'English', isDefault: true}, 3 | {id: 'zh', title: '简体中文'}, 4 | ] 5 | 6 | const i18n = { 7 | languages, 8 | base: languages.find((item) => item.isDefault).id, 9 | } 10 | 11 | const googleTranslateLanguages = languages.map(({id, title}) => ({id, title})) 12 | 13 | // For v2 studio 14 | // module.exports = {i18n, googleTranslateLanguages} 15 | 16 | // For v3 studio 17 | export {i18n, googleTranslateLanguages} 18 | -------------------------------------------------------------------------------- /app/(sanity)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../globals.css"; 2 | 3 | import { Inter } from "next/font/google"; 4 | 5 | const inter = Inter({ 6 | variable: "--font-inter", 7 | subsets: ["latin"], 8 | display: "swap", 9 | }); 10 | 11 | export { metadata, viewport } from "next-sanity/studio"; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /sanity/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "next-sanity"; 2 | 3 | import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; 4 | 5 | export const client = createClient({ 6 | projectId, 7 | dataset, 8 | apiVersion, 9 | useCdn: true, 10 | perspective: "published", 11 | stega: { 12 | studioUrl, 13 | logger: console, 14 | filter: (props) => { 15 | if (props.sourcePath.at(-1) === "title") { 16 | return true; 17 | } 18 | 19 | return props.filterDefault(props); 20 | }, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /sanity/schemas/singletons/localizedString.ts: -------------------------------------------------------------------------------- 1 | import {defineField, defineType} from 'sanity'; 2 | 3 | import {i18n} from '../../../languages'; 4 | 5 | export default defineType({ 6 | name: 'localizedString', 7 | title: 'Localized String', 8 | type: 'object', 9 | fieldsets: [ 10 | { 11 | title: 'Translations', 12 | name: 'translations', 13 | options: {collapsible: true, collapsed: false}, 14 | }, 15 | ], 16 | fields: i18n.languages.map((lang) => 17 | defineField({ 18 | name: lang.id, 19 | title: lang.title, 20 | type: 'string', 21 | fieldset: lang.isDefault ? undefined : 'translations', 22 | }) 23 | ), 24 | }) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /studio/node_modules 6 | /.pnp 7 | .pnp.js 8 | .yarn/install-state.gz 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | /studio/dist 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | # Env files created by scripts for working locally 41 | .env 42 | .env.local -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "module": "preserve", 11 | "isolatedModules": true, 12 | "jsx": "preserve", 13 | "incremental": true, 14 | "plugins": [ 15 | { 16 | "name": "next" 17 | } 18 | ], 19 | "paths": { 20 | "@/*": ["./*"] 21 | } 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "sanity-fix.js"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /sanity/schemas/documents/comment.ts: -------------------------------------------------------------------------------- 1 | import { CommentIcon } from "@sanity/icons"; 2 | import { defineType } from "sanity"; 3 | 4 | // for demo 5 | export const comment = defineType({ 6 | name: "comment", 7 | title: "Comment", 8 | icon: CommentIcon, 9 | type: "document", 10 | fields: [ 11 | { 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | // readOnly: true, 16 | }, 17 | { 18 | name: "email", 19 | title: "Email", 20 | type: "string", 21 | // readOnly: true, 22 | }, 23 | { 24 | name: "comment", 25 | title: "Comment", 26 | type: "text", 27 | // readOnly: true, 28 | }, 29 | { 30 | name: "post", 31 | title: "Post", 32 | type: "reference", 33 | to: [{ type: "product" }], 34 | } 35 | ], 36 | }) -------------------------------------------------------------------------------- /app/(blog)/cover-image.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "next-sanity/image"; 2 | 3 | import { urlForImage } from "@/sanity/lib/utils"; 4 | 5 | interface CoverImageProps { 6 | image: any; 7 | priority?: boolean; 8 | } 9 | 10 | export default function CoverImage(props: CoverImageProps) { 11 | const { image: source, priority } = props; 12 | const image = source?.asset?._ref ? ( 13 | {source?.alt 22 | ) : ( 23 |
24 | ); 25 | 26 | return ( 27 |
28 | {image} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/api/draft/route.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used to allow Presentation to set the app in Draft Mode, which will load Visual Editing 3 | * and query draft content and preview the content as it will appear once everything is published 4 | */ 5 | 6 | import { validatePreviewUrl } from "@sanity/preview-url-secret"; 7 | import { draftMode } from "next/headers"; 8 | import { redirect } from "next/navigation"; 9 | 10 | import { client } from "@/sanity/lib/client"; 11 | import { token } from "@/sanity/lib/token"; 12 | 13 | const clientWithToken = client.withConfig({ token }); 14 | 15 | export async function GET(request: Request) { 16 | const { isValid, redirectTo = "/" } = await validatePreviewUrl( 17 | clientWithToken, 18 | request.url, 19 | ); 20 | if (!isValid) { 21 | return new Response("Invalid secret", { status: 401 }); 22 | } 23 | 24 | draftMode().enable(); 25 | 26 | redirect(redirectTo); 27 | } 28 | -------------------------------------------------------------------------------- /app/(blog)/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "next-sanity/image"; 2 | 3 | import type { Author } from "@/sanity.types"; 4 | import { urlForImage } from "@/sanity/lib/utils"; 5 | 6 | interface Props { 7 | name: string; 8 | picture: Exclude | null; 9 | } 10 | 11 | export default function Avatar({ name, picture }: Props) { 12 | return ( 13 |
14 | {picture?.asset?._ref ? ( 15 |
16 | {picture?.alt 29 |
30 | ) : ( 31 |
By
32 | )} 33 |
{name}
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/(sanity)/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 javayhu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sanity-patch.js: -------------------------------------------------------------------------------- 1 | import { client } from "@/sanity/lib/client"; 2 | 3 | // const client = sanityClient.withConfig({ apiVersion: '2024-02-28' }); 4 | 5 | const query = `*[_type == 'produt' && featured == true]` //get all of your posts that do not have isHighlighted set 6 | 7 | // https://www.sanity.io/answers/updating-default-field-value-in-sanity-io-using-a-script 8 | const mutateDocs = async (query) => { 9 | const docsToMutate = await client.fetch(query, {}); 10 | for (const doc of docsToMutate) { 11 | const mutation = { 12 | featured: false 13 | } 14 | console.log('uploading'); 15 | client 16 | .patch(doc._id) // Document ID to patch 17 | .set(mutation) // Shallow merge 18 | .commit() // Perform the patch and return a promise 19 | .then((updatedDoc) => { 20 | console.log('Hurray, the doc is updated! New document:'); 21 | console.log(updatedDoc._id); 22 | }) 23 | .catch((err) => { 24 | console.error('Oh no, the update failed: ', err.message); 25 | }) 26 | } 27 | } 28 | 29 | mutateDocs(query); -------------------------------------------------------------------------------- /sanity/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import createImageUrlBuilder from "@sanity/image-url"; 2 | 3 | import { dataset, projectId } from "@/sanity/lib/api"; 4 | 5 | const imageBuilder = createImageUrlBuilder({ 6 | projectId: projectId || "", 7 | dataset: dataset || "", 8 | }); 9 | 10 | export const urlForImage = (source: any) => { 11 | // Ensure that source image contains a valid reference 12 | if (!source?.asset?._ref) { 13 | return undefined; 14 | } 15 | 16 | return imageBuilder?.image(source).auto("format").fit("max"); 17 | }; 18 | 19 | export function resolveOpenGraphImage(image: any, width = 1200, height = 627) { 20 | if (!image) return; 21 | const url = urlForImage(image)?.width(1200).height(627).fit("crop").url(); 22 | if (!url) return; 23 | return { url, alt: image?.alt as string, width, height }; 24 | } 25 | 26 | export function resolveHref( 27 | documentType?: string, 28 | slug?: string, 29 | ): string | undefined { 30 | switch (documentType) { 31 | case "post": 32 | return slug ? `/posts/${slug}` : undefined; 33 | default: 34 | console.warn("Invalid document type:", documentType); 35 | return undefined; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sanity/lib/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * As this file is reused in several other files, try to keep it lean and small. 3 | * Importing other npm packages here could lead to needlessly increasing the client bundle size, or end up in a server-only function that don't need it. 4 | */ 5 | 6 | function assertValue(v: T | undefined, errorMessage: string): T { 7 | if (v === undefined) { 8 | throw new Error(errorMessage); 9 | } 10 | 11 | return v; 12 | } 13 | 14 | export const dataset = assertValue( 15 | process.env.NEXT_PUBLIC_SANITY_DATASET, 16 | "Missing environment variable: NEXT_PUBLIC_SANITY_DATASET", 17 | ); 18 | 19 | export const projectId = assertValue( 20 | process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, 21 | "Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID", 22 | ); 23 | 24 | /** 25 | * see https://www.sanity.io/docs/api-versioning for how versioning works 26 | */ 27 | export const apiVersion = 28 | process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-28"; 29 | 30 | /** 31 | * Used to configure edit intent links, for Presentation Mode, as well as to configure where the Studio is mounted in the router. 32 | */ 33 | export const studioUrl = "/studio"; 34 | -------------------------------------------------------------------------------- /sanity/schemas/documents/author.ts: -------------------------------------------------------------------------------- 1 | import { UsersIcon } from "@sanity/icons"; 2 | import { defineField, defineType } from "sanity"; 3 | 4 | // for demo 5 | export default defineType({ 6 | name: "author", 7 | title: "Author", 8 | icon: UsersIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | validation: (rule) => rule.required(), 16 | }), 17 | defineField({ 18 | name: "picture", 19 | title: "Picture", 20 | type: "image", 21 | fields: [ 22 | { 23 | name: "alt", 24 | type: "string", 25 | title: "Alternative text", 26 | description: "Important for SEO and accessiblity.", 27 | validation: (rule) => { 28 | return rule.custom((alt, context) => { 29 | if ((context.document?.picture as any)?.asset?._ref && !alt) { 30 | return "Required"; 31 | } 32 | return true; 33 | }); 34 | }, 35 | }, 36 | ], 37 | options: { 38 | hotspot: true, 39 | aiAssist: { 40 | imageDescriptionField: "alt", 41 | }, 42 | }, 43 | validation: (rule) => rule.required(), 44 | }), 45 | ], 46 | }); 47 | -------------------------------------------------------------------------------- /app/(blog)/portable-text.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This component uses Portable Text to render a post body. 3 | * 4 | * You can learn more about Portable Text on: 5 | * https://www.sanity.io/docs/block-content 6 | * https://github.com/portabletext/react-portabletext 7 | * https://portabletext.org/ 8 | * 9 | */ 10 | 11 | import { 12 | PortableText, 13 | type PortableTextComponents, 14 | type PortableTextBlock, 15 | } from "next-sanity"; 16 | 17 | export default function CustomPortableText({ 18 | className, 19 | value, 20 | }: { 21 | className?: string; 22 | value: PortableTextBlock[]; 23 | }) { 24 | const components: PortableTextComponents = { 25 | block: { 26 | h5: ({ children }) => ( 27 |
{children}
28 | ), 29 | h6: ({ children }) => ( 30 |
{children}
31 | ), 32 | }, 33 | marks: { 34 | link: ({ children, value }) => { 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }, 41 | }, 42 | }; 43 | 44 | return ( 45 |
46 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /sanity/defaultDocumentNode.ts: -------------------------------------------------------------------------------- 1 | import { isDev, type SanityDocument } from 'sanity'; 2 | import { Iframe } from 'sanity-plugin-iframe-pane'; 3 | import type { DefaultDocumentNodeResolver } from 'sanity/structure'; 4 | 5 | const previewUrl = 'https://indie-hackers-site-sanity.vercel.app'; 6 | 7 | const defaultDocumentNode: DefaultDocumentNodeResolver = ( 8 | S, 9 | { schemaType }, 10 | ) => { 11 | const editorView = S.view.form(); 12 | 13 | switch (schemaType) { 14 | case 'post': 15 | return S.document().views([ 16 | editorView, 17 | S.view 18 | .component(Iframe) 19 | .title('Preview') 20 | .options({ 21 | url: ( 22 | doc: SanityDocument & { 23 | slug?: { current: string } 24 | }, 25 | ) => { 26 | const base = isDev ? 'http://localhost:3001' : previewUrl; 27 | const slug = doc?.slug?.current; 28 | const path = slug === 'index' ? '' : slug; 29 | const directory = 'posts'; 30 | 31 | console.log('preview, url', [base, directory, path].filter(Boolean).join('/')); 32 | return [base, directory, path].filter(Boolean).join('/'); 33 | }, 34 | reload: { 35 | button: true, 36 | }, 37 | }), 38 | ]) 39 | 40 | default: 41 | return S.document().views([editorView]) 42 | } 43 | } 44 | 45 | export default defaultDocumentNode 46 | -------------------------------------------------------------------------------- /sanity-fix.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const filePath = 'sanity.types.ts'; 4 | // const searchString = 'Array<{\n _type: "localizedString";\n en?: string;\n zh?: string;\n }> | null;'; 5 | // const searchString = 'Array<{\n\\s*_type:\\s*"localizedString";\n\\s*en?:\\s*string;\n\\s*zh?:\\s*string;\n\\s*}>\\s*\\|\\s*null;'; 6 | const searchString = /Array<\{[^{}]*_type:\s*"localizedString";[^{}]*\}>\s*\|\s*null;/g; 7 | const replacementString = 'string;'; 8 | 9 | fs.readFile(filePath, 'utf8', (err, data) => { 10 | if (err) { 11 | console.error(err); 12 | return; 13 | } 14 | 15 | // const updatedData = data.replaceAll(searchString, replacementString); 16 | 17 | // const regex = new RegExp(searchString, 'g'); 18 | // const updatedData = data.replace(regex, replacementString); 19 | 20 | const regex = new RegExp(searchString, 'g'); 21 | let replacementCount = 0; 22 | 23 | const updatedData = data.replace(regex, (match) => { 24 | replacementCount++; 25 | return replacementString; 26 | }); 27 | console.log('Replacement count:', replacementCount); 28 | 29 | fs.writeFile(filePath, updatedData, 'utf8', (err) => { 30 | if (err) { 31 | console.error(err); 32 | return; 33 | } 34 | 35 | console.log('Replacement done successfully!'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /sanity/lib/demo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Demo data used as placeholders and initial values for the blog 3 | */ 4 | 5 | export const title = "Blog."; 6 | 7 | export const description = [ 8 | { 9 | _key: "9f1a629887fd", 10 | _type: "block", 11 | children: [ 12 | { 13 | _key: "4a58edd077880", 14 | _type: "span", 15 | marks: [], 16 | text: "A statically generated blog example using ", 17 | }, 18 | { 19 | _key: "4a58edd077881", 20 | _type: "span", 21 | marks: ["ec5b66c9b1e0"], 22 | text: "Next.js", 23 | }, 24 | { 25 | _key: "4a58edd077882", 26 | _type: "span", 27 | marks: [], 28 | text: " and ", 29 | }, 30 | { 31 | _key: "4a58edd077883", 32 | _type: "span", 33 | marks: ["1f8991913ea8"], 34 | text: "Sanity", 35 | }, 36 | { 37 | _key: "4a58edd077884", 38 | _type: "span", 39 | marks: [], 40 | text: ".", 41 | }, 42 | ], 43 | markDefs: [ 44 | { 45 | _key: "ec5b66c9b1e0", 46 | _type: "link", 47 | href: "https://nextjs.org/", 48 | }, 49 | { 50 | _key: "1f8991913ea8", 51 | _type: "link", 52 | href: "https://sanity.io/", 53 | }, 54 | ], 55 | style: "normal", 56 | }, 57 | ]; 58 | 59 | export const ogImageTitle = "A Next.js Blog with a Native Authoring Experience"; 60 | -------------------------------------------------------------------------------- /sanity/schemas/documents/user.ts: -------------------------------------------------------------------------------- 1 | import { UsersIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "user", 7 | title: "User", 8 | icon: UsersIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | validation: (rule) => rule.required(), 16 | }), 17 | defineField({ 18 | name: "id", 19 | title: "Id", 20 | type: "string", 21 | }), 22 | defineField({ 23 | name: "email", 24 | title: "Email", 25 | type: "string", 26 | }), 27 | defineField({ 28 | name: "avatar", 29 | title: "Avatar", 30 | type: "string", 31 | }), 32 | defineField({ 33 | name: "link", 34 | title: "Link", 35 | type: "string", 36 | }), 37 | defineField({ 38 | name: "date", 39 | title: "Date", 40 | type: "datetime", 41 | initialValue: () => new Date().toISOString(), 42 | }), 43 | ], 44 | preview: { 45 | select: { 46 | title: "name", 47 | date: "date", 48 | }, 49 | prepare({ title, date }) { 50 | // can not show avatar 51 | const subtitles = [ 52 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 53 | ].filter(Boolean); 54 | return { title, subtitle: subtitles.join(" ") }; 55 | } 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /app/(blog)/alert-banner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useSyncExternalStore, useTransition } from "react"; 5 | 6 | import { disableDraftMode } from "./actions"; 7 | 8 | const emptySubscribe = () => () => {}; 9 | 10 | export default function AlertBanner() { 11 | const router = useRouter(); 12 | const [pending, startTransition] = useTransition(); 13 | 14 | const shouldShow = useSyncExternalStore( 15 | emptySubscribe, 16 | () => window.top === window, 17 | () => false, 18 | ); 19 | 20 | if (!shouldShow) return null; 21 | 22 | return ( 23 |
28 |
29 | {pending ? ( 30 | "Disabling draft mode..." 31 | ) : ( 32 | <> 33 | {"Previewing drafts. "} 34 | 47 | 48 | )} 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /sanity/schemas/documents/tag.ts: -------------------------------------------------------------------------------- 1 | import { TagsIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "tag", 7 | title: "Tag", 8 | icon: TagsIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | validation: (rule) => rule.required(), 16 | }), 17 | defineField({ 18 | name: "slug", 19 | title: "Slug", 20 | type: "slug", 21 | options: { 22 | source: "name", 23 | maxLength: 96, 24 | isUnique: (value, context) => context.defaultIsUnique(value, context), 25 | }, 26 | validation: (rule) => rule.required(), 27 | }), 28 | defineField({ 29 | name: "order", 30 | title: "Order", 31 | type: "number", 32 | initialValue: -1, 33 | }), 34 | defineField({ 35 | name: "date", 36 | title: "Date", 37 | type: "datetime", 38 | initialValue: () => new Date().toISOString(), 39 | }), 40 | ], 41 | preview: { 42 | select: { 43 | title: "name", 44 | order: "order", 45 | date: "date", 46 | }, 47 | prepare({ title, order, date }) { 48 | const subtitles = [ 49 | order && `order:${order}`, 50 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 51 | ].filter(Boolean); 52 | return { title, subtitle: subtitles.join(" ") }; 53 | } 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /sanity/schemas/documents/group.ts: -------------------------------------------------------------------------------- 1 | import { MenuIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "group", 7 | title: "Group", 8 | icon: MenuIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | // type: "string", 15 | type: "localizedString", 16 | validation: (rule) => rule.required(), 17 | }), 18 | defineField({ 19 | name: "slug", 20 | title: "Slug", 21 | type: "slug", 22 | options: { 23 | source: "name.en", 24 | maxLength: 96, 25 | isUnique: (value, context) => context.defaultIsUnique(value, context), 26 | }, 27 | validation: (rule) => rule.required(), 28 | }), 29 | defineField({ 30 | name: "order", 31 | title: "Order", 32 | type: "number", 33 | initialValue: -1, 34 | }), 35 | defineField({ 36 | name: "date", 37 | title: "Date", 38 | type: "datetime", 39 | initialValue: () => new Date().toISOString(), 40 | }), 41 | ], 42 | preview: { 43 | select: { 44 | title: "name.en", 45 | order: "order", 46 | date: "date", 47 | }, 48 | prepare({ title, order, date }) { 49 | const subtitles = [ 50 | order && `order:${order}`, 51 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 52 | ].filter(Boolean); 53 | return { title, subtitle: subtitles.join(" ") }; 54 | } 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /sanity/schemas/documents/appType.ts: -------------------------------------------------------------------------------- 1 | import { ComponentIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "appType", 7 | title: "AppType", 8 | icon: ComponentIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | // type: "string", 15 | type: "localizedString", 16 | validation: (rule) => rule.required(), 17 | }), 18 | defineField({ 19 | name: "slug", 20 | title: "Slug", 21 | type: "slug", 22 | options: { 23 | source: "name.en", 24 | maxLength: 96, 25 | isUnique: (value, context) => context.defaultIsUnique(value, context), 26 | }, 27 | validation: (rule) => rule.required(), 28 | }), 29 | defineField({ 30 | name: "order", 31 | title: "Order", 32 | type: "number", 33 | initialValue: -1, 34 | }), 35 | defineField({ 36 | name: "date", 37 | title: "Date", 38 | type: "datetime", 39 | initialValue: () => new Date().toISOString(), 40 | }), 41 | ], 42 | preview: { 43 | select: { 44 | title: "name.en", 45 | order: "order", 46 | date: "date", 47 | }, 48 | prepare({ title, order, date }) { 49 | const subtitles = [ 50 | order && `order:${order}`, 51 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 52 | ].filter(Boolean); 53 | return { title, subtitle: subtitles.join(" ") }; 54 | } 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /sanity/schemas/documents/guide.ts: -------------------------------------------------------------------------------- 1 | import { DocumentIcon, TagsIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "guide", 7 | title: "Guide", 8 | icon: DocumentIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | validation: (rule) => rule.required(), 16 | }), 17 | defineField({ 18 | name: "slug", 19 | title: "Slug", 20 | type: "slug", 21 | options: { 22 | source: "name", 23 | maxLength: 96, 24 | isUnique: (value, context) => context.defaultIsUnique(value, context), 25 | }, 26 | validation: (rule) => rule.required(), 27 | }), 28 | defineField({ 29 | name: "excerpt", 30 | title: "Excerpt", 31 | type: "string", 32 | }), 33 | defineField({ 34 | name: "link", 35 | title: "Link", 36 | type: "string", 37 | }), 38 | defineField({ 39 | name: "order", 40 | title: "Order", 41 | type: "number", 42 | initialValue: -1, 43 | }), 44 | defineField({ 45 | name: "date", 46 | title: "Date", 47 | type: "datetime", 48 | initialValue: () => new Date().toISOString(), 49 | }), 50 | ], 51 | preview: { 52 | select: { 53 | title: "name", 54 | order: "order", 55 | date: "date", 56 | }, 57 | prepare({ title, order, date }) { 58 | const subtitles = [ 59 | order && `order:${order}`, 60 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 61 | ].filter(Boolean); 62 | return { title, subtitle: subtitles.join(" ") }; 63 | } 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /sanity/schemas/documents/category.ts: -------------------------------------------------------------------------------- 1 | import { TiersIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "category", 7 | title: "Category", 8 | icon: TiersIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | // type: "string", 15 | type: "localizedString", 16 | validation: (rule) => rule.required(), 17 | }), 18 | defineField({ 19 | name: "slug", 20 | title: "Slug", 21 | type: "slug", 22 | options: { 23 | source: "name.en", 24 | maxLength: 96, 25 | isUnique: (value, context) => context.defaultIsUnique(value, context), 26 | }, 27 | validation: (rule) => rule.required(), 28 | }), 29 | defineField({ 30 | name: "group", 31 | title: "Group", 32 | type: "reference", 33 | to: [{ type: "group" }], 34 | }), 35 | defineField({ 36 | name: "order", 37 | title: "Order", 38 | type: "number", 39 | initialValue: -1, 40 | }), 41 | defineField({ 42 | name: "date", 43 | title: "Date", 44 | type: "datetime", 45 | initialValue: () => new Date().toISOString(), 46 | }), 47 | ], 48 | preview: { 49 | select: { 50 | title: "name.en", 51 | group: "group.name.en", 52 | order: "order", 53 | date: "date", 54 | }, 55 | prepare({ title, group, order, date }) { 56 | const subtitles = [ 57 | group && `group:${group}`, 58 | order && `order:${order}`, 59 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 60 | ].filter(Boolean); 61 | return { title, subtitle: subtitles.join(" ") }; 62 | } 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /app/(blog)/more-stories.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import Avatar from "./avatar"; 4 | import CoverImage from "./cover-image"; 5 | import DateComponent from "./date"; 6 | 7 | import type { MoreStoriesQueryResult } from "@/sanity.types"; 8 | import { sanityFetch } from "@/sanity/lib/fetch"; 9 | import { moreStoriesQuery } from "@/sanity/lib/queries"; 10 | 11 | export default async function MoreStories(params: { 12 | skip: string; 13 | limit: number; 14 | }) { 15 | const data = await sanityFetch({ 16 | query: moreStoriesQuery, 17 | params, 18 | }); 19 | 20 | return ( 21 | <> 22 |
23 | {data?.map((post) => { 24 | const { _id, title, slug, coverImage, excerpt, author } = post; 25 | return ( 26 |
27 | 28 | 29 | 30 |

31 | 32 | {title} 33 | 34 |

35 |
36 | 37 |
38 | {excerpt && ( 39 |

40 | {excerpt} 41 |

42 | )} 43 | {author && } 44 |
45 | ); 46 | })} 47 |
48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /sanity/schemas/documents/submission.ts: -------------------------------------------------------------------------------- 1 | import { DiamondIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "submission", 7 | title: "Submission", 8 | icon: DiamondIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | }), 16 | defineField({ 17 | name: "link", 18 | title: "Link", 19 | type: "string", 20 | }), 21 | defineField({ 22 | name: "status", 23 | title: "Status", 24 | type: "string", 25 | initialValue: 'reviewing', 26 | options: { 27 | list: [ 'reviewing', 'rejected', 'approved' ], 28 | layout: 'radio' // <-- defaults to 'dropdown' 29 | } 30 | }), 31 | defineField({ 32 | name: "reason", 33 | title: "Reason", 34 | type: "string", 35 | hidden: ({ parent }) => parent?.status !== 'rejected', 36 | options: { 37 | list: [ 38 | 'rejected: this product is not for indie hackers', 39 | ], 40 | } 41 | }), 42 | defineField({ 43 | name: "user", 44 | title: "User", 45 | type: "reference", 46 | to: [{ type: "user" }], 47 | }), 48 | defineField({ 49 | name: "date", 50 | title: "Date", 51 | type: "datetime", 52 | initialValue: () => new Date().toISOString(), 53 | }), 54 | ], 55 | preview: { 56 | select: { 57 | title: "name", 58 | author: "user.name", 59 | status: "status", 60 | date: "date", 61 | }, 62 | prepare({ title, author, status, date }) { 63 | const subtitles = [ 64 | author && `${author}`, 65 | status && `${status}`, 66 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 67 | ].filter(Boolean); 68 | return { title, subtitle: subtitles.join(" ") }; 69 | }, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "predev": "npm run typegen", 5 | "dev": "next dev -p 3333", 6 | "prebuild": "npm run typegen", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "presetup": "echo 'about to setup env variables, follow the guide here: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#using-the-sanity-cli'", 11 | "setup": "npx sanity@latest init --env .env.local", 12 | "postsetup": "echo 'create the read token by following the rest of the guide: https://github.com/vercel/next.js/tree/canary/examples/cms-sanity#creating-a-read-token'", 13 | "typegen": "sanity schema extract && sanity typegen generate && node sanity-fix.js", 14 | "sanity-fix": "node sanity-fix.js", 15 | "sanity-patch": "node sanity-patch.js" 16 | }, 17 | "dependencies": { 18 | "@sanity/assist": "3.0.3", 19 | "@sanity/code-input": "^4.1.4", 20 | "@sanity/color-input": "^3.1.1", 21 | "@sanity/dashboard": "^3.1.6", 22 | "@sanity/icons": "2.11.8", 23 | "@sanity/image-url": "1.0.2", 24 | "@sanity/preview-url-secret": "1.6.11", 25 | "@sanity/vision": "3.39.0", 26 | "@tailwindcss/typography": "^0.5.13", 27 | "@types/node": "^20.12.7", 28 | "@types/react": "^18.3.1", 29 | "@types/react-dom": "^18.3.0", 30 | "@vercel/speed-insights": "^1.0.10", 31 | "autoprefixer": "^10.4.19", 32 | "date-fns": "^3.6.0", 33 | "easymde": "^2.18.0", 34 | "next": "latest", 35 | "next-sanity": "9.0.10", 36 | "postcss": "^8.4.38", 37 | "react": "^18.3.1", 38 | "react-dom": "^18.3.1", 39 | "rxjs": "^7.8.1", 40 | "sanity": "3.39.0", 41 | "sanity-plugin-asset-source-unsplash": "3.0.1", 42 | "sanity-plugin-iframe-pane": "^3.1.6", 43 | "sanity-plugin-markdown": "^4.1.2", 44 | "sanity-plugin-media": "^2.2.5", 45 | "server-only": "^0.0.1", 46 | "styled-components": "6.1.8", 47 | "tailwindcss": "^3.4.3", 48 | "typescript": "5.4.5" 49 | }, 50 | "devDependencies": { 51 | "@next/env": "latest", 52 | "eslint": "^8.57.0", 53 | "eslint-config-next": "latest" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sanity/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { ClientPerspective, QueryParams } from "next-sanity"; 2 | import { draftMode } from "next/headers"; 3 | 4 | import { client } from "@/sanity/lib/client"; 5 | import { token } from "@/sanity/lib/token"; 6 | 7 | /** 8 | * Used to fetch data in Server Components, it has built in support for handling Draft Mode and perspectives. 9 | * When using the "published" perspective then time-based revalidation is used, set to match the time-to-live on Sanity's API CDN (60 seconds) 10 | * and will also fetch from the CDN. 11 | * When using the "previewDrafts" perspective then the data is fetched from the live API and isn't cached, it will also fetch draft content that isn't published yet. 12 | */ 13 | export async function sanityFetch({ 14 | query, 15 | params = {}, 16 | perspective = (process.env.NODE_ENV === "development" || draftMode().isEnabled) ? "previewDrafts" : "published", 17 | /** 18 | * Stega embedded Content Source Maps are used by Visual Editing by both the Sanity Presentation Tool and Vercel Visual Editing. 19 | * The Sanity Presentation Tool will enable Draft Mode when loading up the live preview, and we use it as a signal for when to embed source maps. 20 | * When outside of the Sanity Studio we also support the Vercel Toolbar Visual Editing feature, which is only enabled in production when it's a Vercel Preview Deployment. 21 | */ 22 | stega = perspective === "previewDrafts" || 23 | process.env.VERCEL_ENV === "preview", 24 | }: { 25 | query: string; 26 | params?: QueryParams; 27 | perspective?: Omit; 28 | stega?: boolean; 29 | }) { 30 | if (perspective === "previewDrafts") { 31 | return client.fetch(query, params, { 32 | stega: false, 33 | perspective: "previewDrafts", 34 | // The token is required to fetch draft content 35 | token, 36 | // The `previewDrafts` perspective isn't available on the API CDN 37 | useCdn: false, 38 | // And we can't cache the responses as it would slow down the live preview experience 39 | next: { revalidate: 0 }, 40 | }); 41 | } 42 | return client.fetch(query, params, { 43 | stega: false, 44 | perspective: "published", 45 | // The `published` perspective is available on the API CDN 46 | useCdn: true, 47 | // Only enable Stega in production if it's a Vercel Preview Deployment, as the Vercel Toolbar supports Visual Editing 48 | // When using the `published` perspective we use time-based revalidation to match the time-to-live on Sanity's API CDN (60 seconds) 49 | next: { revalidate: 60 }, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /sanity/schemas/documents/application.ts: -------------------------------------------------------------------------------- 1 | import { ColorWheelIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "application", 7 | title: "Application", 8 | icon: ColorWheelIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | }), 16 | defineField({ 17 | name: "description", 18 | title: "Description", 19 | type: "text", 20 | }), 21 | defineField({ 22 | name: "link", 23 | title: "Link", 24 | type: "string", 25 | }), 26 | defineField({ 27 | name: "types", 28 | title: "Types", 29 | type: "array", 30 | of: [ 31 | { 32 | type: "reference", 33 | to: [{ type: "appType" }], 34 | } 35 | ], 36 | }), 37 | defineField({ 38 | name: "featured", 39 | title: "Featured", 40 | type: "boolean", 41 | initialValue: false, 42 | }), 43 | defineField({ 44 | name: "status", 45 | title: "Status", 46 | type: "string", 47 | initialValue: 'reviewing', 48 | options: { 49 | list: ['reviewing', 'rejected', 'approved'], 50 | layout: 'radio' // <-- defaults to 'dropdown' 51 | } 52 | }), 53 | defineField({ 54 | name: "reason", 55 | title: "Reason", 56 | type: "string", 57 | hidden: ({ parent }) => parent?.status !== 'rejected', 58 | options: { 59 | list: [ 60 | 'rejected: please upload a better logo image', 61 | 'rejected: please upload a better cover image', 62 | 'rejected: this indie app seems not ready?', 63 | 'rejected: only support self-built indie app', 64 | ], 65 | } 66 | }), 67 | defineField({ 68 | name: "image", 69 | title: "Image", 70 | type: "image", 71 | }), 72 | defineField({ 73 | name: "cover", 74 | title: "Cover Image", 75 | type: "image", 76 | }), 77 | defineField({ 78 | name: "user", 79 | title: "User", 80 | type: "reference", 81 | to: [{ type: "user" }], 82 | }), 83 | defineField({ 84 | name: "date", 85 | title: "Date", 86 | type: "datetime", 87 | initialValue: () => new Date().toISOString(), 88 | }), 89 | ], 90 | preview: { 91 | select: { 92 | title: "name", 93 | author: "user.name", 94 | date: "date", 95 | media: "image", 96 | }, 97 | prepare({ title, author, date, media }) { 98 | const subtitles = [ 99 | author && `${author}`, 100 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 101 | ].filter(Boolean); 102 | return { title, media, subtitle: subtitles.join(" ") }; 103 | }, 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /sanity/plugins/locate.ts: -------------------------------------------------------------------------------- 1 | import { map, Observable } from "rxjs"; 2 | import type { 3 | DocumentLocation, 4 | DocumentLocationResolver, 5 | DocumentLocationsState, 6 | } from "sanity/presentation"; 7 | 8 | import { resolveHref } from "@/sanity/lib/utils"; 9 | 10 | const homeLocation = { 11 | title: "Home", 12 | href: "/", 13 | } satisfies DocumentLocation; 14 | 15 | export const locate: DocumentLocationResolver = (params, context) => { 16 | if (params.type === "settings") { 17 | const doc$ = context.documentStore.listenQuery( 18 | `*[_type == "post" && defined(slug.current)]{title,slug}`, 19 | {}, 20 | { perspective: "previewDrafts" }, 21 | ) as Observable< 22 | | { 23 | slug: { current: string }; 24 | title: string | null; 25 | }[] 26 | | null 27 | >; 28 | return doc$.pipe( 29 | map((docs) => { 30 | return { 31 | message: "This document is used on all pages", 32 | tone: "caution", 33 | locations: docs?.length 34 | ? [ 35 | homeLocation, 36 | ...docs 37 | .map((doc) => ({ 38 | title: doc?.title || "Untitled", 39 | href: resolveHref("post", doc?.slug?.current)!, 40 | })) 41 | .filter((doc) => doc.href !== undefined), 42 | ] 43 | : [], 44 | } satisfies DocumentLocationsState; 45 | }), 46 | ); 47 | } 48 | 49 | if (params.type === "post" || params.type === "author") { 50 | const doc$ = context.documentStore.listenQuery( 51 | `*[defined(slug.current) && _id==$id || references($id)]{_type,slug,title}`, 52 | params, 53 | { perspective: "previewDrafts" }, 54 | ) as Observable< 55 | | { 56 | _type: string; 57 | slug: { current: string }; 58 | title?: string | null; 59 | }[] 60 | | null 61 | >; 62 | return doc$.pipe( 63 | map((docs) => { 64 | switch (params.type) { 65 | case "author": 66 | case "post": 67 | return { 68 | locations: docs?.length 69 | ? [ 70 | homeLocation, 71 | ...docs 72 | .map((doc) => { 73 | const href = resolveHref(doc._type, doc?.slug?.current); 74 | return { 75 | title: doc?.title || "Untitled", 76 | href: href!, 77 | }; 78 | }) 79 | .filter((doc) => doc.href !== undefined), 80 | ] 81 | : [], 82 | } satisfies DocumentLocationsState; 83 | default: 84 | return { 85 | message: "Unable to map document type to locations", 86 | tone: "critical", 87 | } satisfies DocumentLocationsState; 88 | } 89 | }), 90 | ); 91 | } 92 | 93 | return null; 94 | }; 95 | -------------------------------------------------------------------------------- /app/(blog)/onboarding.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | /** 4 | * This file is used for onboarding when you don't have any posts yet and are using the template for the first time. 5 | * Once you have content, and know where to go to access the Sanity Studio and create content, you can delete this file. 6 | */ 7 | 8 | import Link from "next/link"; 9 | import { useSyncExternalStore } from "react"; 10 | 11 | const emptySubscribe = () => () => {}; 12 | 13 | export default function Onboarding() { 14 | const target = useSyncExternalStore( 15 | emptySubscribe, 16 | () => (window.top === window ? undefined : "_blank"), 17 | () => "_blank", 18 | ); 19 | 20 | return ( 21 |
22 | 47 |
48 |

No posts

49 |

50 | Get started by creating a new post. 51 |

52 |
53 | 54 |
55 | 60 | 68 | Create Post 69 | 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /sanity/schemas/documents/post.ts: -------------------------------------------------------------------------------- 1 | import { DocumentsIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | import authorType from "./author"; 6 | 7 | // for demo 8 | export default defineType({ 9 | name: "post", 10 | title: "Post", 11 | icon: DocumentsIcon, 12 | type: "document", 13 | fields: [ 14 | defineField({ 15 | name: "title", 16 | title: "Title", 17 | type: "string", 18 | validation: (rule) => rule.required(), 19 | }), 20 | defineField({ 21 | name: "slug", 22 | title: "Slug", 23 | type: "slug", 24 | description: "A slug is required for the post to show up in the preview", 25 | options: { 26 | source: "title", 27 | maxLength: 96, 28 | isUnique: (value, context) => context.defaultIsUnique(value, context), 29 | }, 30 | validation: (rule) => rule.required(), 31 | }), 32 | defineField({ 33 | name: "content", 34 | title: "Content", 35 | type: "array", 36 | of: [ 37 | { 38 | type: "block" 39 | }, 40 | { 41 | type: 'image' 42 | }, 43 | { 44 | type: 'code' 45 | } 46 | ], 47 | }), 48 | defineField({ 49 | name: "excerpt", 50 | title: "Excerpt", 51 | type: "text", 52 | }), 53 | // defineField({ 54 | // name: "summary", 55 | // title: "Summary", 56 | // type: "text", 57 | // }), 58 | defineField({ 59 | name: "coverImage", 60 | title: "Cover Image", 61 | type: "image", 62 | options: { 63 | hotspot: true, 64 | aiAssist: { 65 | imageDescriptionField: "alt", 66 | }, 67 | }, 68 | fields: [ 69 | { 70 | name: "alt", 71 | type: "string", 72 | title: "Alternative text", 73 | description: "Important for SEO and accessiblity.", 74 | validation: (rule) => { 75 | return rule.custom((alt, context) => { 76 | if ((context.document?.coverImage as any)?.asset?._ref && !alt) { 77 | return "Required"; 78 | } 79 | return true; 80 | }); 81 | }, 82 | }, 83 | ], 84 | validation: (rule) => rule.required(), 85 | }), 86 | defineField({ 87 | name: "date", 88 | title: "Date", 89 | type: "datetime", 90 | initialValue: () => new Date().toISOString(), 91 | }), 92 | defineField({ 93 | name: "author", 94 | title: "Author", 95 | type: "reference", 96 | to: [{ type: authorType.name }], 97 | }), 98 | ], 99 | preview: { 100 | select: { 101 | title: "title", 102 | author: "author.name", 103 | date: "date", 104 | media: "coverImage", 105 | }, 106 | prepare({ title, media, author, date }) { 107 | const subtitles = [ 108 | author && `by ${author}`, 109 | date && `on ${format(parseISO(date), "LLL d, yyyy")}`, 110 | ].filter(Boolean); 111 | 112 | return { title, media, subtitle: subtitles.join(" ") }; 113 | }, 114 | }, 115 | }); 116 | -------------------------------------------------------------------------------- /sanity.config.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | /** 3 | * This config is used to set up Sanity Studio that's mounted on the `app/(sanity)/studio/[[...tool]]/page.tsx` route 4 | */ 5 | import { codeInput } from '@sanity/code-input'; 6 | import { colorInput } from '@sanity/color-input'; 7 | import { dashboardTool, projectInfoWidget, projectUsersWidget } from "@sanity/dashboard"; 8 | import { visionTool } from "@sanity/vision"; 9 | import { PluginOptions, defineConfig } from "sanity"; 10 | import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash"; 11 | import { markdownSchema } from "sanity-plugin-markdown"; 12 | import { media } from 'sanity-plugin-media'; 13 | import { presentationTool } from "sanity/presentation"; 14 | import { structureTool } from "sanity/structure"; 15 | 16 | import defaultDocumentNode from '@/sanity/defaultDocumentNode'; 17 | import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; 18 | import { locate } from "@/sanity/plugins/locate"; 19 | import { pageStructure, singletonPlugin } from "@/sanity/plugins/settings"; 20 | import author from "@/sanity/schemas/documents/author"; 21 | import post from "@/sanity/schemas/documents/post"; 22 | import product from "@/sanity/schemas/documents/product"; 23 | import settings from "@/sanity/schemas/singletons/settings"; 24 | import application from "./sanity/schemas/documents/application"; 25 | import category from "./sanity/schemas/documents/category"; 26 | import { comment } from "./sanity/schemas/documents/comment"; 27 | import submission from "./sanity/schemas/documents/submission"; 28 | import tag from "./sanity/schemas/documents/tag"; 29 | import appType from './sanity/schemas/documents/appType'; 30 | import user from './sanity/schemas/documents/user'; 31 | import group from './sanity/schemas/documents/group'; 32 | import guide from './sanity/schemas/documents/guide'; 33 | import localizedString from './sanity/schemas/singletons/localizedString'; 34 | 35 | export default defineConfig({ 36 | basePath: studioUrl, 37 | projectId, 38 | dataset, 39 | schema: { 40 | types: [ 41 | // Singletons 42 | settings, 43 | localizedString, 44 | 45 | // Documents 46 | product, 47 | submission, 48 | application, 49 | user, 50 | 51 | guide, 52 | 53 | tag, 54 | category, 55 | group, 56 | appType, 57 | 58 | post, 59 | author, 60 | comment, 61 | ], 62 | }, 63 | plugins: [ 64 | structureTool({ 65 | defaultDocumentNode, 66 | structure: pageStructure([settings]), 67 | }), 68 | 69 | dashboardTool({ 70 | widgets: [ 71 | projectInfoWidget(), 72 | projectUsersWidget(), 73 | // sanityTutorialsWidget() 74 | ], 75 | }), 76 | 77 | presentationTool({ 78 | locate, 79 | previewUrl: { previewMode: { enable: "/api/draft" } }, 80 | }), 81 | // https://www.sanity.io/plugins/sanity-plugin-media 82 | media(), 83 | // https://www.sanity.io/plugins/sanity-plugin-markdown 84 | markdownSchema(), 85 | // Configures the global "new document" button, and document actions, to suit the Settings document singleton 86 | singletonPlugin([settings.name]), 87 | // Add an image asset source for Unsplash 88 | unsplashImageAsset(), 89 | // Sets up AI Assist with preset prompts 90 | // https://www.sanity.io/docs/ai-assist 91 | // assistWithPresets(), 92 | colorInput(), 93 | codeInput(), 94 | // Vision lets you query your content with GROQ in the studio 95 | // https://www.sanity.io/docs/the-vision-plugin 96 | // process.env.NODE_ENV === "development" && 97 | visionTool({ 98 | defaultApiVersion: apiVersion 99 | }), 100 | ].filter(Boolean) as PluginOptions[], 101 | }); 102 | -------------------------------------------------------------------------------- /app/(blog)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../globals.css"; 2 | 3 | import { SpeedInsights } from "@vercel/speed-insights/next"; 4 | import type { Metadata } from "next"; 5 | import { 6 | VisualEditing, 7 | toPlainText, 8 | type PortableTextBlock, 9 | } from "next-sanity"; 10 | import { Inter } from "next/font/google"; 11 | import { draftMode } from "next/headers"; 12 | import { Suspense } from "react"; 13 | 14 | import AlertBanner from "./alert-banner"; 15 | import PortableText from "./portable-text"; 16 | 17 | import type { SettingsQueryResult } from "@/sanity.types"; 18 | import * as demo from "@/sanity/lib/demo"; 19 | import { sanityFetch } from "@/sanity/lib/fetch"; 20 | import { settingsQuery } from "@/sanity/lib/queries"; 21 | import { resolveOpenGraphImage } from "@/sanity/lib/utils"; 22 | 23 | export async function generateMetadata(): Promise { 24 | const settings = await sanityFetch({ 25 | query: settingsQuery, 26 | // Metadata should never contain stega 27 | stega: false, 28 | }); 29 | const title = settings?.title || demo.title; 30 | const description = settings?.description || demo.description; 31 | 32 | const ogImage = resolveOpenGraphImage(settings?.ogImage); 33 | let metadataBase: URL | undefined = undefined; 34 | try { 35 | metadataBase = settings?.ogImage?.metadataBase 36 | ? new URL(settings.ogImage.metadataBase) 37 | : undefined; 38 | } catch { 39 | // ignore 40 | } 41 | return { 42 | metadataBase, 43 | title: { 44 | template: `%s | ${title}`, 45 | default: title, 46 | }, 47 | description: toPlainText(description), 48 | openGraph: { 49 | images: ogImage ? [ogImage] : [], 50 | }, 51 | }; 52 | } 53 | 54 | const inter = Inter({ 55 | variable: "--font-inter", 56 | subsets: ["latin"], 57 | display: "swap", 58 | }); 59 | 60 | async function Footer() { 61 | const data = await sanityFetch({ 62 | query: settingsQuery, 63 | }); 64 | const footer = data?.footer || []; 65 | 66 | return ( 67 | 97 | ); 98 | } 99 | 100 | export default function RootLayout({ 101 | children, 102 | }: { 103 | children: React.ReactNode; 104 | }) { 105 | return ( 106 | 107 | 108 |
109 | {draftMode().isEnabled && } 110 |
{children}
111 | 112 |
113 | 114 |
115 | {draftMode().isEnabled && } 116 | 117 | 118 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /sanity/schemas/singletons/settings.tsx: -------------------------------------------------------------------------------- 1 | import { CogIcon } from "@sanity/icons"; 2 | import { defineArrayMember, defineField, defineType } from "sanity"; 3 | 4 | import * as demo from "@/sanity/lib/demo"; 5 | 6 | export default defineType({ 7 | name: "settings", 8 | title: "Settings", 9 | type: "document", 10 | icon: CogIcon, 11 | fields: [ 12 | defineField({ 13 | name: "title", 14 | description: "This field is the title of your blog.", 15 | title: "Title", 16 | type: "string", 17 | initialValue: demo.title, 18 | validation: (rule) => rule.required(), 19 | }), 20 | defineField({ 21 | name: "subtitle", 22 | description: "This field is the subtitle of your blog.", 23 | title: "SubTitle", 24 | type: "markdown", 25 | // initialValue: demo.title, 26 | // validation: (rule) => rule.required(), 27 | }), 28 | defineField({ 29 | name: "description", 30 | description: 31 | "Used both for the description tag for SEO, and the blog subheader.", 32 | title: "Description", 33 | type: "array", 34 | initialValue: demo.description, 35 | of: [ 36 | defineArrayMember({ 37 | type: "block", 38 | options: {}, 39 | styles: [], 40 | lists: [], 41 | marks: { 42 | decorators: [], 43 | annotations: [ 44 | defineField({ 45 | type: "object", 46 | name: "link", 47 | fields: [ 48 | { 49 | type: "string", 50 | name: "href", 51 | title: "URL", 52 | validation: (rule) => rule.required(), 53 | }, 54 | ], 55 | }), 56 | ], 57 | }, 58 | }), 59 | ], 60 | }), 61 | defineField({ 62 | name: "footer", 63 | description: 64 | "This is a block of text that will be displayed at the bottom of the page.", 65 | title: "Footer Info", 66 | type: "array", 67 | of: [ 68 | defineArrayMember({ 69 | type: "block", 70 | marks: { 71 | annotations: [ 72 | { 73 | name: "link", 74 | type: "object", 75 | title: "Link", 76 | fields: [ 77 | { 78 | name: "href", 79 | type: "url", 80 | title: "Url", 81 | }, 82 | ], 83 | }, 84 | ], 85 | }, 86 | }), 87 | ], 88 | }), 89 | defineField({ 90 | name: "ogImage", 91 | title: "Open Graph Image", 92 | type: "image", 93 | description: "Displayed on social cards and search engine results.", 94 | options: { 95 | hotspot: true, 96 | aiAssist: { 97 | imageDescriptionField: "alt", 98 | }, 99 | }, 100 | fields: [ 101 | defineField({ 102 | name: "alt", 103 | description: "Important for accessibility and SEO.", 104 | title: "Alternative text", 105 | type: "string", 106 | validation: (rule) => { 107 | return rule.custom((alt, context) => { 108 | if ((context.document?.ogImage as any)?.asset?._ref && !alt) { 109 | return "Required"; 110 | } 111 | return true; 112 | }); 113 | }, 114 | }), 115 | defineField({ 116 | name: "metadataBase", 117 | type: "url", 118 | description: ( 119 | 123 | More information 124 | 125 | ), 126 | }), 127 | ], 128 | }), 129 | ], 130 | preview: { 131 | prepare() { 132 | return { 133 | title: "Settings", 134 | }; 135 | }, 136 | }, 137 | }); 138 | -------------------------------------------------------------------------------- /app/(blog)/posts/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, ResolvingMetadata } from "next"; 2 | import { groq, type PortableTextBlock } from "next-sanity"; 3 | import Link from "next/link"; 4 | import { notFound } from "next/navigation"; 5 | import { Suspense } from "react"; 6 | 7 | import Avatar from "../../avatar"; 8 | import CoverImage from "../../cover-image"; 9 | import DateComponent from "../../date"; 10 | import MoreStories from "../../more-stories"; 11 | import PortableText from "../../portable-text"; 12 | 13 | import type { 14 | PostQueryResult, 15 | PostSlugsResult, 16 | SettingsQueryResult, 17 | } from "@/sanity.types"; 18 | import * as demo from "@/sanity/lib/demo"; 19 | import { sanityFetch } from "@/sanity/lib/fetch"; 20 | import { postQuery, settingsQuery } from "@/sanity/lib/queries"; 21 | import { resolveOpenGraphImage } from "@/sanity/lib/utils"; 22 | 23 | type Props = { 24 | params: { slug: string }; 25 | }; 26 | 27 | const postSlugs = groq`*[_type == "post"]{slug}`; 28 | 29 | export async function generateStaticParams() { 30 | const params = await sanityFetch({ 31 | query: postSlugs, 32 | perspective: "published", 33 | stega: false, 34 | }); 35 | return params.map(({ slug }) => ({ slug: slug?.current })); 36 | } 37 | 38 | export async function generateMetadata( 39 | { params }: Props, 40 | parent: ResolvingMetadata, 41 | ): Promise { 42 | const post = await sanityFetch({ 43 | query: postQuery, 44 | params, 45 | stega: false, 46 | }); 47 | const previousImages = (await parent).openGraph?.images || []; 48 | const ogImage = resolveOpenGraphImage(post?.coverImage); 49 | 50 | return { 51 | authors: post?.author?.name ? [{ name: post?.author?.name }] : [], 52 | title: post?.title, 53 | description: post?.excerpt, 54 | openGraph: { 55 | images: ogImage ? [ogImage, ...previousImages] : previousImages, 56 | }, 57 | } satisfies Metadata; 58 | } 59 | 60 | export default async function PostPage({ params }: Props) { 61 | const [post, settings] = await Promise.all([ 62 | sanityFetch({ 63 | query: postQuery, 64 | params, 65 | }), 66 | sanityFetch({ 67 | query: settingsQuery, 68 | }), 69 | ]); 70 | 71 | if (!post?._id) { 72 | return notFound(); 73 | } 74 | 75 | return ( 76 |
77 |

78 | 79 | {settings?.title || demo.title} 80 | 81 |

82 |
83 |

84 | {post.title} 85 |

86 |
87 | {post.author && ( 88 | 89 | )} 90 |
91 |
92 | 93 |
94 |
95 |
96 | {post.author && ( 97 | 98 | )} 99 |
100 |
101 |
102 | 103 |
104 |
105 |
106 | {post.content?.length && ( 107 | 111 | )} 112 |
113 | 122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /app/(blog)/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Suspense } from "react"; 3 | 4 | import Avatar from "./avatar"; 5 | import CoverImage from "./cover-image"; 6 | import DateComponent from "./date"; 7 | import MoreStories from "./more-stories"; 8 | import Onboarding from "./onboarding"; 9 | import PortableText from "./portable-text"; 10 | 11 | import type { HeroQueryResult, SettingsQueryResult } from "@/sanity.types"; 12 | import * as demo from "@/sanity/lib/demo"; 13 | import { sanityFetch } from "@/sanity/lib/fetch"; 14 | import { heroQuery, settingsQuery } from "@/sanity/lib/queries"; 15 | 16 | function Intro(props: { title: string | null | undefined; description: any }) { 17 | const title = props.title || demo.title; 18 | const description = props.description?.length 19 | ? props.description 20 | : demo.description; 21 | return ( 22 |
23 |

24 | {title || demo.title} 25 |

26 |

27 | 31 |

32 |
33 | ); 34 | } 35 | 36 | function HeroPost({ 37 | title, 38 | slug, 39 | excerpt, 40 | coverImage, 41 | date, 42 | author, 43 | }: Pick< 44 | Exclude, 45 | "title" | "coverImage" | "date" | "excerpt" | "author" | "slug" 46 | >) { 47 | return ( 48 |
49 | 50 | 51 | 52 |
53 |
54 |

55 | 56 | {title} 57 | 58 |

59 |
60 | 61 |
62 |
63 |
64 | {excerpt && ( 65 |

66 | {excerpt} 67 |

68 | )} 69 | {author && } 70 |
71 |
72 |
73 | ); 74 | } 75 | 76 | export default async function Page() { 77 | const [settings, heroPost] = await Promise.all([ 78 | sanityFetch({ 79 | query: settingsQuery, 80 | }), 81 | sanityFetch({ query: heroQuery }), 82 | ]); 83 | 84 | console.log('heroPost', heroPost); 85 | 86 | console.log('heroPost, slug', heroPost?.slug); 87 | console.log('heroPost, slug length', heroPost?.slug?.length); 88 | 89 | console.log('heroPost, title', heroPost?.title); 90 | console.log('heroPost, title length', heroPost?.title.length); 91 | 92 | const filteredTitle = heroPost?.title.replace(/[^\x20-\x7E]/g, ''); // 只保留可见ASCII字符 93 | console.log('filteredTitle', filteredTitle); 94 | console.log("filteredTitle length:", filteredTitle?.length); 95 | 96 | return ( 97 |
98 | 99 | {heroPost ? ( 100 | 108 | ) : ( 109 | 110 | )} 111 | {heroPost?._id && ( 112 | 120 | )} 121 |
122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /sanity/schemas/documents/product.ts: -------------------------------------------------------------------------------- 1 | import { ProjectsIcon } from "@sanity/icons"; 2 | import { format, parseISO } from "date-fns"; 3 | import { defineField, defineType } from "sanity"; 4 | 5 | export default defineType({ 6 | name: "product", 7 | title: "Product", 8 | icon: ProjectsIcon, 9 | type: "document", 10 | fields: [ 11 | defineField({ 12 | name: "name", 13 | title: "Name", 14 | type: "string", 15 | validation: (rule) => rule.required(), 16 | }), 17 | defineField({ 18 | name: "slug", 19 | title: "Slug", 20 | type: "slug", 21 | options: { 22 | source: "name", 23 | maxLength: 96, 24 | isUnique: (value, context) => context.defaultIsUnique(value, context), 25 | }, 26 | validation: (rule) => rule.required(), 27 | }), 28 | defineField({ 29 | name: "order", 30 | title: "Order", 31 | type: "number", 32 | initialValue: -1, 33 | }), 34 | defineField({ 35 | name: "category", 36 | title: "Category", 37 | type: "reference", 38 | to: [{ type: "category" }], 39 | }), 40 | defineField({ 41 | name: "tags", 42 | title: "Tags", 43 | type: "array", 44 | of: [ 45 | { 46 | type: "reference", 47 | to: [{ type: "tag" }], 48 | } 49 | ], 50 | }), 51 | defineField({ 52 | name: "featured", 53 | title: "Featured", 54 | type: "boolean", 55 | initialValue: false, 56 | }), 57 | defineField({ 58 | name: "visible", 59 | title: "Visible", 60 | type: "boolean", 61 | initialValue: true, 62 | }), 63 | defineField({ 64 | name: "website", 65 | title: "Website", 66 | type: "string", 67 | }), 68 | defineField({ 69 | name: "github", 70 | title: "Github", 71 | type: "string", 72 | }), 73 | defineField({ 74 | name: "priceLink", 75 | title: "PriceLink", 76 | type: "string", 77 | }), 78 | defineField({ 79 | name: "price", 80 | title: "Price", 81 | type: "string", 82 | options: { 83 | list: [ 84 | 'Free', 85 | 'Paid', 86 | 'Free & Paid', 87 | ], 88 | } 89 | }), 90 | defineField({ 91 | name: "source", 92 | title: "source", 93 | type: "string", 94 | }), 95 | defineField({ 96 | name: "submitter", 97 | title: "Submitter", 98 | type: "reference", 99 | to: [{ type: "user" }], 100 | }), 101 | defineField({ 102 | name: "desc", 103 | title: "Description", 104 | type: "localizedString", 105 | }), 106 | defineField({ 107 | name: "content", 108 | title: "Content", 109 | type: "array", 110 | of: [ 111 | { 112 | type: "block" 113 | }, 114 | { 115 | type: 'image' 116 | }, 117 | { 118 | type: 'code' 119 | } 120 | ], 121 | }), 122 | // defineField({ 123 | // name: "content_zh", 124 | // title: "Content_ZH", 125 | // type: "array", 126 | // of: [ 127 | // { 128 | // type: "block" 129 | // }, 130 | // { 131 | // type: 'image' 132 | // }, 133 | // { 134 | // type: 'code' 135 | // } 136 | // ], 137 | // }), 138 | defineField({ 139 | name: "logo", 140 | title: "Logo", 141 | type: "image", 142 | }), 143 | defineField({ 144 | name: "coverImage", 145 | title: "Cover Image", 146 | type: "image", 147 | options: { 148 | hotspot: true, 149 | }, 150 | }), 151 | defineField({ 152 | name: "guides", 153 | title: "Guides", 154 | type: "array", 155 | of: [ 156 | { 157 | type: "reference", 158 | to: [{ type: "guide" }], 159 | } 160 | ], 161 | }), 162 | defineField({ 163 | name: "date", 164 | title: "Date", 165 | type: "datetime", 166 | initialValue: () => new Date().toISOString(), 167 | }), 168 | ], 169 | preview: { 170 | select: { 171 | title: "name", 172 | media: "logo", 173 | order: "order", 174 | date: "date", 175 | }, 176 | prepare({ title, media, order, date }) { 177 | const subtitles = [ 178 | order && `order:${order}`, 179 | date && `${format(parseISO(date), "yyyy/MM/dd")}`, 180 | ].filter(Boolean); 181 | return { title, media, subtitle: subtitles.join(" ") }; 182 | }, 183 | }, 184 | }); 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Free Directory Boilerplate 3 |

Open Source Directory Boilerplate

4 |
5 | 6 |

7 | 8 | Javayhu Twitter follower count 9 | 10 |

11 | 12 |

13 | Introduction · 14 | Features · 15 | Tech Stack · 16 | How to use · 17 | Author · 18 | Compare with Mkdirs · 19 | Notice · 20 | License · 21 | Credits 22 |

23 |
24 | 25 | ## Introduction 26 | 27 | | Component | Website | Repository | 28 | | --------- | ------- | ---------- | 29 | | Frontend | [free-directory-boilerplate.vercel.app](https://free-directory-boilerplate.vercel.app) | [free-directory-boilerplate](https://github.com/javayhu/free-directory-boilerplate) | 30 | | **Backend (Sanity)** | [free-directory-sanity.vercel.app/studio](https://free-directory-sanity.vercel.app/studio) | [free-directory-sanity](https://github.com/javayhu/free-directory-sanity) | 31 | 32 | ## Features 33 | 34 | - Listings (Tools, Products) 35 | - Item Detail Page 36 | - Categories & Tags 37 | - Authentication (GitHub and Google) 38 | - Submission (built-in) 39 | - Sanity Studio (built-in CMS) 40 | - Blog (hidden by default) 41 | - Documentation (hidden by default) 42 | - Analytics (Umami & Google Analytics) 43 | - SEO (Sitemap, Open Graph) 44 | - Modern UI (Shadcn UI) 45 | - Responsive Design 46 | - Multi-language (English & Chinese) 47 | - Multi-theme (Light & Dark) 48 | 49 | 50 | ## Tech Stack 51 | 52 | - Next.js 14 53 | - NextAuth 54 | - Database (PostgreSQL) 55 | - Tailwind CSS 56 | - Shadcn UI 57 | - Lucide Icons 58 | - Contentlayer 59 | - Sanity 60 | - Vercel 61 | 62 | ## How to use 63 | 64 | 1. Clone the repository 65 | 2. Run `pnpm install` 66 | 3. Configure the `.env` file 67 | 4. Run `pnpm dev` 68 | 69 | ## Author 70 | 71 | This project is created by [Fox](https://x.com/indie_maker_fox), the founder of [Mkdirs](https://mkdirs.com) and [MkSaaS](https://mksaas.com). 72 | 73 | [Mkdirs](https://mkdirs.com) is the best directory boilerplate for anyone who wants to launch a profitable directory website in minutes. 74 | 75 | [MkSaaS](https://mksaas.com) is the best AI SaaS boilerplate, you can launch your next AI SaaS in a weekend with MkSaaS template. 76 | 77 | If you are interested in indie hacking, please follow me on X: [@javay_hu](https://x.com/indie_maker_fox) or BlueSky: [@javayhu.com](https://bsky.app/profile/mksaas.me) 78 | 79 | ### Compare with Mkdirs 80 | 81 | [Mkdirs](https://mkdirs.com) - The best directory boilerplate. 82 | 83 | | Feature | Free Directory Boilerplate | Mkdirs | 84 | | ------- | -------------------------- | ------ | 85 | | Repos | ✅ 2 | ✅ 1 | 86 | | Price | ✅ Free and Open Source | ✅ Paid | 87 | | Auth | ✅ GitHub or Google | ✅ GitHub or Google or Email | 88 | | Listings | ✅ Categories | ✅ Categories, Tags & Filters | 89 | | Database | ✅ Need PostgreSQL | ✅ NO NEED! JUST SANITY! | 90 | | Newsletter | ❌ Not supported | ✅ Supported | 91 | | Payment | ❌ Not supported | ✅ Supported | 92 | | Search | ❌ Not supported | ✅ Supported | 93 | | Pagination | ❌ Not supported | ✅ Supported | 94 | | Email Notification | ❌ Not supported | ✅ Supported | 95 | | Submission | ✅ Built-in (Free) | ✅ Built-in (Free & Paid) | 96 | | Blog | ✅ Contentlayer | ✅ Sanity CMS | 97 | | Analytics | ✅ Umami & Google Analytics | ✅ OpenPanel & Google Analytics | 98 | | SEO | ✅ Sitemap & Open Graph | ✅ Sitemap & Open Graph | 99 | | Multi-language | ✅ English & Chinese | ✅ English | 100 | | Multi-theme | ✅ Light & Dark | ✅ Light & Dark | 101 | 102 | ## Notice 103 | 104 | If you have any questions when using this project, please checkout the [docs of Mkidrs](https://docs.mkdirs.com) for more information, because they have almost the same tech stack. 105 | 106 | ## License 107 | 108 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 109 | 110 | ## Credits 111 | 112 | This project was inspired by [@miickasmt](https://twitter.com/miickasmt)'s [next-saas-stripe-starter](https://github.com/mickasmt/next-saas-stripe-starter) 113 | 114 | ## ⭐ Star History 115 | 116 | [![Star History Chart](https://api.star-history.com/svg?repos=javayhu/free-directory-sanity&type=Date)](https://star-history.com/#javayhu/free-directory-sanity&Date) 117 | -------------------------------------------------------------------------------- /sanity/plugins/settings.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This plugin contains all the logic for setting up the singletons 3 | */ 4 | 5 | import { CheckmarkCircleIcon, ClockIcon, CloseCircleIcon, ProjectsIcon } from "@sanity/icons"; 6 | import { definePlugin, type DocumentDefinition } from "sanity"; 7 | import { type StructureResolver } from "sanity/structure"; 8 | import application from "../schemas/documents/application"; 9 | import submission from "../schemas/documents/submission"; 10 | import product from "../schemas/documents/product"; 11 | 12 | export const singletonPlugin = definePlugin((types: string[]) => { 13 | return { 14 | name: "singletonPlugin", 15 | document: { 16 | // Hide 'Singletons (such as Settings)' from new document options 17 | // https://user-images.githubusercontent.com/81981/195728798-e0c6cf7e-d442-4e58-af3a-8cd99d7fcc28.png 18 | newDocumentOptions: (prev, { creationContext, ...rest }) => { 19 | if (creationContext.type === "global") { 20 | return prev.filter( 21 | (templateItem) => !types.includes(templateItem.templateId), 22 | ); 23 | } 24 | 25 | return prev; 26 | }, 27 | // Removes the "duplicate" action on the Singletons (such as Home) 28 | actions: (prev, { schemaType }) => { 29 | if (types.includes(schemaType)) { 30 | return prev.filter(({ action }) => action !== "duplicate"); 31 | } 32 | 33 | return prev; 34 | }, 35 | }, 36 | }; 37 | }); 38 | 39 | // The StructureResolver is how we're changing the DeskTool structure to linking to document (named Singleton) 40 | // like how "Home" is handled. 41 | export const pageStructure = ( 42 | typeDefArray: DocumentDefinition[], 43 | ): StructureResolver => { 44 | return (S) => { 45 | // Goes through all of the singletons that were provided and translates them into something the 46 | // Structure tool can understand 47 | const singletonItems = typeDefArray.map((typeDef) => { 48 | return S.listItem() 49 | .title(typeDef.title!) 50 | .icon(typeDef.icon) 51 | .child( 52 | S.editor() 53 | .id(typeDef.name) 54 | .schemaType(typeDef.name) 55 | .documentId(typeDef.name), 56 | ); 57 | }); 58 | 59 | // The default root list items (except custom ones) 60 | const defaultListItems = S.documentTypeListItems().filter( 61 | (listItem) => 62 | !typeDefArray.find((singleton) => singleton.name === listItem.getId()), 63 | ); 64 | 65 | // applications 66 | // sanity has a bug for setting schemaType has no effect, so I have to set _type! 67 | const reviewingAppliactionListItems = S.listItem() 68 | .title('Reviewing Appliactions') 69 | .schemaType(application.name) 70 | .icon(ClockIcon) 71 | .child(S.documentList() 72 | .schemaType(application.name) 73 | .title('Reviewing Appliactions') 74 | .filter('_type == "application" && status == "reviewing"')); 75 | 76 | const rejectedAppliactionListItems = S.listItem() 77 | .title('Rejected Appliactions') 78 | .schemaType(application.name) 79 | .icon(CloseCircleIcon) 80 | .child(S.documentList() 81 | .schemaType(application.name) 82 | .title('Rejected Appliactions') 83 | .filter('_type == "application" && status == "rejected"')); 84 | 85 | const approvedAppliactionListItems = S.listItem() 86 | .title('Approved Appliactions') 87 | .schemaType(application.name) 88 | .icon(CheckmarkCircleIcon) 89 | .child(S.documentList() 90 | .schemaType(application.name) 91 | .title('Approved Appliactions') 92 | .filter('_type == "application" && status == "approved"')); 93 | 94 | const featuredAppliactionListItems = S.listItem() 95 | .title('Featured Appliactions') 96 | .schemaType(application.name) 97 | .icon(CheckmarkCircleIcon) 98 | .child(S.documentList() 99 | .schemaType(application.name) 100 | .title('Featured Appliactions') 101 | .filter('_type == "application" && featured == true')); 102 | 103 | // submissions 104 | const reviewingSubmissionListItems = S.listItem() 105 | .title('Reviewing Submissions') 106 | .schemaType(submission.name) 107 | .icon(ClockIcon) 108 | .child(S.documentList() 109 | .schemaType(submission.name) 110 | .title('Reviewing Submissions') 111 | .filter('_type == "submission" && status == "reviewing"')); 112 | 113 | const rejectedSubmissionListItems = S.listItem() 114 | .title('Rejected Submissions') 115 | .schemaType(submission.name) 116 | .icon(CloseCircleIcon) 117 | .child(S.documentList() 118 | .schemaType(submission.name) 119 | .title('Rejected Submissions') 120 | .filter('_type == "submission" && status == "rejected"')); 121 | 122 | const approvedSubmissionListItems = S.listItem() 123 | .title('Approved Submissions') 124 | .schemaType(submission.name) 125 | .icon(CheckmarkCircleIcon) 126 | .child(S.documentList() 127 | .schemaType(submission.name) 128 | .title('Approved Submissions') 129 | .filter('_type == "submission" && status == "approved"')); 130 | 131 | const featuredProductListItems = S.listItem() 132 | .title('Featured Products') 133 | .schemaType(product.name) 134 | .icon(CheckmarkCircleIcon) 135 | .child(S.documentList() 136 | .schemaType(product.name) 137 | .title('Featured Products') 138 | .filter('_type == "product" && featured == true')); 139 | 140 | const invisibleProductListItems = S.listItem() 141 | .title('Invisible Products') 142 | .schemaType(product.name) 143 | .icon(CheckmarkCircleIcon) 144 | .child(S.documentList() 145 | .schemaType(product.name) 146 | .title('Invisible Products') 147 | .filter('_type == "product" && visible == false')); 148 | 149 | return S.list() 150 | .title("Content") 151 | .items([ 152 | reviewingAppliactionListItems, 153 | rejectedAppliactionListItems, 154 | approvedAppliactionListItems, 155 | featuredAppliactionListItems, 156 | S.divider(), 157 | reviewingSubmissionListItems, 158 | rejectedSubmissionListItems, 159 | approvedSubmissionListItems, 160 | S.divider(), 161 | featuredProductListItems, 162 | invisibleProductListItems, 163 | S.divider(), 164 | // S.documentTypeListItem('product').title('Products'), // List items with same ID found (product) 165 | 166 | // S.documentTypeListItem('product').title('Products'), 167 | 168 | ...defaultListItems, 169 | 170 | ...singletonItems,]); 171 | }; 172 | }; 173 | -------------------------------------------------------------------------------- /sanity/lib/queries.ts: -------------------------------------------------------------------------------- 1 | import { groq } from "next-sanity"; 2 | 3 | /** 4 | * products 5 | */ 6 | const tagFields = /* groq */ ` 7 | ..., 8 | "slug": slug.current, 9 | `; 10 | 11 | const categoryFields = /* groq */ ` 12 | ..., 13 | "slug": slug.current, 14 | "name": coalesce(name[$lang], name[$defaultLocale]), 15 | group-> { 16 | ..., 17 | "slug": slug.current, 18 | "name": coalesce(name[$lang], name[$defaultLocale]), 19 | }, 20 | `; 21 | 22 | const groupFields = /* groq */ ` 23 | ..., 24 | "slug": slug.current, 25 | "name": coalesce(name[$lang], name[$defaultLocale]), 26 | "categories": *[_type=='category' && references(^._id)] | order(order desc, _createdAt asc) 27 | { 28 | ..., 29 | "slug": slug.current, 30 | "name": coalesce(name[$lang], name[$defaultLocale]), 31 | } 32 | `; 33 | 34 | const guideFields = /* groq */ ` 35 | ..., 36 | "slug": slug.current, 37 | `; 38 | 39 | const apptypeFields = /* groq */ ` 40 | ..., 41 | "slug": slug.current, 42 | "name": coalesce(name[$lang], name[$defaultLocale]), 43 | `; 44 | 45 | // for sitemap 46 | export const productListQueryForSitemap = groq`*[_type == "product" && visible == true] | order(order desc, _createdAt asc) { 47 | _id, 48 | "slug": slug.current, 49 | }`; 50 | 51 | // for sitemap 52 | export const categoryListQueryForSitemap = groq`*[_type == "category"] | order(order desc, _createdAt asc) { 53 | _id, 54 | "slug": slug.current, 55 | group-> { 56 | _id, 57 | "slug": slug.current, 58 | }, 59 | }`; 60 | 61 | // for sitemap 62 | export const appListQueryForSitemap = groq`*[_type == "application" && status == "approved"] | order(order desc, _createdAt asc) { 63 | _id, 64 | name, 65 | }`; 66 | 67 | // for sitemap 68 | export const appTypeListQueryForSitemap = groq`*[_type == "appType"] | order(order desc, _createdAt asc) { 69 | _id, 70 | "slug": slug.current, 71 | }`; 72 | 73 | // for metadata 74 | export const categoryQuery = groq`*[_type == "category" && slug.current == $slug] [0] { 75 | _id, 76 | "name": coalesce(name[$lang], name[$defaultLocale]), 77 | }`; 78 | 79 | // for metadata 80 | export const appTypeQuery = groq`*[_type == "appType" && slug.current == $slug] [0] { 81 | _id, 82 | "name": coalesce(name[$lang], name[$defaultLocale]), 83 | }`; 84 | 85 | /** 86 | * 1、user queries 87 | * 2、_id is sanity id, id is database id 88 | */ 89 | const userFields = /* groq */ ` 90 | ... 91 | `; 92 | 93 | const productFields = /* groq */ ` 94 | ..., 95 | "slug": slug.current, 96 | "status": select(_originalId in path("drafts.**") => "draft", "published"), 97 | "desc": coalesce(desc[$lang], desc[$defaultLocale]), 98 | "date": coalesce(date, _createdAt), 99 | category-> { 100 | ${categoryFields} 101 | }, 102 | tags[]-> { 103 | ${tagFields} 104 | }, 105 | guides[]-> { 106 | ${guideFields} 107 | }, 108 | submitter-> { 109 | ${userFields} 110 | }, 111 | `; 112 | 113 | // this query is not used for now 114 | // "name": coalesce(name[$lang], name[$defaultLocale]), 115 | export const groupListQuery = groq`*[_type == "group"] | order(order desc, _createdAt asc) { 116 | ${groupFields} 117 | }`; 118 | 119 | export const groupQuery = groq`*[_type == "group" && slug.current == $slug] [0] { 120 | ${groupFields} 121 | }`; 122 | 123 | // "name": coalesce(name[$lang], name[$defaultLocale]), is working if convert the below to string in sanity.types.ts 124 | // name: Array<{ 125 | // _type: "localizedString"; 126 | // en?: string; 127 | // zh?: string; 128 | // }> | null; 129 | export const groupListWithCategoryQuery = groq`*[_type=="group"] | order(order desc, _createdAt asc) { 130 | ${groupFields} 131 | }`; 132 | 133 | export const categoryListQuery = groq`*[_type == "category"] | order(order desc, _createdAt asc) { 134 | ${categoryFields} 135 | }`; 136 | 137 | export const tagListQuery = groq`*[_type == "tag"] | order(order desc, _createdAt asc) { 138 | ${tagFields} 139 | }`; 140 | 141 | export const categoryListByGroupQuery = groq`*[_type == "category" && references(*[_type == "group" && slug.current == $groupSlug]._id)] | order(order desc, _createdAt asc) { 142 | ${categoryFields} 143 | }`; 144 | 145 | export const productListByGroupQuery = groq`*[_type == "product" && visible == true && category._ref in (*[_type == "category" && group._ref in (*[_type == "group" && slug.current == $groupSlug]._id)]._id)] | order(order desc, _createdAt asc) { 146 | ${productFields} 147 | }`; 148 | 149 | export const productListQuery = groq`*[_type == "product" && visible == true] | order(order desc, _createdAt asc) { 150 | ${productFields} 151 | }`; 152 | 153 | export const productListOfFeaturedQuery = groq`*[_type == "product" && visible == true && featured == true] | order(order desc, _createdAt asc) [0...$limit] { 154 | ${productFields} 155 | }`; 156 | 157 | export const productListByCategoryQuery = groq`*[_type == "product" && visible == true && references(*[_type == "category" && slug.current == $categorySlug]._id)] | order(order desc, _createdAt asc) { 158 | ${productFields} 159 | }`; 160 | 161 | export const productListOfRecentQuery = groq`*[_type == "product" && visible == true] | order(_createdAt desc) [0...$limit] { 162 | ${productFields} 163 | }`; 164 | 165 | export const productQuery = groq`*[_type == "product" && visible == true && slug.current == $slug] [0] { 166 | content, 167 | ${productFields} 168 | }`; 169 | 170 | /** 171 | * applications 172 | */ 173 | const applicationFields = /* groq */ ` 174 | ..., 175 | types[]-> { 176 | ${apptypeFields} 177 | }, 178 | user-> { 179 | ${userFields} 180 | }, 181 | `; 182 | 183 | export const appQuery = groq`*[_type == "application" && name == $slug] [0] { 184 | ${applicationFields} 185 | }`; 186 | 187 | export const appTypeListQuery = groq`*[_type == "appType"] | order(order desc, _createdAt asc) { 188 | ${apptypeFields} 189 | }`; 190 | 191 | export const applicationListOfFeaturedQuery = groq`*[_type == "application" && status == "approved" && featured == true] | order(order desc, _createdAt asc) { 192 | ${applicationFields} 193 | }`; 194 | 195 | export const applicationListOfRecentQuery = groq`*[_type == "application" && status == "approved"] | order(_createdAt desc) [0...$limit] { 196 | ${applicationFields} 197 | }`; 198 | 199 | export const applicationListByCategoryQuery = groq`*[_type == "application" && status == "approved" && references(*[_type == "appType" && slug.current == $categorySlug]._id)] | order(order desc, _createdAt asc) { 200 | ${applicationFields} 201 | }`; 202 | 203 | export const applicationListByUserQuery = groq`*[_type == "application" && references(*[_type == "user" && id == $userid]._id)] | order(_createdAt asc) { 204 | ${applicationFields} 205 | }`; 206 | 207 | export const userQuery = groq`*[_type == "user" && id == $userId][0] { 208 | ${userFields} 209 | }`; 210 | 211 | 212 | /** 213 | * demo queries 214 | */ 215 | const postFields = /* groq */ ` 216 | _id, 217 | "status": select(_originalId in path("drafts.**") => "draft", "published"), 218 | "title": coalesce(title, "Untitled"), 219 | "slug": slug.current, 220 | excerpt, 221 | coverImage, 222 | "date": coalesce(date, _updatedAt), 223 | "author": author->{"name": coalesce(name, "Anonymous"), picture}, 224 | `; 225 | 226 | export const moreStoriesQuery = groq`*[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] { 227 | ${postFields} 228 | }`; 229 | 230 | export const postQuery = groq`*[_type == "post" && slug.current == $slug] [0] { 231 | content, 232 | ${postFields} 233 | }`; 234 | 235 | export const settingsQuery = groq`*[_type == "settings"][0]`; 236 | 237 | export const heroQuery = groq`*[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] { 238 | ${postFields} 239 | }`; 240 | -------------------------------------------------------------------------------- /sanity/plugins/assist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets up the AI Assist plugin with preset prompts for content creation 3 | */ 4 | 5 | import { assist } from "@sanity/assist"; 6 | 7 | import postType from "../schemas/documents/post"; 8 | 9 | export const assistWithPresets = () => 10 | assist({ 11 | __presets: { 12 | [postType.name]: { 13 | fields: [ 14 | { 15 | /** 16 | * Creates Portable Text `content` blocks from the `title` field 17 | */ 18 | path: "content", 19 | instructions: [ 20 | { 21 | _key: "preset-instruction-1", 22 | title: "Generate sample content", 23 | icon: "block-content", 24 | prompt: [ 25 | { 26 | _key: "86e70087d4d5", 27 | markDefs: [], 28 | children: [ 29 | { 30 | _type: "span", 31 | marks: [], 32 | text: "Given the draft title ", 33 | _key: "6b5d5d6a63cf0", 34 | }, 35 | { 36 | path: "title", 37 | _type: "sanity.assist.instruction.fieldRef", 38 | _key: "0132742d463b", 39 | }, 40 | { 41 | _type: "span", 42 | marks: [], 43 | text: " of a blog post, generate a comprehensive and engaging sample content that spans the length of one to two A4 pages. The content should be structured, informative, and tailored to the subject matter implied by the title, whether it be travel, software engineering, fashion, politics, or any other theme. The text will be displayed below the ", 44 | _key: "a02c9ab4eb2d", 45 | }, 46 | { 47 | _type: "sanity.assist.instruction.fieldRef", 48 | _key: "f208ef240062", 49 | path: "title", 50 | }, 51 | { 52 | text: " and doesn't need to repeat it in the text. The generated text should include the following elements:", 53 | _key: "8ecfa74a8487", 54 | _type: "span", 55 | marks: [], 56 | }, 57 | ], 58 | _type: "block", 59 | style: "normal", 60 | }, 61 | { 62 | style: "normal", 63 | _key: "e4dded41ea89", 64 | markDefs: [], 65 | children: [ 66 | { 67 | _type: "span", 68 | marks: [], 69 | text: "1. Introduction: A brief paragraph that captures the essence of the blog post, hooks the reader with intriguing insights, and outlines the purpose of the post.", 70 | _key: "cc5ef44a2fb5", 71 | }, 72 | ], 73 | _type: "block", 74 | }, 75 | { 76 | style: "normal", 77 | _key: "585e8de2fe35", 78 | markDefs: [], 79 | children: [ 80 | { 81 | _type: "span", 82 | marks: [], 83 | text: "2. Main Body:", 84 | _key: "fab36eb7c541", 85 | }, 86 | ], 87 | _type: "block", 88 | }, 89 | { 90 | _type: "block", 91 | style: "normal", 92 | _key: "e96b89ef6357", 93 | markDefs: [], 94 | children: [ 95 | { 96 | _type: "span", 97 | marks: [], 98 | text: "- For thematic consistency, divide the body into several sections with subheadings that explore different facets of the topic.", 99 | _key: "b685a310a0ff", 100 | }, 101 | ], 102 | }, 103 | { 104 | children: [ 105 | { 106 | marks: [], 107 | text: "- Include engaging and informative content such as personal anecdotes (for travel or fashion blogs), technical explanations or tutorials (for software engineering blogs), satirical or humorous observations (for shitposting), or well-argued positions (for political blogs).", 108 | _key: "c7468d106c91", 109 | _type: "span", 110 | }, 111 | ], 112 | _type: "block", 113 | style: "normal", 114 | _key: "ce4acdb00da9", 115 | markDefs: [], 116 | }, 117 | { 118 | _type: "block", 119 | style: "normal", 120 | _key: "fb4572e65833", 121 | markDefs: [], 122 | children: [ 123 | { 124 | _type: "span", 125 | marks: [], 126 | text: "- ", 127 | _key: "5358f261dce4", 128 | }, 129 | { 130 | _type: "span", 131 | marks: [], 132 | text: " observations (for shitposting), or well-argued positions (for political blogs).", 133 | _key: "50792c6d0f77", 134 | }, 135 | ], 136 | }, 137 | { 138 | children: [ 139 | { 140 | marks: [], 141 | text: "Where applicable, incorporate bullet points or numbered lists to break down complex information, steps in a process, or key highlights.", 142 | _key: "3b891d8c1dde0", 143 | _type: "span", 144 | }, 145 | ], 146 | _type: "block", 147 | style: "normal", 148 | _key: "9364b67074ce", 149 | markDefs: [], 150 | }, 151 | { 152 | _key: "a6ba7579cd66", 153 | markDefs: [], 154 | children: [ 155 | { 156 | _type: "span", 157 | marks: [], 158 | text: "3. Conclusion: Summarize the main points discussed in the post, offer final thoughts or calls to action, and invite readers to engage with the content through comments or social media sharing.", 159 | _key: "1280f11d499d", 160 | }, 161 | ], 162 | _type: "block", 163 | style: "normal", 164 | }, 165 | { 166 | style: "normal", 167 | _key: "719a79eb4c1c", 168 | markDefs: [], 169 | children: [ 170 | { 171 | marks: [], 172 | text: "4. Engagement Prompts: Conclude with questions or prompts that encourage readers to share their experiences, opinions, or questions related to the blog post's topic, but keep in mind there is no Comments field below the blog post.", 173 | _key: "f1512086bab6", 174 | _type: "span", 175 | }, 176 | ], 177 | _type: "block", 178 | }, 179 | { 180 | _type: "block", 181 | style: "normal", 182 | _key: "4a1c586fd44a", 183 | markDefs: [], 184 | children: [ 185 | { 186 | marks: [], 187 | text: "Ensure the generated content maintains a balance between being informative and entertaining, to capture the interest of a wide audience. The sample content should serve as a solid foundation that can be further customized or expanded upon by the blog author to finalize the post.", 188 | _key: "697bbd03cb110", 189 | _type: "span", 190 | }, 191 | ], 192 | }, 193 | { 194 | children: [ 195 | { 196 | marks: [], 197 | text: 'Don\'t prefix each section with "Introduction", "Main Body", "Conclusion" or "Engagement Prompts"', 198 | _key: "d20bb9a03b0d", 199 | _type: "span", 200 | }, 201 | ], 202 | _type: "block", 203 | style: "normal", 204 | _key: "b072b3c62c3c", 205 | markDefs: [], 206 | }, 207 | ], 208 | }, 209 | ], 210 | }, 211 | { 212 | /** 213 | * Summarize content into the `excerpt` field 214 | */ 215 | path: "excerpt", 216 | instructions: [ 217 | { 218 | _key: "preset-instruction-2", 219 | title: "Summarize content", 220 | icon: "blockquote", 221 | prompt: [ 222 | { 223 | markDefs: [], 224 | children: [ 225 | { 226 | _key: "650a0dcc327d", 227 | _type: "span", 228 | marks: [], 229 | text: "Create a short excerpt based on ", 230 | }, 231 | { 232 | path: "content", 233 | _type: "sanity.assist.instruction.fieldRef", 234 | _key: "c62d14c73496", 235 | }, 236 | { 237 | _key: "38e043efa606", 238 | _type: "span", 239 | marks: [], 240 | text: " that doesn't repeat what's already in the ", 241 | }, 242 | { 243 | path: "title", 244 | _type: "sanity.assist.instruction.fieldRef", 245 | _key: "445e62dda246", 246 | }, 247 | { 248 | _key: "98cce773915e", 249 | _type: "span", 250 | marks: [], 251 | text: " . Consider the UI has limited horizontal space and try to avoid too many line breaks and make it as short, terse and brief as possible. At best a single sentence, at most two sentences.", 252 | }, 253 | ], 254 | _type: "block", 255 | style: "normal", 256 | _key: "392c618784b0", 257 | }, 258 | ], 259 | }, 260 | ], 261 | }, 262 | ], 263 | }, 264 | }, 265 | }); 266 | -------------------------------------------------------------------------------- /sanity.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * --------------------------------------------------------------------------------- 3 | * This file has been generated by Sanity TypeGen. 4 | * Command: `sanity typegen generate` 5 | * 6 | * Any modifications made directly to this file will be overwritten the next time 7 | * the TypeScript definitions are generated. Please make changes to the Sanity 8 | * schema definitions and/or GROQ queries if you need to update these types. 9 | * 10 | * For more information on how to use Sanity TypeGen, visit the official documentation: 11 | * https://www.sanity.io/docs/sanity-typegen 12 | * --------------------------------------------------------------------------------- 13 | */ 14 | 15 | // Source: schema.json 16 | export type SanityImagePaletteSwatch = { 17 | _type: "sanity.imagePaletteSwatch"; 18 | background?: string; 19 | foreground?: string; 20 | population?: number; 21 | title?: string; 22 | }; 23 | 24 | export type SanityImagePalette = { 25 | _type: "sanity.imagePalette"; 26 | darkMuted?: SanityImagePaletteSwatch; 27 | lightVibrant?: SanityImagePaletteSwatch; 28 | darkVibrant?: SanityImagePaletteSwatch; 29 | vibrant?: SanityImagePaletteSwatch; 30 | dominant?: SanityImagePaletteSwatch; 31 | lightMuted?: SanityImagePaletteSwatch; 32 | muted?: SanityImagePaletteSwatch; 33 | }; 34 | 35 | export type SanityImageDimensions = { 36 | _type: "sanity.imageDimensions"; 37 | height?: number; 38 | width?: number; 39 | aspectRatio?: number; 40 | }; 41 | 42 | export type SanityFileAsset = { 43 | _id: string; 44 | _type: "sanity.fileAsset"; 45 | _createdAt: string; 46 | _updatedAt: string; 47 | _rev: string; 48 | originalFilename?: string; 49 | label?: string; 50 | title?: string; 51 | description?: string; 52 | altText?: string; 53 | sha1hash?: string; 54 | extension?: string; 55 | mimeType?: string; 56 | size?: number; 57 | assetId?: string; 58 | uploadId?: string; 59 | path?: string; 60 | url?: string; 61 | source?: SanityAssetSourceData; 62 | }; 63 | 64 | export type Geopoint = { 65 | _type: "geopoint"; 66 | lat?: number; 67 | lng?: number; 68 | alt?: number; 69 | }; 70 | 71 | export type Comment = { 72 | _id: string; 73 | _type: "comment"; 74 | _createdAt: string; 75 | _updatedAt: string; 76 | _rev: string; 77 | name?: string; 78 | email?: string; 79 | comment?: string; 80 | post?: { 81 | _ref: string; 82 | _type: "reference"; 83 | _weak?: boolean; 84 | [internalGroqTypeReferenceTo]?: "product"; 85 | }; 86 | }; 87 | 88 | export type Post = { 89 | _id: string; 90 | _type: "post"; 91 | _createdAt: string; 92 | _updatedAt: string; 93 | _rev: string; 94 | title?: string; 95 | slug?: Slug; 96 | content?: Array<{ 97 | children?: Array<{ 98 | marks?: Array; 99 | text?: string; 100 | _type: "span"; 101 | _key: string; 102 | }>; 103 | style?: "normal" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote"; 104 | listItem?: "bullet" | "number"; 105 | markDefs?: Array<{ 106 | href?: string; 107 | _type: "link"; 108 | _key: string; 109 | }>; 110 | level?: number; 111 | _type: "block"; 112 | _key: string; 113 | } | { 114 | asset?: { 115 | _ref: string; 116 | _type: "reference"; 117 | _weak?: boolean; 118 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 119 | }; 120 | hotspot?: SanityImageHotspot; 121 | crop?: SanityImageCrop; 122 | _type: "image"; 123 | _key: string; 124 | } | ({ 125 | _key: string; 126 | } & Code)>; 127 | excerpt?: string; 128 | coverImage?: { 129 | asset?: { 130 | _ref: string; 131 | _type: "reference"; 132 | _weak?: boolean; 133 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 134 | }; 135 | hotspot?: SanityImageHotspot; 136 | crop?: SanityImageCrop; 137 | alt?: string; 138 | _type: "image"; 139 | }; 140 | date?: string; 141 | author?: { 142 | _ref: string; 143 | _type: "reference"; 144 | _weak?: boolean; 145 | [internalGroqTypeReferenceTo]?: "author"; 146 | }; 147 | }; 148 | 149 | export type Author = { 150 | _id: string; 151 | _type: "author"; 152 | _createdAt: string; 153 | _updatedAt: string; 154 | _rev: string; 155 | name?: string; 156 | picture?: { 157 | asset?: { 158 | _ref: string; 159 | _type: "reference"; 160 | _weak?: boolean; 161 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 162 | }; 163 | hotspot?: SanityImageHotspot; 164 | crop?: SanityImageCrop; 165 | alt?: string; 166 | _type: "image"; 167 | }; 168 | }; 169 | 170 | export type AppType = { 171 | _id: string; 172 | _type: "appType"; 173 | _createdAt: string; 174 | _updatedAt: string; 175 | _rev: string; 176 | name?: LocalizedString; 177 | slug?: Slug; 178 | order?: number; 179 | date?: string; 180 | }; 181 | 182 | export type Tag = { 183 | _id: string; 184 | _type: "tag"; 185 | _createdAt: string; 186 | _updatedAt: string; 187 | _rev: string; 188 | name?: string; 189 | slug?: Slug; 190 | order?: number; 191 | date?: string; 192 | }; 193 | 194 | export type Guide = { 195 | _id: string; 196 | _type: "guide"; 197 | _createdAt: string; 198 | _updatedAt: string; 199 | _rev: string; 200 | name?: string; 201 | slug?: Slug; 202 | excerpt?: string; 203 | link?: string; 204 | order?: number; 205 | date?: string; 206 | }; 207 | 208 | export type Application = { 209 | _id: string; 210 | _type: "application"; 211 | _createdAt: string; 212 | _updatedAt: string; 213 | _rev: string; 214 | name?: string; 215 | description?: string; 216 | link?: string; 217 | types?: Array<{ 218 | _ref: string; 219 | _type: "reference"; 220 | _weak?: boolean; 221 | _key: string; 222 | [internalGroqTypeReferenceTo]?: "appType"; 223 | }>; 224 | featured?: boolean; 225 | status?: "reviewing" | "rejected" | "approved"; 226 | reason?: "rejected: please upload a better logo image" | "rejected: please upload a better cover image" | "rejected: this indie app seems not ready?" | "rejected: only support self-built indie app"; 227 | image?: { 228 | asset?: { 229 | _ref: string; 230 | _type: "reference"; 231 | _weak?: boolean; 232 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 233 | }; 234 | hotspot?: SanityImageHotspot; 235 | crop?: SanityImageCrop; 236 | _type: "image"; 237 | }; 238 | cover?: { 239 | asset?: { 240 | _ref: string; 241 | _type: "reference"; 242 | _weak?: boolean; 243 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 244 | }; 245 | hotspot?: SanityImageHotspot; 246 | crop?: SanityImageCrop; 247 | _type: "image"; 248 | }; 249 | user?: { 250 | _ref: string; 251 | _type: "reference"; 252 | _weak?: boolean; 253 | [internalGroqTypeReferenceTo]?: "user"; 254 | }; 255 | date?: string; 256 | }; 257 | 258 | export type Submission = { 259 | _id: string; 260 | _type: "submission"; 261 | _createdAt: string; 262 | _updatedAt: string; 263 | _rev: string; 264 | name?: string; 265 | link?: string; 266 | status?: "reviewing" | "rejected" | "approved"; 267 | reason?: "rejected: this product is not for indie hackers"; 268 | user?: { 269 | _ref: string; 270 | _type: "reference"; 271 | _weak?: boolean; 272 | [internalGroqTypeReferenceTo]?: "user"; 273 | }; 274 | date?: string; 275 | }; 276 | 277 | export type Product = { 278 | _id: string; 279 | _type: "product"; 280 | _createdAt: string; 281 | _updatedAt: string; 282 | _rev: string; 283 | name?: string; 284 | slug?: Slug; 285 | order?: number; 286 | category?: { 287 | _ref: string; 288 | _type: "reference"; 289 | _weak?: boolean; 290 | [internalGroqTypeReferenceTo]?: "category"; 291 | }; 292 | tags?: Array<{ 293 | _ref: string; 294 | _type: "reference"; 295 | _weak?: boolean; 296 | _key: string; 297 | [internalGroqTypeReferenceTo]?: "tag"; 298 | }>; 299 | featured?: boolean; 300 | visible?: boolean; 301 | website?: string; 302 | github?: string; 303 | priceLink?: string; 304 | price?: "Free" | "Paid" | "Free & Paid"; 305 | source?: string; 306 | submitter?: { 307 | _ref: string; 308 | _type: "reference"; 309 | _weak?: boolean; 310 | [internalGroqTypeReferenceTo]?: "user"; 311 | }; 312 | desc?: LocalizedString; 313 | content?: Array<{ 314 | children?: Array<{ 315 | marks?: Array; 316 | text?: string; 317 | _type: "span"; 318 | _key: string; 319 | }>; 320 | style?: "normal" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote"; 321 | listItem?: "bullet" | "number"; 322 | markDefs?: Array<{ 323 | href?: string; 324 | _type: "link"; 325 | _key: string; 326 | }>; 327 | level?: number; 328 | _type: "block"; 329 | _key: string; 330 | } | { 331 | asset?: { 332 | _ref: string; 333 | _type: "reference"; 334 | _weak?: boolean; 335 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 336 | }; 337 | hotspot?: SanityImageHotspot; 338 | crop?: SanityImageCrop; 339 | _type: "image"; 340 | _key: string; 341 | } | ({ 342 | _key: string; 343 | } & Code)>; 344 | logo?: { 345 | asset?: { 346 | _ref: string; 347 | _type: "reference"; 348 | _weak?: boolean; 349 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 350 | }; 351 | hotspot?: SanityImageHotspot; 352 | crop?: SanityImageCrop; 353 | _type: "image"; 354 | }; 355 | coverImage?: { 356 | asset?: { 357 | _ref: string; 358 | _type: "reference"; 359 | _weak?: boolean; 360 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 361 | }; 362 | hotspot?: SanityImageHotspot; 363 | crop?: SanityImageCrop; 364 | _type: "image"; 365 | }; 366 | guides?: Array<{ 367 | _ref: string; 368 | _type: "reference"; 369 | _weak?: boolean; 370 | _key: string; 371 | [internalGroqTypeReferenceTo]?: "guide"; 372 | }>; 373 | date?: string; 374 | }; 375 | 376 | export type User = { 377 | _id: string; 378 | _type: "user"; 379 | _createdAt: string; 380 | _updatedAt: string; 381 | _rev: string; 382 | name?: string; 383 | id?: string; 384 | email?: string; 385 | avatar?: string; 386 | link?: string; 387 | date?: string; 388 | }; 389 | 390 | export type Category = { 391 | _id: string; 392 | _type: "category"; 393 | _createdAt: string; 394 | _updatedAt: string; 395 | _rev: string; 396 | name?: LocalizedString; 397 | slug?: Slug; 398 | group?: { 399 | _ref: string; 400 | _type: "reference"; 401 | _weak?: boolean; 402 | [internalGroqTypeReferenceTo]?: "group"; 403 | }; 404 | order?: number; 405 | date?: string; 406 | }; 407 | 408 | export type Group = { 409 | _id: string; 410 | _type: "group"; 411 | _createdAt: string; 412 | _updatedAt: string; 413 | _rev: string; 414 | name?: LocalizedString; 415 | slug?: Slug; 416 | order?: number; 417 | date?: string; 418 | }; 419 | 420 | export type LocalizedString = { 421 | _type: "localizedString"; 422 | en?: string; 423 | zh?: string; 424 | }; 425 | 426 | export type Settings = { 427 | _id: string; 428 | _type: "settings"; 429 | _createdAt: string; 430 | _updatedAt: string; 431 | _rev: string; 432 | title?: string; 433 | subtitle?: string; 434 | description?: Array<{ 435 | children?: Array<{ 436 | marks?: Array; 437 | text?: string; 438 | _type: "span"; 439 | _key: string; 440 | }>; 441 | style?: "normal"; 442 | listItem?: never; 443 | markDefs?: Array<{ 444 | href?: string; 445 | _type: "link"; 446 | _key: string; 447 | }>; 448 | level?: number; 449 | _type: "block"; 450 | _key: string; 451 | }>; 452 | footer?: Array<{ 453 | children?: Array<{ 454 | marks?: Array; 455 | text?: string; 456 | _type: "span"; 457 | _key: string; 458 | }>; 459 | style?: "normal" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote"; 460 | listItem?: "bullet" | "number"; 461 | markDefs?: Array<{ 462 | href?: string; 463 | _type: "link"; 464 | _key: string; 465 | }>; 466 | level?: number; 467 | _type: "block"; 468 | _key: string; 469 | }>; 470 | ogImage?: { 471 | asset?: { 472 | _ref: string; 473 | _type: "reference"; 474 | _weak?: boolean; 475 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 476 | }; 477 | hotspot?: SanityImageHotspot; 478 | crop?: SanityImageCrop; 479 | alt?: string; 480 | metadataBase?: string; 481 | _type: "image"; 482 | }; 483 | }; 484 | 485 | export type SanityImageCrop = { 486 | _type: "sanity.imageCrop"; 487 | top?: number; 488 | bottom?: number; 489 | left?: number; 490 | right?: number; 491 | }; 492 | 493 | export type SanityImageHotspot = { 494 | _type: "sanity.imageHotspot"; 495 | x?: number; 496 | y?: number; 497 | height?: number; 498 | width?: number; 499 | }; 500 | 501 | export type SanityImageAsset = { 502 | _id: string; 503 | _type: "sanity.imageAsset"; 504 | _createdAt: string; 505 | _updatedAt: string; 506 | _rev: string; 507 | originalFilename?: string; 508 | label?: string; 509 | title?: string; 510 | description?: string; 511 | altText?: string; 512 | sha1hash?: string; 513 | extension?: string; 514 | mimeType?: string; 515 | size?: number; 516 | assetId?: string; 517 | uploadId?: string; 518 | path?: string; 519 | url?: string; 520 | metadata?: SanityImageMetadata; 521 | source?: SanityAssetSourceData; 522 | }; 523 | 524 | export type SanityAssetSourceData = { 525 | _type: "sanity.assetSourceData"; 526 | name?: string; 527 | id?: string; 528 | url?: string; 529 | }; 530 | 531 | export type SanityImageMetadata = { 532 | _type: "sanity.imageMetadata"; 533 | location?: Geopoint; 534 | dimensions?: SanityImageDimensions; 535 | palette?: SanityImagePalette; 536 | lqip?: string; 537 | blurHash?: string; 538 | hasAlpha?: boolean; 539 | isOpaque?: boolean; 540 | }; 541 | 542 | export type Code = { 543 | _type: "code"; 544 | language?: string; 545 | filename?: string; 546 | code?: string; 547 | highlightedLines?: Array; 548 | }; 549 | 550 | export type Color = { 551 | _type: "color"; 552 | hex?: string; 553 | alpha?: number; 554 | hsl?: HslaColor; 555 | hsv?: HsvaColor; 556 | rgb?: RgbaColor; 557 | }; 558 | 559 | export type RgbaColor = { 560 | _type: "rgbaColor"; 561 | r?: number; 562 | g?: number; 563 | b?: number; 564 | a?: number; 565 | }; 566 | 567 | export type HsvaColor = { 568 | _type: "hsvaColor"; 569 | h?: number; 570 | s?: number; 571 | v?: number; 572 | a?: number; 573 | }; 574 | 575 | export type HslaColor = { 576 | _type: "hslaColor"; 577 | h?: number; 578 | s?: number; 579 | l?: number; 580 | a?: number; 581 | }; 582 | 583 | export type Markdown = string; 584 | 585 | export type MediaTag = { 586 | _id: string; 587 | _type: "media.tag"; 588 | _createdAt: string; 589 | _updatedAt: string; 590 | _rev: string; 591 | name?: Slug; 592 | }; 593 | 594 | export type Slug = { 595 | _type: "slug"; 596 | current?: string; 597 | source?: string; 598 | }; 599 | export declare const internalGroqTypeReferenceTo: unique symbol; 600 | 601 | // Source: sanity/lib/queries.ts 602 | // Variable: productListQueryForSitemap 603 | // Query: *[_type == "product" && visible == true] | order(order desc, _createdAt asc) { _id, "slug": slug.current,} 604 | export type ProductListQueryForSitemapResult = Array<{ 605 | _id: string; 606 | slug: string | null; 607 | }>; 608 | // Variable: categoryListQueryForSitemap 609 | // Query: *[_type == "category"] | order(order desc, _createdAt asc) { _id, "slug": slug.current, group-> { _id, "slug": slug.current, },} 610 | export type CategoryListQueryForSitemapResult = Array<{ 611 | _id: string; 612 | slug: string | null; 613 | group: { 614 | _id: string; 615 | slug: string | null; 616 | } | null; 617 | }>; 618 | // Variable: appListQueryForSitemap 619 | // Query: *[_type == "application" && status == "approved"] | order(order desc, _createdAt asc) { _id, name,} 620 | export type AppListQueryForSitemapResult = Array<{ 621 | _id: string; 622 | name: string | null; 623 | }>; 624 | // Variable: appTypeListQueryForSitemap 625 | // Query: *[_type == "appType"] | order(order desc, _createdAt asc) { _id, "slug": slug.current,} 626 | export type AppTypeListQueryForSitemapResult = Array<{ 627 | _id: string; 628 | slug: string | null; 629 | }>; 630 | // Variable: categoryQuery 631 | // Query: *[_type == "category" && slug.current == $slug] [0] { _id, "name": coalesce(name[$lang], name[$defaultLocale]),} 632 | export type CategoryQueryResult = { 633 | _id: string; 634 | name: string; 635 | } | null; 636 | // Variable: appTypeQuery 637 | // Query: *[_type == "appType" && slug.current == $slug] [0] { _id, "name": coalesce(name[$lang], name[$defaultLocale]),} 638 | export type AppTypeQueryResult = { 639 | _id: string; 640 | name: string; 641 | } | null; 642 | // Variable: groupListQuery 643 | // Query: *[_type == "group"] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), "categories": *[_type=='category' && references(^._id)] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }} 644 | export type GroupListQueryResult = Array<{ 645 | _id: string; 646 | _type: "group"; 647 | _createdAt: string; 648 | _updatedAt: string; 649 | _rev: string; 650 | name: string; 651 | slug: string | null; 652 | order?: number; 653 | date?: string; 654 | categories: Array<{ 655 | _id: string; 656 | _type: "category"; 657 | _createdAt: string; 658 | _updatedAt: string; 659 | _rev: string; 660 | name: string; 661 | slug: string | null; 662 | group?: { 663 | _ref: string; 664 | _type: "reference"; 665 | _weak?: boolean; 666 | [internalGroqTypeReferenceTo]?: "group"; 667 | }; 668 | order?: number; 669 | date?: string; 670 | }>; 671 | }>; 672 | // Variable: groupQuery 673 | // Query: *[_type == "group" && slug.current == $slug] [0] { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), "categories": *[_type=='category' && references(^._id)] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }} 674 | export type GroupQueryResult = { 675 | _id: string; 676 | _type: "group"; 677 | _createdAt: string; 678 | _updatedAt: string; 679 | _rev: string; 680 | name: string; 681 | slug: string | null; 682 | order?: number; 683 | date?: string; 684 | categories: Array<{ 685 | _id: string; 686 | _type: "category"; 687 | _createdAt: string; 688 | _updatedAt: string; 689 | _rev: string; 690 | name: string; 691 | slug: string | null; 692 | group?: { 693 | _ref: string; 694 | _type: "reference"; 695 | _weak?: boolean; 696 | [internalGroqTypeReferenceTo]?: "group"; 697 | }; 698 | order?: number; 699 | date?: string; 700 | }>; 701 | } | null; 702 | // Variable: groupListWithCategoryQuery 703 | // Query: *[_type=="group"] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), "categories": *[_type=='category' && references(^._id)] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }} 704 | export type GroupListWithCategoryQueryResult = Array<{ 705 | _id: string; 706 | _type: "group"; 707 | _createdAt: string; 708 | _updatedAt: string; 709 | _rev: string; 710 | name: string; 711 | slug: string | null; 712 | order?: number; 713 | date?: string; 714 | categories: Array<{ 715 | _id: string; 716 | _type: "category"; 717 | _createdAt: string; 718 | _updatedAt: string; 719 | _rev: string; 720 | name: string; 721 | slug: string | null; 722 | group?: { 723 | _ref: string; 724 | _type: "reference"; 725 | _weak?: boolean; 726 | [internalGroqTypeReferenceTo]?: "group"; 727 | }; 728 | order?: number; 729 | date?: string; 730 | }>; 731 | }>; 732 | // Variable: categoryListQuery 733 | // Query: *[_type == "category"] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), },} 734 | export type CategoryListQueryResult = Array<{ 735 | _id: string; 736 | _type: "category"; 737 | _createdAt: string; 738 | _updatedAt: string; 739 | _rev: string; 740 | name: string; 741 | slug: string | null; 742 | group: { 743 | _id: string; 744 | _type: "group"; 745 | _createdAt: string; 746 | _updatedAt: string; 747 | _rev: string; 748 | name: string; 749 | slug: string | null; 750 | order?: number; 751 | date?: string; 752 | } | null; 753 | order?: number; 754 | date?: string; 755 | }>; 756 | // Variable: tagListQuery 757 | // Query: *[_type == "tag"] | order(order desc, _createdAt asc) { ..., "slug": slug.current,} 758 | export type TagListQueryResult = Array<{ 759 | _id: string; 760 | _type: "tag"; 761 | _createdAt: string; 762 | _updatedAt: string; 763 | _rev: string; 764 | name?: string; 765 | slug: string | null; 766 | order?: number; 767 | date?: string; 768 | }>; 769 | // Variable: categoryListByGroupQuery 770 | // Query: *[_type == "category" && references(*[_type == "group" && slug.current == $groupSlug]._id)] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), },} 771 | export type CategoryListByGroupQueryResult = Array<{ 772 | _id: string; 773 | _type: "category"; 774 | _createdAt: string; 775 | _updatedAt: string; 776 | _rev: string; 777 | name: string; 778 | slug: string | null; 779 | group: { 780 | _id: string; 781 | _type: "group"; 782 | _createdAt: string; 783 | _updatedAt: string; 784 | _rev: string; 785 | name: string; 786 | slug: string | null; 787 | order?: number; 788 | date?: string; 789 | } | null; 790 | order?: number; 791 | date?: string; 792 | }>; 793 | // Variable: productListByGroupQuery 794 | // Query: *[_type == "product" && visible == true && category._ref in (*[_type == "category" && group._ref in (*[_type == "group" && slug.current == $groupSlug]._id)]._id)] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "status": select(_originalId in path("drafts.**") => "draft", "published"), "desc": coalesce(desc[$lang], desc[$defaultLocale]), "date": coalesce(date, _createdAt), category-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, }, tags[]-> { ..., "slug": slug.current, }, guides[]-> { ..., "slug": slug.current, }, submitter-> { ... },} 795 | export type ProductListByGroupQueryResult = Array; 796 | // Variable: productListQuery 797 | // Query: *[_type == "product" && visible == true] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "status": select(_originalId in path("drafts.**") => "draft", "published"), "desc": coalesce(desc[$lang], desc[$defaultLocale]), "date": coalesce(date, _createdAt), category-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, }, tags[]-> { ..., "slug": slug.current, }, guides[]-> { ..., "slug": slug.current, }, submitter-> { ... },} 798 | export type ProductListQueryResult = Array<{ 799 | _id: string; 800 | _type: "product"; 801 | _createdAt: string; 802 | _updatedAt: string; 803 | _rev: string; 804 | name?: string; 805 | slug: string | null; 806 | order?: number; 807 | category: { 808 | _id: string; 809 | _type: "category"; 810 | _createdAt: string; 811 | _updatedAt: string; 812 | _rev: string; 813 | name: string; 814 | slug: string | null; 815 | group: { 816 | _id: string; 817 | _type: "group"; 818 | _createdAt: string; 819 | _updatedAt: string; 820 | _rev: string; 821 | name: string; 822 | slug: string | null; 823 | order?: number; 824 | date?: string; 825 | } | null; 826 | order?: number; 827 | date?: string; 828 | } | null; 829 | tags: Array<{ 830 | _id: string; 831 | _type: "tag"; 832 | _createdAt: string; 833 | _updatedAt: string; 834 | _rev: string; 835 | name?: string; 836 | slug: string | null; 837 | order?: number; 838 | date?: string; 839 | }> | null; 840 | featured?: boolean; 841 | visible?: boolean; 842 | website?: string; 843 | github?: string; 844 | priceLink?: string; 845 | price?: "Free" | "Free & Paid" | "Paid"; 846 | source?: string; 847 | submitter: { 848 | _id: string; 849 | _type: "user"; 850 | _createdAt: string; 851 | _updatedAt: string; 852 | _rev: string; 853 | name?: string; 854 | id?: string; 855 | email?: string; 856 | avatar?: string; 857 | link?: string; 858 | date?: string; 859 | } | null; 860 | desc: string; 861 | content?: Array<({ 862 | _key: string; 863 | } & Code) | { 864 | asset?: { 865 | _ref: string; 866 | _type: "reference"; 867 | _weak?: boolean; 868 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 869 | }; 870 | hotspot?: SanityImageHotspot; 871 | crop?: SanityImageCrop; 872 | _type: "image"; 873 | _key: string; 874 | } | { 875 | children?: Array<{ 876 | marks?: Array; 877 | text?: string; 878 | _type: "span"; 879 | _key: string; 880 | }>; 881 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 882 | listItem?: "bullet" | "number"; 883 | markDefs?: Array<{ 884 | href?: string; 885 | _type: "link"; 886 | _key: string; 887 | }>; 888 | level?: number; 889 | _type: "block"; 890 | _key: string; 891 | }>; 892 | logo?: { 893 | asset?: { 894 | _ref: string; 895 | _type: "reference"; 896 | _weak?: boolean; 897 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 898 | }; 899 | hotspot?: SanityImageHotspot; 900 | crop?: SanityImageCrop; 901 | _type: "image"; 902 | }; 903 | coverImage?: { 904 | asset?: { 905 | _ref: string; 906 | _type: "reference"; 907 | _weak?: boolean; 908 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 909 | }; 910 | hotspot?: SanityImageHotspot; 911 | crop?: SanityImageCrop; 912 | _type: "image"; 913 | }; 914 | guides: Array<{ 915 | _id: string; 916 | _type: "guide"; 917 | _createdAt: string; 918 | _updatedAt: string; 919 | _rev: string; 920 | name?: string; 921 | slug: string | null; 922 | excerpt?: string; 923 | link?: string; 924 | order?: number; 925 | date?: string; 926 | }> | null; 927 | date: string; 928 | status: "draft" | "published"; 929 | }>; 930 | // Variable: productListOfFeaturedQuery 931 | // Query: *[_type == "product" && visible == true && featured == true] | order(order desc, _createdAt asc) [0...$limit] { ..., "slug": slug.current, "status": select(_originalId in path("drafts.**") => "draft", "published"), "desc": coalesce(desc[$lang], desc[$defaultLocale]), "date": coalesce(date, _createdAt), category-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, }, tags[]-> { ..., "slug": slug.current, }, guides[]-> { ..., "slug": slug.current, }, submitter-> { ... },} 932 | export type ProductListOfFeaturedQueryResult = Array<{ 933 | _id: string; 934 | _type: "product"; 935 | _createdAt: string; 936 | _updatedAt: string; 937 | _rev: string; 938 | name?: string; 939 | slug: string | null; 940 | order?: number; 941 | category: { 942 | _id: string; 943 | _type: "category"; 944 | _createdAt: string; 945 | _updatedAt: string; 946 | _rev: string; 947 | name: string; 948 | slug: string | null; 949 | group: { 950 | _id: string; 951 | _type: "group"; 952 | _createdAt: string; 953 | _updatedAt: string; 954 | _rev: string; 955 | name: string; 956 | slug: string | null; 957 | order?: number; 958 | date?: string; 959 | } | null; 960 | order?: number; 961 | date?: string; 962 | } | null; 963 | tags: Array<{ 964 | _id: string; 965 | _type: "tag"; 966 | _createdAt: string; 967 | _updatedAt: string; 968 | _rev: string; 969 | name?: string; 970 | slug: string | null; 971 | order?: number; 972 | date?: string; 973 | }> | null; 974 | featured?: boolean; 975 | visible?: boolean; 976 | website?: string; 977 | github?: string; 978 | priceLink?: string; 979 | price?: "Free" | "Free & Paid" | "Paid"; 980 | source?: string; 981 | submitter: { 982 | _id: string; 983 | _type: "user"; 984 | _createdAt: string; 985 | _updatedAt: string; 986 | _rev: string; 987 | name?: string; 988 | id?: string; 989 | email?: string; 990 | avatar?: string; 991 | link?: string; 992 | date?: string; 993 | } | null; 994 | desc: string; 995 | content?: Array<({ 996 | _key: string; 997 | } & Code) | { 998 | asset?: { 999 | _ref: string; 1000 | _type: "reference"; 1001 | _weak?: boolean; 1002 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1003 | }; 1004 | hotspot?: SanityImageHotspot; 1005 | crop?: SanityImageCrop; 1006 | _type: "image"; 1007 | _key: string; 1008 | } | { 1009 | children?: Array<{ 1010 | marks?: Array; 1011 | text?: string; 1012 | _type: "span"; 1013 | _key: string; 1014 | }>; 1015 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 1016 | listItem?: "bullet" | "number"; 1017 | markDefs?: Array<{ 1018 | href?: string; 1019 | _type: "link"; 1020 | _key: string; 1021 | }>; 1022 | level?: number; 1023 | _type: "block"; 1024 | _key: string; 1025 | }>; 1026 | logo?: { 1027 | asset?: { 1028 | _ref: string; 1029 | _type: "reference"; 1030 | _weak?: boolean; 1031 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1032 | }; 1033 | hotspot?: SanityImageHotspot; 1034 | crop?: SanityImageCrop; 1035 | _type: "image"; 1036 | }; 1037 | coverImage?: { 1038 | asset?: { 1039 | _ref: string; 1040 | _type: "reference"; 1041 | _weak?: boolean; 1042 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1043 | }; 1044 | hotspot?: SanityImageHotspot; 1045 | crop?: SanityImageCrop; 1046 | _type: "image"; 1047 | }; 1048 | guides: Array<{ 1049 | _id: string; 1050 | _type: "guide"; 1051 | _createdAt: string; 1052 | _updatedAt: string; 1053 | _rev: string; 1054 | name?: string; 1055 | slug: string | null; 1056 | excerpt?: string; 1057 | link?: string; 1058 | order?: number; 1059 | date?: string; 1060 | }> | null; 1061 | date: string; 1062 | status: "draft" | "published"; 1063 | }>; 1064 | // Variable: productListByCategoryQuery 1065 | // Query: *[_type == "product" && visible == true && references(*[_type == "category" && slug.current == $categorySlug]._id)] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "status": select(_originalId in path("drafts.**") => "draft", "published"), "desc": coalesce(desc[$lang], desc[$defaultLocale]), "date": coalesce(date, _createdAt), category-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, }, tags[]-> { ..., "slug": slug.current, }, guides[]-> { ..., "slug": slug.current, }, submitter-> { ... },} 1066 | export type ProductListByCategoryQueryResult = Array<{ 1067 | _id: string; 1068 | _type: "product"; 1069 | _createdAt: string; 1070 | _updatedAt: string; 1071 | _rev: string; 1072 | name?: string; 1073 | slug: string | null; 1074 | order?: number; 1075 | category: { 1076 | _id: string; 1077 | _type: "category"; 1078 | _createdAt: string; 1079 | _updatedAt: string; 1080 | _rev: string; 1081 | name: string; 1082 | slug: string | null; 1083 | group: { 1084 | _id: string; 1085 | _type: "group"; 1086 | _createdAt: string; 1087 | _updatedAt: string; 1088 | _rev: string; 1089 | name: string; 1090 | slug: string | null; 1091 | order?: number; 1092 | date?: string; 1093 | } | null; 1094 | order?: number; 1095 | date?: string; 1096 | } | null; 1097 | tags: Array<{ 1098 | _id: string; 1099 | _type: "tag"; 1100 | _createdAt: string; 1101 | _updatedAt: string; 1102 | _rev: string; 1103 | name?: string; 1104 | slug: string | null; 1105 | order?: number; 1106 | date?: string; 1107 | }> | null; 1108 | featured?: boolean; 1109 | visible?: boolean; 1110 | website?: string; 1111 | github?: string; 1112 | priceLink?: string; 1113 | price?: "Free" | "Free & Paid" | "Paid"; 1114 | source?: string; 1115 | submitter: { 1116 | _id: string; 1117 | _type: "user"; 1118 | _createdAt: string; 1119 | _updatedAt: string; 1120 | _rev: string; 1121 | name?: string; 1122 | id?: string; 1123 | email?: string; 1124 | avatar?: string; 1125 | link?: string; 1126 | date?: string; 1127 | } | null; 1128 | desc: string; 1129 | content?: Array<({ 1130 | _key: string; 1131 | } & Code) | { 1132 | asset?: { 1133 | _ref: string; 1134 | _type: "reference"; 1135 | _weak?: boolean; 1136 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1137 | }; 1138 | hotspot?: SanityImageHotspot; 1139 | crop?: SanityImageCrop; 1140 | _type: "image"; 1141 | _key: string; 1142 | } | { 1143 | children?: Array<{ 1144 | marks?: Array; 1145 | text?: string; 1146 | _type: "span"; 1147 | _key: string; 1148 | }>; 1149 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 1150 | listItem?: "bullet" | "number"; 1151 | markDefs?: Array<{ 1152 | href?: string; 1153 | _type: "link"; 1154 | _key: string; 1155 | }>; 1156 | level?: number; 1157 | _type: "block"; 1158 | _key: string; 1159 | }>; 1160 | logo?: { 1161 | asset?: { 1162 | _ref: string; 1163 | _type: "reference"; 1164 | _weak?: boolean; 1165 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1166 | }; 1167 | hotspot?: SanityImageHotspot; 1168 | crop?: SanityImageCrop; 1169 | _type: "image"; 1170 | }; 1171 | coverImage?: { 1172 | asset?: { 1173 | _ref: string; 1174 | _type: "reference"; 1175 | _weak?: boolean; 1176 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1177 | }; 1178 | hotspot?: SanityImageHotspot; 1179 | crop?: SanityImageCrop; 1180 | _type: "image"; 1181 | }; 1182 | guides: Array<{ 1183 | _id: string; 1184 | _type: "guide"; 1185 | _createdAt: string; 1186 | _updatedAt: string; 1187 | _rev: string; 1188 | name?: string; 1189 | slug: string | null; 1190 | excerpt?: string; 1191 | link?: string; 1192 | order?: number; 1193 | date?: string; 1194 | }> | null; 1195 | date: string; 1196 | status: "draft" | "published"; 1197 | }>; 1198 | // Variable: productListOfRecentQuery 1199 | // Query: *[_type == "product" && visible == true] | order(_createdAt desc) [0...$limit] { ..., "slug": slug.current, "status": select(_originalId in path("drafts.**") => "draft", "published"), "desc": coalesce(desc[$lang], desc[$defaultLocale]), "date": coalesce(date, _createdAt), category-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, }, tags[]-> { ..., "slug": slug.current, }, guides[]-> { ..., "slug": slug.current, }, submitter-> { ... },} 1200 | export type ProductListOfRecentQueryResult = Array<{ 1201 | _id: string; 1202 | _type: "product"; 1203 | _createdAt: string; 1204 | _updatedAt: string; 1205 | _rev: string; 1206 | name?: string; 1207 | slug: string | null; 1208 | order?: number; 1209 | category: { 1210 | _id: string; 1211 | _type: "category"; 1212 | _createdAt: string; 1213 | _updatedAt: string; 1214 | _rev: string; 1215 | name: string; 1216 | slug: string | null; 1217 | group: { 1218 | _id: string; 1219 | _type: "group"; 1220 | _createdAt: string; 1221 | _updatedAt: string; 1222 | _rev: string; 1223 | name: string; 1224 | slug: string | null; 1225 | order?: number; 1226 | date?: string; 1227 | } | null; 1228 | order?: number; 1229 | date?: string; 1230 | } | null; 1231 | tags: Array<{ 1232 | _id: string; 1233 | _type: "tag"; 1234 | _createdAt: string; 1235 | _updatedAt: string; 1236 | _rev: string; 1237 | name?: string; 1238 | slug: string | null; 1239 | order?: number; 1240 | date?: string; 1241 | }> | null; 1242 | featured?: boolean; 1243 | visible?: boolean; 1244 | website?: string; 1245 | github?: string; 1246 | priceLink?: string; 1247 | price?: "Free" | "Free & Paid" | "Paid"; 1248 | source?: string; 1249 | submitter: { 1250 | _id: string; 1251 | _type: "user"; 1252 | _createdAt: string; 1253 | _updatedAt: string; 1254 | _rev: string; 1255 | name?: string; 1256 | id?: string; 1257 | email?: string; 1258 | avatar?: string; 1259 | link?: string; 1260 | date?: string; 1261 | } | null; 1262 | desc: string; 1263 | content?: Array<({ 1264 | _key: string; 1265 | } & Code) | { 1266 | asset?: { 1267 | _ref: string; 1268 | _type: "reference"; 1269 | _weak?: boolean; 1270 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1271 | }; 1272 | hotspot?: SanityImageHotspot; 1273 | crop?: SanityImageCrop; 1274 | _type: "image"; 1275 | _key: string; 1276 | } | { 1277 | children?: Array<{ 1278 | marks?: Array; 1279 | text?: string; 1280 | _type: "span"; 1281 | _key: string; 1282 | }>; 1283 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 1284 | listItem?: "bullet" | "number"; 1285 | markDefs?: Array<{ 1286 | href?: string; 1287 | _type: "link"; 1288 | _key: string; 1289 | }>; 1290 | level?: number; 1291 | _type: "block"; 1292 | _key: string; 1293 | }>; 1294 | logo?: { 1295 | asset?: { 1296 | _ref: string; 1297 | _type: "reference"; 1298 | _weak?: boolean; 1299 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1300 | }; 1301 | hotspot?: SanityImageHotspot; 1302 | crop?: SanityImageCrop; 1303 | _type: "image"; 1304 | }; 1305 | coverImage?: { 1306 | asset?: { 1307 | _ref: string; 1308 | _type: "reference"; 1309 | _weak?: boolean; 1310 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1311 | }; 1312 | hotspot?: SanityImageHotspot; 1313 | crop?: SanityImageCrop; 1314 | _type: "image"; 1315 | }; 1316 | guides: Array<{ 1317 | _id: string; 1318 | _type: "guide"; 1319 | _createdAt: string; 1320 | _updatedAt: string; 1321 | _rev: string; 1322 | name?: string; 1323 | slug: string | null; 1324 | excerpt?: string; 1325 | link?: string; 1326 | order?: number; 1327 | date?: string; 1328 | }> | null; 1329 | date: string; 1330 | status: "draft" | "published"; 1331 | }>; 1332 | // Variable: productQuery 1333 | // Query: *[_type == "product" && visible == true && slug.current == $slug] [0] { content, ..., "slug": slug.current, "status": select(_originalId in path("drafts.**") => "draft", "published"), "desc": coalesce(desc[$lang], desc[$defaultLocale]), "date": coalesce(date, _createdAt), category-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), group-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, }, tags[]-> { ..., "slug": slug.current, }, guides[]-> { ..., "slug": slug.current, }, submitter-> { ... },} 1334 | export type ProductQueryResult = { 1335 | content?: Array<({ 1336 | _key: string; 1337 | } & Code) | { 1338 | asset?: { 1339 | _ref: string; 1340 | _type: "reference"; 1341 | _weak?: boolean; 1342 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1343 | }; 1344 | hotspot?: SanityImageHotspot; 1345 | crop?: SanityImageCrop; 1346 | _type: "image"; 1347 | _key: string; 1348 | } | { 1349 | children?: Array<{ 1350 | marks?: Array; 1351 | text?: string; 1352 | _type: "span"; 1353 | _key: string; 1354 | }>; 1355 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 1356 | listItem?: "bullet" | "number"; 1357 | markDefs?: Array<{ 1358 | href?: string; 1359 | _type: "link"; 1360 | _key: string; 1361 | }>; 1362 | level?: number; 1363 | _type: "block"; 1364 | _key: string; 1365 | }>; 1366 | _id: string; 1367 | _type: "product"; 1368 | _createdAt: string; 1369 | _updatedAt: string; 1370 | _rev: string; 1371 | name?: string; 1372 | slug: string | null; 1373 | order?: number; 1374 | category: { 1375 | _id: string; 1376 | _type: "category"; 1377 | _createdAt: string; 1378 | _updatedAt: string; 1379 | _rev: string; 1380 | name: string; 1381 | slug: string | null; 1382 | group: { 1383 | _id: string; 1384 | _type: "group"; 1385 | _createdAt: string; 1386 | _updatedAt: string; 1387 | _rev: string; 1388 | name: string; 1389 | slug: string | null; 1390 | order?: number; 1391 | date?: string; 1392 | } | null; 1393 | order?: number; 1394 | date?: string; 1395 | } | null; 1396 | tags: Array<{ 1397 | _id: string; 1398 | _type: "tag"; 1399 | _createdAt: string; 1400 | _updatedAt: string; 1401 | _rev: string; 1402 | name?: string; 1403 | slug: string | null; 1404 | order?: number; 1405 | date?: string; 1406 | }> | null; 1407 | featured?: boolean; 1408 | visible?: boolean; 1409 | website?: string; 1410 | github?: string; 1411 | priceLink?: string; 1412 | price?: "Free" | "Free & Paid" | "Paid"; 1413 | source?: string; 1414 | submitter: { 1415 | _id: string; 1416 | _type: "user"; 1417 | _createdAt: string; 1418 | _updatedAt: string; 1419 | _rev: string; 1420 | name?: string; 1421 | id?: string; 1422 | email?: string; 1423 | avatar?: string; 1424 | link?: string; 1425 | date?: string; 1426 | } | null; 1427 | desc: string; 1428 | logo?: { 1429 | asset?: { 1430 | _ref: string; 1431 | _type: "reference"; 1432 | _weak?: boolean; 1433 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1434 | }; 1435 | hotspot?: SanityImageHotspot; 1436 | crop?: SanityImageCrop; 1437 | _type: "image"; 1438 | }; 1439 | coverImage?: { 1440 | asset?: { 1441 | _ref: string; 1442 | _type: "reference"; 1443 | _weak?: boolean; 1444 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1445 | }; 1446 | hotspot?: SanityImageHotspot; 1447 | crop?: SanityImageCrop; 1448 | _type: "image"; 1449 | }; 1450 | guides: Array<{ 1451 | _id: string; 1452 | _type: "guide"; 1453 | _createdAt: string; 1454 | _updatedAt: string; 1455 | _rev: string; 1456 | name?: string; 1457 | slug: string | null; 1458 | excerpt?: string; 1459 | link?: string; 1460 | order?: number; 1461 | date?: string; 1462 | }> | null; 1463 | date: string; 1464 | status: "draft" | "published"; 1465 | } | null; 1466 | // Variable: appQuery 1467 | // Query: *[_type == "application" && name == $slug] [0] { ..., types[]-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, user-> { ... },} 1468 | export type AppQueryResult = { 1469 | _id: string; 1470 | _type: "application"; 1471 | _createdAt: string; 1472 | _updatedAt: string; 1473 | _rev: string; 1474 | name?: string; 1475 | description?: string; 1476 | link?: string; 1477 | types: Array<{ 1478 | _id: string; 1479 | _type: "appType"; 1480 | _createdAt: string; 1481 | _updatedAt: string; 1482 | _rev: string; 1483 | name: string; 1484 | slug: string | null; 1485 | order?: number; 1486 | date?: string; 1487 | }> | null; 1488 | featured?: boolean; 1489 | status?: "approved" | "rejected" | "reviewing"; 1490 | reason?: "rejected: only support self-built indie app" | "rejected: please upload a better cover image" | "rejected: please upload a better logo image" | "rejected: this indie app seems not ready?"; 1491 | image?: { 1492 | asset?: { 1493 | _ref: string; 1494 | _type: "reference"; 1495 | _weak?: boolean; 1496 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1497 | }; 1498 | hotspot?: SanityImageHotspot; 1499 | crop?: SanityImageCrop; 1500 | _type: "image"; 1501 | }; 1502 | cover?: { 1503 | asset?: { 1504 | _ref: string; 1505 | _type: "reference"; 1506 | _weak?: boolean; 1507 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1508 | }; 1509 | hotspot?: SanityImageHotspot; 1510 | crop?: SanityImageCrop; 1511 | _type: "image"; 1512 | }; 1513 | user: { 1514 | _id: string; 1515 | _type: "user"; 1516 | _createdAt: string; 1517 | _updatedAt: string; 1518 | _rev: string; 1519 | name?: string; 1520 | id?: string; 1521 | email?: string; 1522 | avatar?: string; 1523 | link?: string; 1524 | date?: string; 1525 | } | null; 1526 | date?: string; 1527 | } | null; 1528 | // Variable: appTypeListQuery 1529 | // Query: *[_type == "appType"] | order(order desc, _createdAt asc) { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), } 1530 | export type AppTypeListQueryResult = Array<{ 1531 | _id: string; 1532 | _type: "appType"; 1533 | _createdAt: string; 1534 | _updatedAt: string; 1535 | _rev: string; 1536 | name: string; 1537 | slug: string | null; 1538 | order?: number; 1539 | date?: string; 1540 | }>; 1541 | // Variable: applicationListOfFeaturedQuery 1542 | // Query: *[_type == "application" && status == "approved" && featured == true] | order(order desc, _createdAt asc) { ..., types[]-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, user-> { ... },} 1543 | export type ApplicationListOfFeaturedQueryResult = Array<{ 1544 | _id: string; 1545 | _type: "application"; 1546 | _createdAt: string; 1547 | _updatedAt: string; 1548 | _rev: string; 1549 | name?: string; 1550 | description?: string; 1551 | link?: string; 1552 | types: Array<{ 1553 | _id: string; 1554 | _type: "appType"; 1555 | _createdAt: string; 1556 | _updatedAt: string; 1557 | _rev: string; 1558 | name: string; 1559 | slug: string | null; 1560 | order?: number; 1561 | date?: string; 1562 | }> | null; 1563 | featured?: boolean; 1564 | status?: "approved" | "rejected" | "reviewing"; 1565 | reason?: "rejected: only support self-built indie app" | "rejected: please upload a better cover image" | "rejected: please upload a better logo image" | "rejected: this indie app seems not ready?"; 1566 | image?: { 1567 | asset?: { 1568 | _ref: string; 1569 | _type: "reference"; 1570 | _weak?: boolean; 1571 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1572 | }; 1573 | hotspot?: SanityImageHotspot; 1574 | crop?: SanityImageCrop; 1575 | _type: "image"; 1576 | }; 1577 | cover?: { 1578 | asset?: { 1579 | _ref: string; 1580 | _type: "reference"; 1581 | _weak?: boolean; 1582 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1583 | }; 1584 | hotspot?: SanityImageHotspot; 1585 | crop?: SanityImageCrop; 1586 | _type: "image"; 1587 | }; 1588 | user: { 1589 | _id: string; 1590 | _type: "user"; 1591 | _createdAt: string; 1592 | _updatedAt: string; 1593 | _rev: string; 1594 | name?: string; 1595 | id?: string; 1596 | email?: string; 1597 | avatar?: string; 1598 | link?: string; 1599 | date?: string; 1600 | } | null; 1601 | date?: string; 1602 | }>; 1603 | // Variable: applicationListOfRecentQuery 1604 | // Query: *[_type == "application" && status == "approved"] | order(_createdAt desc) [0...$limit] { ..., types[]-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, user-> { ... },} 1605 | export type ApplicationListOfRecentQueryResult = Array<{ 1606 | _id: string; 1607 | _type: "application"; 1608 | _createdAt: string; 1609 | _updatedAt: string; 1610 | _rev: string; 1611 | name?: string; 1612 | description?: string; 1613 | link?: string; 1614 | types: Array<{ 1615 | _id: string; 1616 | _type: "appType"; 1617 | _createdAt: string; 1618 | _updatedAt: string; 1619 | _rev: string; 1620 | name: string; 1621 | slug: string | null; 1622 | order?: number; 1623 | date?: string; 1624 | }> | null; 1625 | featured?: boolean; 1626 | status?: "approved" | "rejected" | "reviewing"; 1627 | reason?: "rejected: only support self-built indie app" | "rejected: please upload a better cover image" | "rejected: please upload a better logo image" | "rejected: this indie app seems not ready?"; 1628 | image?: { 1629 | asset?: { 1630 | _ref: string; 1631 | _type: "reference"; 1632 | _weak?: boolean; 1633 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1634 | }; 1635 | hotspot?: SanityImageHotspot; 1636 | crop?: SanityImageCrop; 1637 | _type: "image"; 1638 | }; 1639 | cover?: { 1640 | asset?: { 1641 | _ref: string; 1642 | _type: "reference"; 1643 | _weak?: boolean; 1644 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1645 | }; 1646 | hotspot?: SanityImageHotspot; 1647 | crop?: SanityImageCrop; 1648 | _type: "image"; 1649 | }; 1650 | user: { 1651 | _id: string; 1652 | _type: "user"; 1653 | _createdAt: string; 1654 | _updatedAt: string; 1655 | _rev: string; 1656 | name?: string; 1657 | id?: string; 1658 | email?: string; 1659 | avatar?: string; 1660 | link?: string; 1661 | date?: string; 1662 | } | null; 1663 | date?: string; 1664 | }>; 1665 | // Variable: applicationListByCategoryQuery 1666 | // Query: *[_type == "application" && status == "approved" && references(*[_type == "appType" && slug.current == $categorySlug]._id)] | order(order desc, _createdAt asc) { ..., types[]-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, user-> { ... },} 1667 | export type ApplicationListByCategoryQueryResult = Array<{ 1668 | _id: string; 1669 | _type: "application"; 1670 | _createdAt: string; 1671 | _updatedAt: string; 1672 | _rev: string; 1673 | name?: string; 1674 | description?: string; 1675 | link?: string; 1676 | types: Array<{ 1677 | _id: string; 1678 | _type: "appType"; 1679 | _createdAt: string; 1680 | _updatedAt: string; 1681 | _rev: string; 1682 | name: string; 1683 | slug: string | null; 1684 | order?: number; 1685 | date?: string; 1686 | }> | null; 1687 | featured?: boolean; 1688 | status?: "approved" | "rejected" | "reviewing"; 1689 | reason?: "rejected: only support self-built indie app" | "rejected: please upload a better cover image" | "rejected: please upload a better logo image" | "rejected: this indie app seems not ready?"; 1690 | image?: { 1691 | asset?: { 1692 | _ref: string; 1693 | _type: "reference"; 1694 | _weak?: boolean; 1695 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1696 | }; 1697 | hotspot?: SanityImageHotspot; 1698 | crop?: SanityImageCrop; 1699 | _type: "image"; 1700 | }; 1701 | cover?: { 1702 | asset?: { 1703 | _ref: string; 1704 | _type: "reference"; 1705 | _weak?: boolean; 1706 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1707 | }; 1708 | hotspot?: SanityImageHotspot; 1709 | crop?: SanityImageCrop; 1710 | _type: "image"; 1711 | }; 1712 | user: { 1713 | _id: string; 1714 | _type: "user"; 1715 | _createdAt: string; 1716 | _updatedAt: string; 1717 | _rev: string; 1718 | name?: string; 1719 | id?: string; 1720 | email?: string; 1721 | avatar?: string; 1722 | link?: string; 1723 | date?: string; 1724 | } | null; 1725 | date?: string; 1726 | }>; 1727 | // Variable: applicationListByUserQuery 1728 | // Query: *[_type == "application" && references(*[_type == "user" && id == $userid]._id)] | order(_createdAt asc) { ..., types[]-> { ..., "slug": slug.current, "name": coalesce(name[$lang], name[$defaultLocale]), }, user-> { ... },} 1729 | export type ApplicationListByUserQueryResult = Array<{ 1730 | _id: string; 1731 | _type: "application"; 1732 | _createdAt: string; 1733 | _updatedAt: string; 1734 | _rev: string; 1735 | name?: string; 1736 | description?: string; 1737 | link?: string; 1738 | types: Array<{ 1739 | _id: string; 1740 | _type: "appType"; 1741 | _createdAt: string; 1742 | _updatedAt: string; 1743 | _rev: string; 1744 | name: string; 1745 | slug: string | null; 1746 | order?: number; 1747 | date?: string; 1748 | }> | null; 1749 | featured?: boolean; 1750 | status?: "approved" | "rejected" | "reviewing"; 1751 | reason?: "rejected: only support self-built indie app" | "rejected: please upload a better cover image" | "rejected: please upload a better logo image" | "rejected: this indie app seems not ready?"; 1752 | image?: { 1753 | asset?: { 1754 | _ref: string; 1755 | _type: "reference"; 1756 | _weak?: boolean; 1757 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1758 | }; 1759 | hotspot?: SanityImageHotspot; 1760 | crop?: SanityImageCrop; 1761 | _type: "image"; 1762 | }; 1763 | cover?: { 1764 | asset?: { 1765 | _ref: string; 1766 | _type: "reference"; 1767 | _weak?: boolean; 1768 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1769 | }; 1770 | hotspot?: SanityImageHotspot; 1771 | crop?: SanityImageCrop; 1772 | _type: "image"; 1773 | }; 1774 | user: { 1775 | _id: string; 1776 | _type: "user"; 1777 | _createdAt: string; 1778 | _updatedAt: string; 1779 | _rev: string; 1780 | name?: string; 1781 | id?: string; 1782 | email?: string; 1783 | avatar?: string; 1784 | link?: string; 1785 | date?: string; 1786 | } | null; 1787 | date?: string; 1788 | }>; 1789 | // Variable: userQuery 1790 | // Query: *[_type == "user" && id == $userId][0] { ...} 1791 | export type UserQueryResult = { 1792 | _id: string; 1793 | _type: "user"; 1794 | _createdAt: string; 1795 | _updatedAt: string; 1796 | _rev: string; 1797 | name?: string; 1798 | id?: string; 1799 | email?: string; 1800 | avatar?: string; 1801 | link?: string; 1802 | date?: string; 1803 | } | null; 1804 | // Variable: moreStoriesQuery 1805 | // Query: *[_type == "post" && _id != $skip && defined(slug.current)] | order(date desc, _updatedAt desc) [0...$limit] { _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture},} 1806 | export type MoreStoriesQueryResult = Array<{ 1807 | _id: string; 1808 | status: "draft" | "published"; 1809 | title: string | "Untitled"; 1810 | slug: string | null; 1811 | excerpt: string | null; 1812 | coverImage: { 1813 | asset?: { 1814 | _ref: string; 1815 | _type: "reference"; 1816 | _weak?: boolean; 1817 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1818 | }; 1819 | hotspot?: SanityImageHotspot; 1820 | crop?: SanityImageCrop; 1821 | alt?: string; 1822 | _type: "image"; 1823 | } | null; 1824 | date: string; 1825 | author: { 1826 | name: string | "Anonymous"; 1827 | picture: { 1828 | asset?: { 1829 | _ref: string; 1830 | _type: "reference"; 1831 | _weak?: boolean; 1832 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1833 | }; 1834 | hotspot?: SanityImageHotspot; 1835 | crop?: SanityImageCrop; 1836 | alt?: string; 1837 | _type: "image"; 1838 | } | null; 1839 | } | null; 1840 | }>; 1841 | // Variable: postQuery 1842 | // Query: *[_type == "post" && slug.current == $slug] [0] { content, _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture},} 1843 | export type PostQueryResult = { 1844 | content: Array<({ 1845 | _key: string; 1846 | } & Code) | { 1847 | asset?: { 1848 | _ref: string; 1849 | _type: "reference"; 1850 | _weak?: boolean; 1851 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1852 | }; 1853 | hotspot?: SanityImageHotspot; 1854 | crop?: SanityImageCrop; 1855 | _type: "image"; 1856 | _key: string; 1857 | } | { 1858 | children?: Array<{ 1859 | marks?: Array; 1860 | text?: string; 1861 | _type: "span"; 1862 | _key: string; 1863 | }>; 1864 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 1865 | listItem?: "bullet" | "number"; 1866 | markDefs?: Array<{ 1867 | href?: string; 1868 | _type: "link"; 1869 | _key: string; 1870 | }>; 1871 | level?: number; 1872 | _type: "block"; 1873 | _key: string; 1874 | }> | null; 1875 | _id: string; 1876 | status: "draft" | "published"; 1877 | title: string | "Untitled"; 1878 | slug: string | null; 1879 | excerpt: string | null; 1880 | coverImage: { 1881 | asset?: { 1882 | _ref: string; 1883 | _type: "reference"; 1884 | _weak?: boolean; 1885 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1886 | }; 1887 | hotspot?: SanityImageHotspot; 1888 | crop?: SanityImageCrop; 1889 | alt?: string; 1890 | _type: "image"; 1891 | } | null; 1892 | date: string; 1893 | author: { 1894 | name: string | "Anonymous"; 1895 | picture: { 1896 | asset?: { 1897 | _ref: string; 1898 | _type: "reference"; 1899 | _weak?: boolean; 1900 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1901 | }; 1902 | hotspot?: SanityImageHotspot; 1903 | crop?: SanityImageCrop; 1904 | alt?: string; 1905 | _type: "image"; 1906 | } | null; 1907 | } | null; 1908 | } | null; 1909 | // Variable: settingsQuery 1910 | // Query: *[_type == "settings"][0] 1911 | export type SettingsQueryResult = { 1912 | _id: string; 1913 | _type: "settings"; 1914 | _createdAt: string; 1915 | _updatedAt: string; 1916 | _rev: string; 1917 | title?: string; 1918 | subtitle?: string; 1919 | description?: Array<{ 1920 | children?: Array<{ 1921 | marks?: Array; 1922 | text?: string; 1923 | _type: "span"; 1924 | _key: string; 1925 | }>; 1926 | style?: "normal"; 1927 | listItem?: never; 1928 | markDefs?: Array<{ 1929 | href?: string; 1930 | _type: "link"; 1931 | _key: string; 1932 | }>; 1933 | level?: number; 1934 | _type: "block"; 1935 | _key: string; 1936 | }>; 1937 | footer?: Array<{ 1938 | children?: Array<{ 1939 | marks?: Array; 1940 | text?: string; 1941 | _type: "span"; 1942 | _key: string; 1943 | }>; 1944 | style?: "blockquote" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "normal"; 1945 | listItem?: "bullet" | "number"; 1946 | markDefs?: Array<{ 1947 | href?: string; 1948 | _type: "link"; 1949 | _key: string; 1950 | }>; 1951 | level?: number; 1952 | _type: "block"; 1953 | _key: string; 1954 | }>; 1955 | ogImage?: { 1956 | asset?: { 1957 | _ref: string; 1958 | _type: "reference"; 1959 | _weak?: boolean; 1960 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1961 | }; 1962 | hotspot?: SanityImageHotspot; 1963 | crop?: SanityImageCrop; 1964 | alt?: string; 1965 | metadataBase?: string; 1966 | _type: "image"; 1967 | }; 1968 | } | null; 1969 | // Variable: heroQuery 1970 | // Query: *[_type == "post" && defined(slug.current)] | order(date desc, _updatedAt desc) [0] { _id, "status": select(_originalId in path("drafts.**") => "draft", "published"), "title": coalesce(title, "Untitled"), "slug": slug.current, excerpt, coverImage, "date": coalesce(date, _updatedAt), "author": author->{"name": coalesce(name, "Anonymous"), picture},} 1971 | export type HeroQueryResult = { 1972 | _id: string; 1973 | status: "draft" | "published"; 1974 | title: string | "Untitled"; 1975 | slug: string | null; 1976 | excerpt: string | null; 1977 | coverImage: { 1978 | asset?: { 1979 | _ref: string; 1980 | _type: "reference"; 1981 | _weak?: boolean; 1982 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1983 | }; 1984 | hotspot?: SanityImageHotspot; 1985 | crop?: SanityImageCrop; 1986 | alt?: string; 1987 | _type: "image"; 1988 | } | null; 1989 | date: string; 1990 | author: { 1991 | name: string | "Anonymous"; 1992 | picture: { 1993 | asset?: { 1994 | _ref: string; 1995 | _type: "reference"; 1996 | _weak?: boolean; 1997 | [internalGroqTypeReferenceTo]?: "sanity.imageAsset"; 1998 | }; 1999 | hotspot?: SanityImageHotspot; 2000 | crop?: SanityImageCrop; 2001 | alt?: string; 2002 | _type: "image"; 2003 | } | null; 2004 | } | null; 2005 | } | null; 2006 | 2007 | // Source: app/(blog)/posts/[slug]/page.tsx 2008 | // Variable: postSlugs 2009 | // Query: *[_type == "post"]{slug} 2010 | export type PostSlugsResult = Array<{ 2011 | slug: Slug | null; 2012 | }>; 2013 | 2014 | --------------------------------------------------------------------------------