├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── (admin)
│ ├── head.tsx
│ ├── layout.tsx
│ └── studio
│ │ └── [[...index]]
│ │ ├── head.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
├── (user)
│ ├── layout.tsx
│ ├── page.tsx
│ └── post
│ │ └── [slug]
│ │ └── page.tsx
└── head.tsx
├── components
├── Banner.tsx
├── BlogList.tsx
├── ClientSideRoute.tsx
├── DarkModeButton.tsx
├── Footer.tsx
├── Header.tsx
├── Logo.tsx
├── PostCard.tsx
├── PreviewBlogList.tsx
├── PreviewSuspense.tsx
├── Providers.tsx
├── RichTextComponents.tsx
├── ScrollToTop.tsx
└── StudioNavbar.tsx
├── lib
├── sanity.client.ts
├── sanity.preview.ts
└── urlFor.ts
├── next.config.js
├── package.json
├── pages
└── api
│ ├── exit-preview.ts
│ └── preview.ts
├── postcss.config.js
├── public
├── cover.png
├── favicon.ico
└── vercel.svg
├── sanity.cli.ts
├── sanity.config.ts
├── schemas
├── author.ts
├── blockContent.ts
├── category.ts
├── index.ts
└── post.ts
├── structure.ts
├── styles
└── globals.css
├── tailwind.config.js
├── theme.ts
├── tsconfig.json
├── typings.d.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NextJS-Sanity-Blog
2 |
3 | For this project I built a Blog with Next.js 13 (Sanity v3, TypeScript, Tailwind CSS, Auth, CMS, Preview Mode) and Vercel for deployment.
4 |
5 | ## Screen
6 |
7 | 
8 |
9 | ## Link
10 |
11 | - [NextJS Blog](https://adperformance-blog.vercel.app/)
12 |
13 | ### Next.js + Tailwind CSS Example
14 |
15 | This example shows how to use [Tailwind CSS](https://tailwindcss.com/) [(v3.2)](https://tailwindcss.com/blog/tailwindcss-v3-2) with Next.js. It follows the steps outlined in the official [Tailwind docs](https://tailwindcss.com/docs/guides/nextjs).
16 |
17 | ### Deploy your own
18 |
19 | Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-tailwindcss)
20 |
21 | [](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss&project-name=with-tailwindcss&repository-name=with-tailwindcss)
22 |
23 | ### How to use
24 |
25 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example:
26 |
27 | ```bash
28 | npx create-next-app --example with-tailwindcss with-tailwindcss-app
29 | ```
30 |
31 | ```bash
32 | yarn create next-app --example with-tailwindcss with-tailwindcss-app
33 | ```
34 |
35 | ```bash
36 | pnpm create next-app --example with-tailwindcss with-tailwindcss-app
37 | ```
38 |
39 | Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
40 |
--------------------------------------------------------------------------------
/app/(admin)/head.tsx:
--------------------------------------------------------------------------------
1 | export default function Head() {
2 | return (
3 | <>
4 |
5 |
6 |
7 | >
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/app/(admin)/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../../styles/globals.css";
2 |
3 | export default function RootLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/app/(admin)/studio/[[...index]]/head.tsx:
--------------------------------------------------------------------------------
1 | // Re-export `NextStudioHead` as default if you're happy with the default behavior
2 | export { NextStudioHead } from "next-sanity/studio/head";
3 |
4 | // To customize it, use it as a children component:
5 | import { NextStudioHead } from "next-sanity/studio/head";
6 |
7 | export default function CustomStudioHead() {
8 | return (
9 | <>
10 |
11 | CMS
12 |
18 | >
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/(admin)/studio/[[...index]]/loading.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import config from "../../../../sanity.config";
4 | import NextStudioLoading from "next-sanity/studio/loading";
5 |
6 | export default function Loading() {
7 | return ;
8 | }
9 |
--------------------------------------------------------------------------------
/app/(admin)/studio/[[...index]]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { NextStudio } from "next-sanity/studio";
4 |
5 | import config from "../../../../sanity.config";
6 |
7 | export default function StudioPage() {
8 | // Supports the same props as `import {Studio} from 'sanity'`, `config` is required
9 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/app/(user)/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname } from "next/navigation";
4 | import Banner from "../../components/Banner";
5 | import Footer from "../../components/Footer";
6 | import Header from "../../components/Header";
7 | import ScrollToTop from "../../components/ScrollToTop";
8 |
9 | import Providers from "../../components/Providers";
10 |
11 | import "../../styles/globals.css";
12 |
13 | export default function RootLayout({
14 | children,
15 | }: {
16 | children: React.ReactNode;
17 | }) {
18 | const router = usePathname();
19 | const hideBanner = router?.startsWith("/post/") ? false : true;
20 | return (
21 |
22 |
23 |
24 |
25 |
26 | {hideBanner && }
27 | {children}
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/(user)/page.tsx:
--------------------------------------------------------------------------------
1 | import { previewData } from "next/headers";
2 | import { groq } from "next-sanity";
3 | import { client } from "../../lib/sanity.client";
4 | import PreviewSuspense from "../../components/PreviewSuspense";
5 | import BlogList from "../../components/BlogList";
6 | import PreviewBlogList from "../../components/PreviewBlogList";
7 |
8 | const query = groq`
9 | *[_type == "post"] {
10 | ...,
11 | author->,
12 | categories[]->
13 | } | order(_createdAt desc)
14 | `;
15 |
16 | export const revalidate = 60; // revalidate this page every 60 seconds
17 |
18 | export default async function HomePage() {
19 | if (previewData()) {
20 | return (
21 |
24 |
25 | Loading Preview Data...
26 |
27 |
28 | }
29 | >
30 |
31 |
32 | );
33 | }
34 |
35 | const posts = await client.fetch(query);
36 |
37 | return ;
38 | }
39 |
--------------------------------------------------------------------------------
/app/(user)/post/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { groq } from "next-sanity";
2 | import Image from "next/image";
3 | import { client } from "../../../../lib/sanity.client";
4 | import urlFor from "../../../../lib/urlFor";
5 | import { PortableText } from "@portabletext/react";
6 | import { RichTextComponents } from "../../../../components/RichTextComponents";
7 |
8 | type Props = {
9 | params: {
10 | slug: string;
11 | };
12 | };
13 |
14 | export const revalidate = 60;
15 |
16 | export async function generateStaticParams() {
17 | const query = groq`
18 | *[_type == "post"]
19 | {
20 | slug
21 | }
22 | `;
23 |
24 | const slugs: Post[] = await client.fetch(query);
25 | const slugRoutes = slugs.map((slug) => slug.slug.current);
26 |
27 | return slugRoutes.map((slug) => ({
28 | slug,
29 | }));
30 | }
31 |
32 | async function Post({ params: { slug } }: Props) {
33 | const query = groq`
34 | *[_type == "post" && slug.current == $slug][0] {
35 | ...,
36 | author->,
37 | categories[]->,
38 | }
39 | `;
40 |
41 | const post: Post = await client.fetch(query, { slug });
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
63 | Go back
64 |
65 |
66 |
70 |
71 |
72 |
73 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
{post.title}
87 |
88 | {new Date(post._createdAt).toLocaleDateString("en-US", {
89 | day: "numeric",
90 | month: "long",
91 | year: "numeric",
92 | })}
93 |
94 |
95 |
96 |
103 |
104 |
105 |
{post.author.name}
106 |
107 |
108 |
109 |
110 |
111 |
{post.description}
112 |
113 | {post.categories.map((category) => (
114 |
118 | {category.title}
119 |
120 | ))}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
131 | export default Post;
132 |
--------------------------------------------------------------------------------
/app/head.tsx:
--------------------------------------------------------------------------------
1 | export default function Head() {
2 | return (
3 | <>
4 | AD Blog | Athlete Development
5 |
6 |
7 | >
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/components/Banner.tsx:
--------------------------------------------------------------------------------
1 | const Banner = () => {
2 | return (
3 |
7 |
8 |
AD Performance Blog
9 |
13 | Welcome to{" "}
14 |
15 | the athlete
16 |
17 | {""} blog, from the field to the blog.
18 |
19 |
20 |
21 | The{" "}
22 |
23 | {" "}
24 | ultimate resource
25 | {" "}
26 | for athlete training, development and nutrition.
27 |
28 |
29 | );
30 | };
31 |
32 | export default Banner;
33 |
--------------------------------------------------------------------------------
/components/BlogList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 | import ClientSideRoute from "./ClientSideRoute";
4 | import PostCard from "./PostCard";
5 |
6 | type Props = {
7 | posts: Post[];
8 | };
9 |
10 | const BlogList = ({ posts }: Props) => {
11 | const articlesShown = 4;
12 | const [loadMore, setLoadMore] = useState(articlesShown);
13 | const showMoreArticles = () => {
14 | setLoadMore(loadMore + articlesShown);
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 | {posts.slice(0, loadMore)?.map((item) => (
22 | <>
23 |
27 |
28 |
29 | >
30 | ))}
31 |
32 |
33 | {loadMore < posts?.length ? (
34 |
44 | ) : (
45 |
53 | )}
54 |
55 |
56 | );
57 | };
58 |
59 | export default BlogList;
60 |
--------------------------------------------------------------------------------
/components/ClientSideRoute.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 |
5 | function ClientSideRoute({
6 | children,
7 | route,
8 | }: {
9 | children: React.ReactNode;
10 | route: string;
11 | }) {
12 | return {children};
13 | }
14 |
15 | export default ClientSideRoute;
16 |
--------------------------------------------------------------------------------
/components/DarkModeButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useTheme } from "next-themes";
3 | import { SunIcon, MoonIcon } from "@heroicons/react/24/solid";
4 |
5 | const DarkModeButton = () => {
6 | const [mount, setMount] = useState(false);
7 | const { systemTheme, theme, setTheme } = useTheme();
8 |
9 | useEffect(() => {
10 | setMount(true);
11 | }, []);
12 |
13 | if (!mount) return null;
14 |
15 | const currentTheme = theme === "system" ? systemTheme : theme;
16 |
17 | return (
18 |
19 | {currentTheme === "dark" ? (
20 | setTheme("light")}
23 | />
24 | ) : (
25 | setTheme("dark")}
28 | />
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default DarkModeButton;
35 |
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Footer = () => {
4 | return (
5 |
14 | );
15 | };
16 |
17 | export default Footer;
18 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 | import DarkModeButton from "./DarkModeButton";
4 |
5 | const Header = () => {
6 | return (
7 |
32 | );
33 | };
34 |
35 | export default Header;
36 |
--------------------------------------------------------------------------------
/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | const Logo = (props: any) => {
4 | const { renderDefault, title } = props;
5 |
6 | return (
7 |
8 |
15 | <>{renderDefault(props)}>
16 |
17 | );
18 | };
19 |
20 | export default Logo;
21 |
--------------------------------------------------------------------------------
/components/PostCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Image from "next/image";
3 | import urlFor from "../lib/urlFor";
4 | import { ArrowUpRightIcon } from "@heroicons/react/24/solid";
5 |
6 | interface Props {
7 | post: Post;
8 | }
9 |
10 | const PostCard = ({ post }: Props) => {
11 | return (
12 |
13 |
14 |
20 |
21 |
22 |
{post.title}
23 |
24 | {new Date(post._createdAt).toLocaleDateString("en-US", {
25 | day: "numeric",
26 | month: "long",
27 | year: "numeric",
28 | })}
29 |
30 |
31 |
32 |
33 | {post.categories.map((category) => (
34 |
35 |
{category.title}
36 |
37 | ))}
38 |
39 |
40 |
41 |
42 |
{post.title}
43 |
{post.description}
44 |
45 |
46 | Read Post
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default PostCard;
54 |
--------------------------------------------------------------------------------
/components/PreviewBlogList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePreview } from "../lib/sanity.preview";
4 | import BlogList from "./BlogList";
5 |
6 | type Props = {
7 | query: string;
8 | };
9 |
10 | export default function PreviewBlogList({ query }: Props) {
11 | const posts = usePreview(null, query);
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/components/PreviewSuspense.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export { PreviewSuspense as default } from "next-sanity/preview";
4 |
--------------------------------------------------------------------------------
/components/Providers.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "next-themes";
2 |
3 | const Provider = ({ children }: { children: React.ReactNode }) => {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | };
10 |
11 | export default Provider;
12 |
--------------------------------------------------------------------------------
/components/RichTextComponents.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import urlFor from "../lib/urlFor";
4 |
5 | export const RichTextComponents = {
6 | types: {
7 | image: ({ value }: any) => {
8 | return (
9 |
10 |
16 |
17 | );
18 | },
19 | },
20 | list: {
21 | bullet: ({ children }: any) => (
22 |
23 | ),
24 | number: ({ children }: any) => (
25 | {children}
26 | ),
27 | },
28 | block: {
29 | h1: ({ children }: any) => (
30 | {children}
31 | ),
32 | h2: ({ children }: any) => (
33 | {children}
34 | ),
35 | h3: ({ children }: any) => (
36 | {children}
37 | ),
38 | h4: ({ children }: any) => (
39 | {children}
40 | ),
41 |
42 | blockquote: ({ children }: any) => (
43 |
44 | {children}
45 |
46 | ),
47 | },
48 | marks: {
49 | link: ({ children, value }: any) => {
50 | const rel = !value.href.startsWith("/")
51 | ? "noopener noreferrer"
52 | : undefined;
53 |
54 | return (
55 |
60 | {children}
61 |
62 | );
63 | },
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/components/ScrollToTop.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { ArrowUpCircleIcon } from "@heroicons/react/24/solid";
3 |
4 | const ScrollToTop = () => {
5 | const [isVisible, setIsVisible] = useState(false);
6 |
7 | const toggleVisibility = () => {
8 | if (window.pageYOffset > 300) {
9 | setIsVisible(true);
10 | } else {
11 | setIsVisible(false);
12 | }
13 | };
14 |
15 | const scrollToTop = () => {
16 | window.scrollTo({
17 | top: 0,
18 | behavior: "smooth",
19 | });
20 | };
21 |
22 | useEffect(() => {
23 | window.addEventListener("scroll", toggleVisibility);
24 | return () => {
25 | window.removeEventListener("scroll", toggleVisibility);
26 | };
27 | }, []);
28 |
29 | return (
30 |
31 | {isVisible && (
32 |
36 | )}
37 |
38 | );
39 | };
40 |
41 | export default ScrollToTop;
42 |
--------------------------------------------------------------------------------
/components/StudioNavbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ArrowUturnLeftIcon } from "@heroicons/react/24/solid";
3 |
4 | const StudioNavbar = (props: any) => {
5 | return (
6 |
7 | <>
8 |
9 |
10 |
11 | Go To Website
12 |
13 |
14 |
15 |
Check out my portfolio
16 |
20 | www.alladin-daher.netlify.app
21 |
22 |
23 |
24 | {props.renderDefault(props)}
25 | >
26 |
27 | );
28 | };
29 |
30 | export default StudioNavbar;
31 |
--------------------------------------------------------------------------------
/lib/sanity.client.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "next-sanity";
2 |
3 | export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
4 | export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
5 | const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION;
6 |
7 | export const client = createClient({
8 | projectId,
9 | dataset,
10 | apiVersion,
11 | useCdn: false,
12 | });
13 |
--------------------------------------------------------------------------------
/lib/sanity.preview.ts:
--------------------------------------------------------------------------------
1 | import { definePreview } from "next-sanity/preview";
2 | import { projectId, dataset } from "./sanity.client";
3 |
4 | function onPublicAccessOnly() {
5 | throw new Error(`Unable to load preview as you're not logged in`);
6 | }
7 |
8 | if (!projectId || !dataset) {
9 | throw new Error(
10 | `Missing projectId or dataset. Check your sanity.json or .env`
11 | );
12 | }
13 |
14 | export const usePreview = definePreview({
15 | projectId,
16 | dataset,
17 | onPublicAccessOnly,
18 | });
19 |
--------------------------------------------------------------------------------
/lib/urlFor.ts:
--------------------------------------------------------------------------------
1 | import { client } from "./sanity.client";
2 | import imageUrlBuilder from "@sanity/image-url";
3 |
4 | // Get a pre-configured url-builder from your sanity client
5 | const builder = imageUrlBuilder(client);
6 |
7 | function urlFor(source: any) {
8 | return builder.image(source);
9 | }
10 |
11 | export default urlFor;
12 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | reactStrictMode: true,
4 | experimental: {
5 | appDir: true,
6 | },
7 | images: {
8 | domains: ["avatars.githubusercontent.com", "cdn.sanity.io"],
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "next dev",
5 | "build": "next build",
6 | "start": "next start"
7 | },
8 | "dependencies": {
9 | "@heroicons/react": "^2.0.13",
10 | "@portabletext/react": "^2.0.0",
11 | "@sanity/icons": "^2.1.0",
12 | "@sanity/image-url": "^1.0.1",
13 | "@sanity/ui": "^1.0.1",
14 | "@sanity/vision": "^3.0.6",
15 | "@tailwindcss/line-clamp": "^0.4.2",
16 | "next": "latest",
17 | "next-sanity": "^3.1.3",
18 | "next-themes": "^0.2.1",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "react-portable-text": "^0.5.1",
22 | "sanity": "^3.0.6",
23 | "sanity-plugin-iframe-pane": "^2.1.1",
24 | "styled-components": "^5.3.6"
25 | },
26 | "devDependencies": {
27 | "@types/node": "18.11.3",
28 | "autoprefixer": "^10.4.12",
29 | "postcss": "^8.4.18",
30 | "tailwindcss": "^3.2.1",
31 | "typescript": "4.8.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/pages/api/exit-preview.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function exit(req: NextApiRequest, res: NextApiResponse) {
4 | res.clearPreviewData();
5 | res.writeHead(307, { Location: "/" });
6 | res.end();
7 | }
8 |
--------------------------------------------------------------------------------
/pages/api/preview.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | export default function preview(req: NextApiRequest, res: NextApiResponse) {
4 | res.setPreviewData({});
5 | res.writeHead(307, { Location: "/" });
6 | res.end();
7 | }
8 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AD757/Nextjs-Sanity-Blog/0305da45fc9a454ae96ff086f7f6cb8961baa3a9/public/cover.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AD757/Nextjs-Sanity-Blog/0305da45fc9a454ae96ff086f7f6cb8961baa3a9/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sanity.cli.ts:
--------------------------------------------------------------------------------
1 | import { defineCliConfig } from "sanity/cli";
2 |
3 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!;
4 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!;
5 |
6 | export default defineCliConfig({
7 | api: {
8 | projectId,
9 | dataset,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/sanity.config.ts:
--------------------------------------------------------------------------------
1 | import { visionTool } from "@sanity/vision";
2 | import { defineConfig } from "sanity";
3 | import { deskTool } from "sanity/desk";
4 | import Logo from "./components/Logo";
5 | import StudioNavbar from "./components/StudioNavbar";
6 | import { schemaTypes } from "./schemas";
7 | import { getDefaultDocumentNode } from "./structure";
8 | import { myTheme } from "./theme";
9 |
10 | const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!;
11 | const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!;
12 |
13 | export default defineConfig({
14 | basePath: "/studio",
15 | name: 'AD_Content_Studio',
16 | title: 'AD Content Studio',
17 | projectId,
18 | dataset,
19 | plugins: [
20 | deskTool({
21 | defaultDocumentNode: getDefaultDocumentNode,
22 | }),
23 | visionTool(),
24 | ],
25 | schema: {
26 | types: schemaTypes,
27 | },
28 | icon: Logo,
29 | logo: Logo,
30 | subtitle: "Login to manage the Blog",
31 | studio: {
32 | components: {
33 | logo: Logo,
34 | navbar: StudioNavbar,
35 | },
36 | },
37 | theme: myTheme,
38 | });
39 |
--------------------------------------------------------------------------------
/schemas/author.ts:
--------------------------------------------------------------------------------
1 | import {defineField, defineType} from 'sanity'
2 |
3 | export default defineType({
4 | name: 'author',
5 | title: 'Author',
6 | type: 'document',
7 | fields: [
8 | defineField({
9 | name: 'name',
10 | title: 'Name',
11 | type: 'string',
12 | }),
13 | defineField({
14 | name: 'slug',
15 | title: 'Slug',
16 | type: 'slug',
17 | options: {
18 | source: 'name',
19 | maxLength: 96,
20 | },
21 | }),
22 | defineField({
23 | name: 'image',
24 | title: 'Image',
25 | type: 'image',
26 | options: {
27 | hotspot: true,
28 | },
29 | }),
30 | defineField({
31 | name: 'bio',
32 | title: 'Bio',
33 | type: 'array',
34 | of: [
35 | {
36 | title: 'Block',
37 | type: 'block',
38 | styles: [{title: 'Normal', value: 'normal'}],
39 | lists: [],
40 | },
41 | ],
42 | }),
43 | ],
44 | preview: {
45 | select: {
46 | title: 'name',
47 | media: 'image',
48 | },
49 | },
50 | })
51 |
--------------------------------------------------------------------------------
/schemas/blockContent.ts:
--------------------------------------------------------------------------------
1 | import {defineType, defineArrayMember} from 'sanity'
2 |
3 | /**
4 | * This is the schema definition for the rich text fields used for
5 | * for this blog studio. When you import it in schemas.js it can be
6 | * reused in other parts of the studio with:
7 | * {
8 | * name: 'someName',
9 | * title: 'Some title',
10 | * type: 'blockContent'
11 | * }
12 | */
13 | export default defineType({
14 | title: 'Block Content',
15 | name: 'blockContent',
16 | type: 'array',
17 | of: [
18 | defineArrayMember({
19 | title: 'Block',
20 | type: 'block',
21 | // Styles let you set what your user can mark up blocks with. These
22 | // correspond with HTML tags, but you can set any title or value
23 | // you want and decide how you want to deal with it where you want to
24 | // use your content.
25 | styles: [
26 | {title: 'Normal', value: 'normal'},
27 | {title: 'H1', value: 'h1'},
28 | {title: 'H2', value: 'h2'},
29 | {title: 'H3', value: 'h3'},
30 | {title: 'H4', value: 'h4'},
31 | {title: 'Quote', value: 'blockquote'},
32 | ],
33 | lists: [{title: 'Bullet', value: 'bullet'}],
34 | // Marks let you mark up inline text in the block editor.
35 | marks: {
36 | // Decorators usually describe a single property – e.g. a typographic
37 | // preference or highlighting by editors.
38 | decorators: [
39 | {title: 'Strong', value: 'strong'},
40 | {title: 'Emphasis', value: 'em'},
41 | ],
42 | // Annotations can be any object structure – e.g. a link or a footnote.
43 | annotations: [
44 | {
45 | title: 'URL',
46 | name: 'link',
47 | type: 'object',
48 | fields: [
49 | {
50 | title: 'URL',
51 | name: 'href',
52 | type: 'url',
53 | },
54 | ],
55 | },
56 | ],
57 | },
58 | }),
59 | // You can add additional types here. Note that you can't use
60 | // primitive types such as 'string' and 'number' in the same array
61 | // as a block type.
62 | defineArrayMember({
63 | type: 'image',
64 | options: {hotspot: true},
65 | }),
66 | ],
67 | })
68 |
--------------------------------------------------------------------------------
/schemas/category.ts:
--------------------------------------------------------------------------------
1 | import {defineField, defineType} from 'sanity'
2 |
3 | export default defineType({
4 | name: 'category',
5 | title: 'Category',
6 | type: 'document',
7 | fields: [
8 | defineField({
9 | name: 'title',
10 | title: 'Title',
11 | type: 'string',
12 | }),
13 | defineField({
14 | name: 'description',
15 | title: 'Description',
16 | type: 'text',
17 | }),
18 | ],
19 | })
20 |
--------------------------------------------------------------------------------
/schemas/index.ts:
--------------------------------------------------------------------------------
1 | import blockContent from './blockContent'
2 | import category from './category'
3 | import post from './post'
4 | import author from './author'
5 |
6 | export const schemaTypes = [post, author, category, blockContent]
7 |
--------------------------------------------------------------------------------
/schemas/post.ts:
--------------------------------------------------------------------------------
1 | import { defineField, defineType } from "sanity";
2 |
3 | export default defineType({
4 | name: "post",
5 | title: "Post",
6 | type: "document",
7 | fields: [
8 | defineField({
9 | name: "title",
10 | title: "Title",
11 | type: "string",
12 | }),
13 | defineField({
14 | name: "description",
15 | description: "Enter a short snippet for the blog...",
16 | title: "Description",
17 | type: "string",
18 | }),
19 | defineField({
20 | name: "slug",
21 | title: "Slug",
22 | type: "slug",
23 | options: {
24 | source: "title",
25 | maxLength: 96,
26 | },
27 | }),
28 | defineField({
29 | name: "author",
30 | title: "Author",
31 | type: "reference",
32 | to: { type: "author" },
33 | }),
34 | defineField({
35 | name: "mainImage",
36 | title: "Main image",
37 | type: "image",
38 | options: {
39 | hotspot: true,
40 | },
41 | }),
42 | defineField({
43 | name: "categories",
44 | title: "Categories",
45 | type: "array",
46 | of: [{ type: "reference", to: { type: "category" } }],
47 | }),
48 | defineField({
49 | name: "publishedAt",
50 | title: "Published at",
51 | type: "datetime",
52 | }),
53 | defineField({
54 | name: "body",
55 | title: "Body",
56 | type: "blockContent",
57 | }),
58 | ],
59 |
60 | preview: {
61 | select: {
62 | title: "title",
63 | author: "author.name",
64 | media: "mainImage",
65 | },
66 | prepare(selection) {
67 | const { author } = selection;
68 | return { ...selection, subtitle: author && `by ${author}` };
69 | },
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/structure.ts:
--------------------------------------------------------------------------------
1 | import Iframe from "sanity-plugin-iframe-pane";
2 | import type { DefaultDocumentNodeResolver } from "sanity/desk";
3 |
4 | export const getDefaultDocumentNode: DefaultDocumentNodeResolver = (
5 | S,
6 | { schemaType }
7 | ) => {
8 | // Conditionally return a different configuration based on the schema type
9 | if (schemaType === "post") {
10 | return S.document().views([
11 | S.view.form(),
12 |
13 | S.view
14 | .component(Iframe)
15 | .options({
16 | // Required: Accepts an async function
17 | // OR a string
18 | url: `${
19 | process.env.NEXT_PUBLIC_VERCEL_URL || "http://localhost:3000"
20 | }/api/preview`,
21 | // Optional: Set the default size
22 | defaultSize: `desktop`, // default `desktop`
23 | // Optional: Add a reload button, or reload on new document revisions
24 | reload: {
25 | button: true, // default `undefined`
26 | },
27 | // Optional: Pass attributes to the underlying `iframe` element:
28 | // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
29 | attributes: {},
30 | })
31 | .title("Preview"),
32 | ]);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./pages/**/*.{js,ts,jsx,tsx}",
5 | "./components/**/*.{js,ts,jsx,tsx}",
6 | "./app/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | darkMode: "class",
9 | theme: {
10 | extend: {},
11 | },
12 | plugins: [require("@tailwindcss/line-clamp")],
13 | };
14 |
--------------------------------------------------------------------------------
/theme.ts:
--------------------------------------------------------------------------------
1 | import { buildLegacyTheme } from "sanity";
2 |
3 | const props = {
4 | "--my-white": "#fff",
5 | "--my-black": "#1a1a1a",
6 | "--ad-performance-brand": "#8F00FF",
7 | "--my-red": "#db4437",
8 | "--my-yellow": "#fee502",
9 | "--my-green": "#0f9d58",
10 | };
11 |
12 | export const myTheme = buildLegacyTheme({
13 | /* Base theme colors */
14 | "--black": props["--my-black"],
15 | "--white": props["--my-white"],
16 |
17 | "--gray": "#666",
18 | "--gray-base": "#666",
19 |
20 | "--component-bg": props["--my-black"],
21 | "--component-text-color": props["--my-white"],
22 |
23 | /* Brand */
24 | "--brand-primary": props["--ad-performance-brand"],
25 |
26 | // Default button
27 | "--default-button-color": "#666",
28 | "--default-button-primary-color": props["--ad-performance-brand"],
29 | "--default-button-success-color": props["--my-green"],
30 | "--default-button-warning-color": props["--my-yellow"],
31 | "--default-button-danger-color": props["--my-red"],
32 |
33 | /* State */
34 | "--state-info-color": props["--ad-performance-brand"],
35 | "--state-success-color": props["--my-green"],
36 | "--state-warning-color": props["--my-yellow"],
37 | "--state-danger-color": props["--my-red"],
38 |
39 | /* Navbar */
40 | "--main-navigation-color": props["--my-black"],
41 | "--main-navigation-color--inverted": props["--my-white"],
42 |
43 | "--focus-color": props["--ad-performance-brand"],
44 | });
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ]
26 | },
27 | "include": [
28 | "next-env.d.ts",
29 | "**/*.ts",
30 | "**/*.tsx",
31 | ".next/types/**/*.ts"
32 | ],
33 | "exclude": [
34 | "node_modules"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | type Base = {
2 | _createdAt: string;
3 | _id: string;
4 | _rev: string;
5 | _type: string;
6 | _updatedAt: string;
7 | };
8 |
9 | interface Post extends Base {
10 | author: Author;
11 | body: Block[];
12 | categories: Category[];
13 | mainImage: Image;
14 | slug: Slug;
15 | title: string;
16 | description: string;
17 | }
18 |
19 | interface Author extends Base {
20 | bio: Block[];
21 | image: Image;
22 | name: string;
23 | slug: Slug;
24 | }
25 |
26 | interface Image {
27 | _type: "image";
28 | asset: Reference;
29 | }
30 |
31 | interface Reference {
32 | _ref: string;
33 | _type: "reference";
34 | }
35 |
36 | interface Slug {
37 | _type: "slug";
38 | current: string;
39 | }
40 |
41 | interface Block {
42 | _key: string;
43 | _type: "block";
44 | children: Span[];
45 | markDefs: any[];
46 | style: "normal" | "h1" | "h2" | "h3" | "h4" | "blockquote";
47 | }
48 |
49 | interface Span {
50 | _key: string;
51 | _type: "span";
52 | marks: string[];
53 | text: string;
54 | }
55 |
56 | interface Category extends Base {
57 | title: string;
58 | description: string;
59 | }
60 |
61 | interface MainImage {
62 | _type: "image";
63 | asset: Reference;
64 | }
65 |
66 | interface Title {
67 | _type: "string";
68 | current: string;
69 | }
70 |
--------------------------------------------------------------------------------