├── .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 |
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 |
29 |
30 | ) : (
31 |
By
32 | )}
33 |
{name}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/(sanity)/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
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 |
3 | Open Source Directory Boilerplate
4 |
5 |
6 |
7 |
8 |
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 | [](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 |
--------------------------------------------------------------------------------