♻️ 使用 NextJS 重构 #1
4
.gitignore
vendored
4
.gitignore
vendored
@ -35,4 +35,6 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
bun.lockb
|
bun.lockb
|
||||||
|
|
||||||
|
.env
|
@ -2,4 +2,4 @@
|
|||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"printWidth": 120
|
"printWidth": 120
|
||||||
}
|
}
|
||||||
|
17
app/console/[[...index]]/page.tsx
Normal file
17
app/console/[[...index]]/page.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route is responsible for the built-in authoring environment using Sanity Studio.
|
||||||
|
* All routes under your studio path is handled by this file using Next.js' catch-all routes:
|
||||||
|
* https://nextjs.org/docs/routing/dynamic-routes#catch-all-routes
|
||||||
|
*
|
||||||
|
* You can learn more about the next-sanity package here:
|
||||||
|
* https://github.com/sanity-io/next-sanity
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextStudio } from 'next-sanity/studio'
|
||||||
|
import config from '../../../sanity.config'
|
||||||
|
|
||||||
|
export default function StudioPage() {
|
||||||
|
return <NextStudio config={config} />
|
||||||
|
}
|
@ -12,10 +12,20 @@ export interface RelatedAccount {
|
|||||||
|
|
||||||
export const SITE_NAME = "Goatshed";
|
export const SITE_NAME = "Goatshed";
|
||||||
export const SITE_DESCRIPTION = "山羊寒舍,在这里最终智羊工作室的最新动态。";
|
export const SITE_DESCRIPTION = "山羊寒舍,在这里最终智羊工作室的最新动态。";
|
||||||
export const SITE_URL = "https://smartsheep.studio"
|
export const SITE_URL = "https://smartsheep.studio";
|
||||||
|
|
||||||
export const RELATED_ACCOUNTS: RelatedAccount[] = [
|
export const RELATED_ACCOUNTS: RelatedAccount[] = [
|
||||||
{ icon: <GitHubIcon />, platform: "GitHub", accountName: "@smartsheep-hq", link: "https://github.com/smartsheep-hq" },
|
{ icon: <GitHubIcon />, platform: "GitHub", accountName: "@smartsheep-hq", link: "https://github.com/smartsheep-hq" },
|
||||||
{ icon: <TwitterIcon />, platform: "Twitter", accountName: "@littlesheepovo", link: "https://twitter.com/littlesheepovo" },
|
{
|
||||||
{ icon: <CoffeeIcon />, platform: "Ko-fi", accountName: "@littlesheep2code", link: "https://ko-fi.com/littlesheep2code" },
|
icon: <TwitterIcon />,
|
||||||
];
|
platform: "Twitter",
|
||||||
|
accountName: "@littlesheepovo",
|
||||||
|
link: "https://twitter.com/littlesheepovo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <CoffeeIcon />,
|
||||||
|
platform: "Ko-fi",
|
||||||
|
accountName: "@littlesheep2code",
|
||||||
|
link: "https://ko-fi.com/littlesheep2code",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -8,7 +8,7 @@ export async function GET() {
|
|||||||
description: SITE_DESCRIPTION,
|
description: SITE_DESCRIPTION,
|
||||||
site_url: SITE_URL,
|
site_url: SITE_URL,
|
||||||
feed_url: `${SITE_URL}/feed`,
|
feed_url: `${SITE_URL}/feed`,
|
||||||
language: "zh-CN"
|
language: "zh-CN",
|
||||||
});
|
});
|
||||||
|
|
||||||
getSortedPosts().forEach((item) => {
|
getSortedPosts().forEach((item) => {
|
||||||
@ -22,7 +22,7 @@ export async function GET() {
|
|||||||
|
|
||||||
return new Response(feed.xml(), {
|
return new Response(feed.xml(), {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/xml"
|
"content-type": "application/xml",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
@ -14,6 +14,7 @@ import "@fontsource/roboto/700.css";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
import AppShell from "@/components/AppShell";
|
import AppShell from "@/components/AppShell";
|
||||||
|
import NextTopLoader from "nextjs-toploader";
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = "edge";
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ export default function RootLayout({ children }: Readonly<{
|
|||||||
<body>
|
<body>
|
||||||
<AppRouterCacheProvider>
|
<AppRouterCacheProvider>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<NextTopLoader showAtBottom color="#4a5099" />
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AppShell>{children}</AppShell>
|
<AppShell>{children}</AppShell>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
31
app/page.tsx
31
app/page.tsx
@ -2,14 +2,15 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card, colors,
|
Card,
|
||||||
|
colors,
|
||||||
Container,
|
Container,
|
||||||
Grid,
|
Grid,
|
||||||
List,
|
List,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemButton,
|
ListItemButton,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Typography
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { RELATED_ACCOUNTS } from "@/app/consts";
|
import { RELATED_ACCOUNTS } from "@/app/consts";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -18,19 +19,18 @@ import Link from "next/link";
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<Container sx={{ scrollBehavior: "smooth", px: 5 }}>
|
<Container sx={{ scrollBehavior: "smooth", px: 5 }}>
|
||||||
<Grid
|
<Grid container id="introduce" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||||
container
|
|
||||||
id="introduce"
|
|
||||||
alignItems="center"
|
|
||||||
sx={{ height: "calc(100vh - 64px)" }}
|
|
||||||
>
|
|
||||||
<Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}>
|
<Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}>
|
||||||
<Typography variant="h1" gutterBottom>你好呀 👋</Typography>
|
<Typography variant="h1" gutterBottom>
|
||||||
|
你好呀 👋
|
||||||
|
</Typography>
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
欢迎来到 SmartSheep Studio 的官方网站!在这里了解,订阅,跟踪我们的最新消息。
|
欢迎来到 SmartSheep Studio 的官方网站!在这里了解,订阅,跟踪我们的最新消息。
|
||||||
接触我们最大的官方社区,并且尝试最新产品,参与各种活动,提供反馈,让我们更好的服务您。
|
接触我们最大的官方社区,并且尝试最新产品,参与各种活动,提供反馈,让我们更好的服务您。
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" href="#about-us" size="large">探索更多</Button>
|
<Button variant="contained" href="#about-us" size="large">
|
||||||
|
探索更多
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
@ -44,12 +44,7 @@ export default function Home() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid container id="about-us" alignItems="center" sx={{ height: "calc(100vh - 64px)" }}>
|
||||||
container
|
|
||||||
id="about-us"
|
|
||||||
alignItems="center"
|
|
||||||
sx={{ height: "calc(100vh - 64px)" }}
|
|
||||||
>
|
|
||||||
<Grid item xs={12} sm={6} sx={{ display: "flex", justifyContent: { xs: "center", sm: "end" } }}>
|
<Grid item xs={12} sm={6} sx={{ display: "flex", justifyContent: { xs: "center", sm: "end" } }}>
|
||||||
<Card sx={{ flexGrow: 1, mr: { xs: 0, sm: 4, md: 8 } }}>
|
<Card sx={{ flexGrow: 1, mr: { xs: 0, sm: 4, md: 8 } }}>
|
||||||
<List sx={{ width: "100%", bgcolor: "background.paper" }}>
|
<List sx={{ width: "100%", bgcolor: "background.paper" }}>
|
||||||
@ -67,7 +62,9 @@ export default function Home() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}>
|
<Grid item xs={12} sm={6} sx={{ textAlign: { xs: "center", sm: "initial" } }}>
|
||||||
<Typography variant="h1" gutterBottom>关于我们</Typography>
|
<Typography variant="h1" gutterBottom>
|
||||||
|
关于我们
|
||||||
|
</Typography>
|
||||||
<Typography paragraph>
|
<Typography paragraph>
|
||||||
我们是一群充满活力、对开源充满热情的开发者。成立于 2019 年。自那年起我们一直在开发让人喜欢的开源软件。
|
我们是一群充满活力、对开源充满热情的开发者。成立于 2019 年。自那年起我们一直在开发让人喜欢的开源软件。
|
||||||
在我们这里,“取之于开源,用之于开源” 不仅是原则,更是我们信仰的座右铭。
|
在我们这里,“取之于开源,用之于开源” 不仅是原则,更是我们信仰的座右铭。
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
import { Box, Card, CardContent, CardMedia, Divider, Typography } from "@mui/material";
|
import { Box, Card, CardContent, CardMedia, Chip, Divider, Stack, Typography } from "@mui/material";
|
||||||
import { getSinglePost } from "@/content/posts";
|
import { client } from "@/sanity/lib/client";
|
||||||
import Image from "next/image";
|
|
||||||
import PostContent from "@/components/posts/PostContent";
|
import PostContent from "@/components/posts/PostContent";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function PostDetailPage({ params }: { params: { id: string } }) {
|
export default async function PostDetailPage({ params }: { params: { id: string } }) {
|
||||||
const post = getSinglePost(params.id);
|
const post = await client.fetch<any>(`*[_type == "post" && slug.current == $slug][0] {
|
||||||
|
title, description, slug, body, author, publishedAt,
|
||||||
|
mainImage {
|
||||||
|
asset -> {
|
||||||
|
_id,
|
||||||
|
url
|
||||||
|
},
|
||||||
|
alt
|
||||||
|
},
|
||||||
|
"categories": categories[]->title,
|
||||||
|
"author_name": author->name,
|
||||||
|
"author_image": author->image
|
||||||
|
}`, { slug: params.id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
{
|
{
|
||||||
post.thumbnail &&
|
post.mainImage &&
|
||||||
<CardMedia sx={{ height: 360, position: "relative" }} title={post.title}>
|
<CardMedia sx={{ height: 360, position: "relative" }} title={post.mainImage.alt}>
|
||||||
<Image
|
<Image
|
||||||
fill
|
fill
|
||||||
src={post.thumbnail}
|
src={post.mainImage.asset.url}
|
||||||
alt={post.title}
|
alt={post.mainImage.alt}
|
||||||
style={{ objectFit: "cover" }}
|
style={{ objectFit: "cover" }}
|
||||||
/>
|
/>
|
||||||
</CardMedia>
|
</CardMedia>
|
||||||
@ -22,18 +34,22 @@ export default function PostDetailPage({ params }: { params: { id: string } }) {
|
|||||||
|
|
||||||
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography gutterBottom variant="h2">
|
<Typography variant="h2">
|
||||||
{post.title}
|
{post.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<Stack direction="row" sx={{ mx: -0.5, mt: 1, mb: 1.2 }}>
|
||||||
|
{post.categories.map((category: string, idx: number) => <Chip size="small" label={category} key={idx} />)}
|
||||||
|
</Stack>
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography color="text.secondary" variant="body2">
|
||||||
{post.description ?? "No description yet."}
|
{post.description ?? "No description yet."}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider sx={{ my: 5 }} />
|
<Divider sx={{ my: 2.5, mx: -5 }} />
|
||||||
<Box component="article" className="prose max-w-none" sx={{ minWidth: 0 }}>
|
<Box component="article" className="prose max-w-none" sx={{ minWidth: 0 }}>
|
||||||
<PostContent content={post.content ?? ""} />
|
<PostContent content={post.body} />
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { Box, Container } from "@mui/material";
|
import { Box, Container } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export default function PostLayout({children}: Readonly<{
|
export default function PostLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}>
|
<Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 2 }}>
|
||||||
<Box sx={{ flexGrow: 1, maxWidth: 720 }}>
|
<Box sx={{ flexGrow: 1, maxWidth: 720 }}>{children}</Box>
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,52 @@
|
|||||||
import { Button, Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material";
|
import { Button, Card, CardActions, CardContent, CardMedia, Chip, Stack, Typography } from "@mui/material";
|
||||||
import { getSortedPosts } from "@/content/posts";
|
import { client } from "@/sanity/lib/client";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function PostList() {
|
export default async function PostList() {
|
||||||
const posts = getSortedPosts();
|
const posts = await client.fetch<any[]>(`*[_type == "post"] {
|
||||||
|
title, description, slug, author, publishedAt,
|
||||||
|
mainImage {
|
||||||
|
asset -> {
|
||||||
|
_id,
|
||||||
|
url
|
||||||
|
},
|
||||||
|
alt
|
||||||
|
},
|
||||||
|
"categories": categories[]->title,
|
||||||
|
"author_name": author->name,
|
||||||
|
"author_image": author->image
|
||||||
|
}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
posts.map((post) => (
|
posts.map((post) => (
|
||||||
<Card key={post.id} sx={{ width: "100%" }}>
|
<Card key={post.slug.current} sx={{ width: "100%" }}>
|
||||||
{
|
{
|
||||||
post.thumbnail &&
|
post.mainImage &&
|
||||||
<CardMedia sx={{ height: 160, position: "relative" }} title={post.title}>
|
<CardMedia sx={{ height: 160, position: "relative" }} title={post.mainImage.alt}>
|
||||||
<Image
|
<Image
|
||||||
fill
|
fill
|
||||||
src={post.thumbnail}
|
src={post.mainImage.asset.url}
|
||||||
alt={post.title}
|
alt={post.mainImage.alt}
|
||||||
style={{ objectFit: "cover" }}
|
style={{ objectFit: "cover" }}
|
||||||
/>
|
/>
|
||||||
</CardMedia>
|
</CardMedia>
|
||||||
}
|
}
|
||||||
|
|
||||||
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
<CardContent sx={{ paddingX: 5, paddingY: 3 }}>
|
||||||
<Typography gutterBottom variant="h3">
|
<Typography variant="h3">
|
||||||
{post.title}
|
{post.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
|
<Stack direction="row" sx={{ mx: -0.5, mt: 1, mb: 1.2 }}>
|
||||||
|
{post.categories.map((category: string, idx: number) => <Chip size="small" label={category} key={idx} />)}
|
||||||
|
</Stack>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{post.description ?? "No description yet."}
|
{post.description ? post.description : "No description yet."}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions sx={{ paddingX: 4, paddingBottom: 2 }}>
|
<CardActions sx={{ paddingX: 4, paddingBottom: 2 }}>
|
||||||
<Link href={`/p/${post.id}`} passHref>
|
<Link href={`/p/${post.slug.current}`} passHref>
|
||||||
<Button>Read more</Button>
|
<Button>Read more</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
|
@ -10,20 +10,20 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
url: `${SITE_URL}/`,
|
url: `${SITE_URL}/`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "weekly",
|
changeFrequency: "weekly",
|
||||||
priority: 1
|
priority: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${SITE_URL}/posts`,
|
url: `${SITE_URL}/posts`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "daily",
|
changeFrequency: "daily",
|
||||||
priority: 0.8
|
priority: 0.8,
|
||||||
},
|
},
|
||||||
|
|
||||||
...posts.map((item: Post) => ({
|
...posts.map((item: Post) => ({
|
||||||
url: `${SITE_URL}/posts/${item.id}`,
|
url: `${SITE_URL}/posts/${item.id}`,
|
||||||
lastModified: item.date,
|
lastModified: item.date,
|
||||||
changeFrequency: "daily" as any,
|
changeFrequency: "daily" as any,
|
||||||
priority: 0.75
|
priority: 0.75,
|
||||||
}))
|
})),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,11 @@ import { createTheme } from "@mui/material/styles";
|
|||||||
export const theme = createTheme({
|
export const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: "#49509e"
|
main: "#49509e",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: "#d43630"
|
main: "#d43630",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
h1: { fontSize: "2.5rem" },
|
h1: { fontSize: "2.5rem" },
|
||||||
|
@ -8,7 +8,9 @@ import {
|
|||||||
AppBarProps as MuiAppBarProps,
|
AppBarProps as MuiAppBarProps,
|
||||||
useScrollTrigger,
|
useScrollTrigger,
|
||||||
IconButton,
|
IconButton,
|
||||||
styled, Box, useMediaQuery
|
styled,
|
||||||
|
Box,
|
||||||
|
useMediaQuery,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { ReactElement, ReactNode, useEffect, useState } from "react";
|
import { ReactElement, ReactNode, useEffect, useState } from "react";
|
||||||
import { SITE_NAME } from "@/app/consts";
|
import { SITE_NAME } from "@/app/consts";
|
||||||
@ -17,13 +19,10 @@ import MenuIcon from "@mui/icons-material/Menu";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
function HideOnScroll(props: {
|
function HideOnScroll(props: { window?: () => Window; children: ReactElement }) {
|
||||||
window?: () => Window;
|
|
||||||
children: ReactElement;
|
|
||||||
}) {
|
|
||||||
const { children, window } = props;
|
const { children, window } = props;
|
||||||
const trigger = useScrollTrigger({
|
const trigger = useScrollTrigger({
|
||||||
target: window ? window() : undefined
|
target: window ? window() : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,24 +37,25 @@ interface AppBarProps extends MuiAppBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ShellAppBar = styled(MuiAppBar, {
|
const ShellAppBar = styled(MuiAppBar, {
|
||||||
shouldForwardProp: (prop) => prop !== "open"
|
shouldForwardProp: (prop) => prop !== "open",
|
||||||
})<AppBarProps>(({ theme, open }) => {
|
})<AppBarProps>(({ theme, open }) => {
|
||||||
const isMobile = useMediaQuery(isMobileQuery);
|
const isMobile = useMediaQuery(isMobileQuery);
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
transition: theme.transitions.create(["margin", "width"], {
|
transition: theme.transitions.create(["margin", "width"], {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.leavingScreen
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
}),
|
}),
|
||||||
...(!isMobile && open && {
|
...(!isMobile &&
|
||||||
width: `calc(100% - ${DRAWER_WIDTH}px)`,
|
open && {
|
||||||
transition: theme.transitions.create(["margin", "width"], {
|
width: `calc(100% - ${DRAWER_WIDTH}px)`,
|
||||||
easing: theme.transitions.easing.easeOut,
|
transition: theme.transitions.create(["margin", "width"], {
|
||||||
duration: theme.transitions.duration.enteringScreen
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
marginRight: DRAWER_WIDTH,
|
||||||
}),
|
}),
|
||||||
marginRight: DRAWER_WIDTH
|
};
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{
|
const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{
|
||||||
@ -63,28 +63,26 @@ const AppMain = styled("main", { shouldForwardProp: (prop) => prop !== "open" })
|
|||||||
}>(({ theme, open }) => {
|
}>(({ theme, open }) => {
|
||||||
const isMobile = useMediaQuery(isMobileQuery);
|
const isMobile = useMediaQuery(isMobileQuery);
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
transition: theme.transitions.create("margin", {
|
transition: theme.transitions.create("margin", {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.leavingScreen
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
}),
|
}),
|
||||||
marginRight: -DRAWER_WIDTH,
|
marginRight: -DRAWER_WIDTH,
|
||||||
...(!isMobile && open && {
|
...(!isMobile &&
|
||||||
transition: theme.transitions.create("margin", {
|
open && {
|
||||||
easing: theme.transitions.easing.easeOut,
|
transition: theme.transitions.create("margin", {
|
||||||
duration: theme.transitions.duration.enteringScreen
|
easing: theme.transitions.easing.easeOut,
|
||||||
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
|
}),
|
||||||
|
marginRight: 0,
|
||||||
}),
|
}),
|
||||||
marginRight: 0
|
position: "relative",
|
||||||
}),
|
};
|
||||||
position: "relative"
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default function AppShell({ children }: { children: ReactNode }) {
|
||||||
export default function AppShell({ children }: {
|
|
||||||
children: ReactNode,
|
|
||||||
}) {
|
|
||||||
let documentWindow: Window;
|
let documentWindow: Window;
|
||||||
|
|
||||||
const isMobile = useMediaQuery(isMobileQuery);
|
const isMobile = useMediaQuery(isMobileQuery);
|
||||||
@ -110,9 +108,7 @@ export default function AppShell({ children }: {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}>
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1, fontSize: "1.2rem" }}>
|
||||||
<Link href="/">
|
<Link href="/">{SITE_NAME}</Link>
|
||||||
{SITE_NAME}
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -140,4 +136,4 @@ export default function AppShell({ children }: {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
styled,
|
styled,
|
||||||
useMediaQuery
|
useMediaQuery,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { theme } from "@/app/theme";
|
import { theme } from "@/app/theme";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
@ -42,27 +42,25 @@ export const AppNavigationHeader = styled("div")(({ theme }) => ({
|
|||||||
padding: theme.spacing(0, 1),
|
padding: theme.spacing(0, 1),
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
height: 64,
|
height: 64,
|
||||||
...theme.mixins.toolbar
|
...theme.mixins.toolbar,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function AppNavigation({ showClose, onClose }: {
|
export function AppNavigation({ showClose, onClose }: { showClose?: boolean; onClose: () => void }) {
|
||||||
showClose?: boolean,
|
|
||||||
onClose: () => void
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppNavigationHeader>
|
<AppNavigationHeader>
|
||||||
{
|
{showClose && (
|
||||||
showClose &&
|
|
||||||
<IconButton onClick={onClose}>
|
<IconButton onClick={onClose}>
|
||||||
{theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
{theme.direction === "rtl" ? <ChevronLeftIcon /> : <ChevronRightIcon />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
)}
|
||||||
</AppNavigationHeader>
|
</AppNavigationHeader>
|
||||||
<Divider />
|
<Divider />
|
||||||
<List>
|
<List>
|
||||||
{NAVIGATION_ITEMS.map((item, idx) => {
|
{NAVIGATION_ITEMS.map((item, idx) => {
|
||||||
return item.divider ? <Divider key={idx} /> : (
|
return item.divider ? (
|
||||||
|
<Divider key={idx} />
|
||||||
|
) : (
|
||||||
<Link key={idx} href={item.link ?? "/"} passHref>
|
<Link key={idx} href={item.link ?? "/"} passHref>
|
||||||
<ListItemButton>
|
<ListItemButton>
|
||||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
||||||
@ -78,10 +76,7 @@ export function AppNavigation({ showClose, onClose }: {
|
|||||||
|
|
||||||
export const isMobileQuery = theme.breakpoints.down("md");
|
export const isMobileQuery = theme.breakpoints.down("md");
|
||||||
|
|
||||||
export default function NavigationDrawer({ open, onClose }: {
|
export default function NavigationDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||||
open: boolean,
|
|
||||||
onClose: () => void,
|
|
||||||
}) {
|
|
||||||
const isMobile = useMediaQuery(isMobileQuery);
|
const isMobile = useMediaQuery(isMobileQuery);
|
||||||
|
|
||||||
return isMobile ? (
|
return isMobile ? (
|
||||||
@ -96,8 +91,8 @@ export default function NavigationDrawer({ open, onClose }: {
|
|||||||
sx={{
|
sx={{
|
||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
width: DRAWER_WIDTH
|
width: DRAWER_WIDTH,
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppNavigation onClose={onClose} />
|
<AppNavigation onClose={onClose} />
|
||||||
@ -112,11 +107,11 @@ export default function NavigationDrawer({ open, onClose }: {
|
|||||||
width: DRAWER_WIDTH,
|
width: DRAWER_WIDTH,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
"& .MuiDrawer-paper": {
|
"& .MuiDrawer-paper": {
|
||||||
width: DRAWER_WIDTH
|
width: DRAWER_WIDTH,
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppNavigation showClose onClose={onClose} />
|
<AppNavigation showClose onClose={onClose} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Markdown from "react-markdown";
|
import { PortableText } from "@portabletext/react";
|
||||||
|
|
||||||
export default function PostContent({ content }: { content: string }) {
|
export default function PostContent({ content }: { content: any }) {
|
||||||
return <Markdown>{content}</Markdown>;
|
return <PortableText value={content} />;
|
||||||
}
|
}
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import matter from "gray-matter";
|
|
||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), "content", "posts");
|
|
||||||
|
|
||||||
export interface Post {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
description?: string;
|
|
||||||
content?: string;
|
|
||||||
date: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSortedPosts() {
|
|
||||||
const fileNames = fs.readdirSync(postsDirectory);
|
|
||||||
const allPostsData: Post[] = fileNames.map((fileName) => {
|
|
||||||
const id = fileName.replace(/\.md$/, "");
|
|
||||||
|
|
||||||
const fullPath = path.join(postsDirectory, fileName);
|
|
||||||
const fileContents = fs.readFileSync(fullPath, "utf8");
|
|
||||||
|
|
||||||
const matterResult = matter(fileContents);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
...matterResult.data
|
|
||||||
} as Post;
|
|
||||||
});
|
|
||||||
|
|
||||||
return allPostsData.sort((a, b) => {
|
|
||||||
return a.date < b.date ? 1 : -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSinglePost(id: string) {
|
|
||||||
const fullPath = path.join(postsDirectory, id + ".md");
|
|
||||||
const fileContents = fs.readFileSync(fullPath, "utf8");
|
|
||||||
const matterResult = matter(fileContents);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
content: matterResult.content,
|
|
||||||
...matterResult.data,
|
|
||||||
} as Post;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
thumbnail: 'https://images.unsplash.com/photo-1707344088547-3cf7cea5ca49?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
|
|
||||||
title: 'Two Forms of Pre-rendering'
|
|
||||||
date: '2020-01-01'
|
|
||||||
---
|
|
||||||
|
|
||||||
# Woah!
|
|
||||||
|
|
||||||
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
|
|
||||||
|
|
||||||
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
|
|
||||||
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
|
|
||||||
|
|
||||||
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
|
|
@ -5,9 +5,9 @@ const nextConfig = {
|
|||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "**"
|
hostname: "**",
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
@ -15,9 +15,9 @@ const nextConfig = {
|
|||||||
{ source: "/rss.xml", destination: "/feed.xml" },
|
{ source: "/rss.xml", destination: "/feed.xml" },
|
||||||
{ source: "/feed.xml", destination: "/feed" },
|
{ source: "/feed.xml", destination: "/feed" },
|
||||||
|
|
||||||
{ source: "/p/:id", destination: "/posts/:id" }
|
{ source: "/p/:id", destination: "/posts/:id" },
|
||||||
];
|
];
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
18
package.json
18
package.json
@ -19,17 +19,29 @@
|
|||||||
"@mui/material": "^5.15.10",
|
"@mui/material": "^5.15.10",
|
||||||
"@mui/material-nextjs": "^5.15.11",
|
"@mui/material-nextjs": "^5.15.11",
|
||||||
"@next/mdx": "^14.1.0",
|
"@next/mdx": "^14.1.0",
|
||||||
|
"@portabletext/react": "^3.0.11",
|
||||||
|
"@sanity/client": "^6.12.4",
|
||||||
|
"@sanity/icons": "^2.8",
|
||||||
|
"@sanity/image-url": "1",
|
||||||
|
"@sanity/types": "^3.25",
|
||||||
|
"@sanity/ui": "^2.0",
|
||||||
|
"@sanity/vision": "3",
|
||||||
"@types/mdx": "^2.0.11",
|
"@types/mdx": "^2.0.11",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"next": "14.1.0",
|
"next": "^14.1",
|
||||||
"react": "^18",
|
"next-sanity": "7.1.4",
|
||||||
|
"nextjs-toploader": "^1.6.6",
|
||||||
|
"react": "^18.2",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"rss": "^1.2.2"
|
"rss": "^1.2.2",
|
||||||
|
"sanity": "^3.25",
|
||||||
|
"styled-components": "^6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/rss": "^0.0.32",
|
"@types/rss": "^0.0.32",
|
||||||
|
10
sanity.cli.ts
Normal file
10
sanity.cli.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* This configuration file lets you run `$ sanity [command]` in this folder
|
||||||
|
* Go to https://www.sanity.io/docs/cli to learn more.
|
||||||
|
**/
|
||||||
|
import { defineCliConfig } from "sanity/cli";
|
||||||
|
|
||||||
|
const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
|
||||||
|
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
|
||||||
|
|
||||||
|
export default defineCliConfig({ api: { projectId, dataset } });
|
25
sanity.config.ts
Normal file
25
sanity.config.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* This configuration is used to for the Sanity Studio that’s mounted on the `/pages/studio/page.tsx` route
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { visionTool } from "@sanity/vision";
|
||||||
|
import { defineConfig } from "sanity";
|
||||||
|
import { structureTool } from "sanity/structure";
|
||||||
|
|
||||||
|
// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
|
||||||
|
import { apiVersion, dataset, projectId } from "./sanity/env";
|
||||||
|
import { schema } from "./sanity/schema";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
basePath: "/console",
|
||||||
|
projectId,
|
||||||
|
dataset,
|
||||||
|
// Add and edit the content schema in the './sanity/schema' folder
|
||||||
|
schema,
|
||||||
|
plugins: [
|
||||||
|
structureTool(),
|
||||||
|
// Vision is a tool that lets you query your content with GROQ in the studio
|
||||||
|
// https://www.sanity.io/docs/the-vision-plugin
|
||||||
|
visionTool({ defaultApiVersion: apiVersion }),
|
||||||
|
],
|
||||||
|
});
|
21
sanity/env.ts
Normal file
21
sanity/env.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-02-24";
|
||||||
|
|
||||||
|
export const dataset = assertValue(
|
||||||
|
process.env.NEXT_PUBLIC_SANITY_DATASET,
|
||||||
|
"Missing environment variable: NEXT_PUBLIC_SANITY_DATASET",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const projectId = assertValue(
|
||||||
|
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
|
||||||
|
"Missing environment variable: NEXT_PUBLIC_SANITY_PROJECT_ID",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useCdn = false;
|
||||||
|
|
||||||
|
function assertValue<T>(v: T | undefined, errorMessage: string): T {
|
||||||
|
if (v === undefined) {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
10
sanity/lib/client.ts
Normal file
10
sanity/lib/client.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createClient } from "next-sanity";
|
||||||
|
|
||||||
|
import { apiVersion, dataset, projectId, useCdn } from "../env";
|
||||||
|
|
||||||
|
export const client = createClient({
|
||||||
|
apiVersion,
|
||||||
|
dataset,
|
||||||
|
projectId,
|
||||||
|
useCdn,
|
||||||
|
});
|
13
sanity/lib/image.ts
Normal file
13
sanity/lib/image.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import createImageUrlBuilder from "@sanity/image-url";
|
||||||
|
import type { Image } from "sanity";
|
||||||
|
|
||||||
|
import { dataset, projectId } from "../env";
|
||||||
|
|
||||||
|
const imageBuilder = createImageUrlBuilder({
|
||||||
|
projectId: projectId || "",
|
||||||
|
dataset: dataset || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const urlForImage = (source: Image) => {
|
||||||
|
return imageBuilder?.image(source).auto("format").fit("max").url();
|
||||||
|
};
|
10
sanity/schema.ts
Normal file
10
sanity/schema.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { type SchemaTypeDefinition } from "sanity";
|
||||||
|
|
||||||
|
import blockContent from "./schemaTypes/blockContent";
|
||||||
|
import category from "./schemaTypes/category";
|
||||||
|
import post from "./schemaTypes/post";
|
||||||
|
import author from "./schemaTypes/author";
|
||||||
|
|
||||||
|
export const schema: { types: SchemaTypeDefinition[] } = {
|
||||||
|
types: [post, author, category, blockContent],
|
||||||
|
};
|
57
sanity/schemaTypes/author.ts
Normal file
57
sanity/schemaTypes/author.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { defineField, defineType } from "sanity";
|
||||||
|
|
||||||
|
export default defineType({
|
||||||
|
name: "author",
|
||||||
|
title: "Author",
|
||||||
|
type: "document",
|
||||||
|
fields: [
|
||||||
|
defineField({
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
type: "string",
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "slug",
|
||||||
|
title: "Slug",
|
||||||
|
type: "slug",
|
||||||
|
options: {
|
||||||
|
source: "name",
|
||||||
|
maxLength: 96,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "image",
|
||||||
|
title: "Image",
|
||||||
|
type: "image",
|
||||||
|
options: {
|
||||||
|
hotspot: true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "alt",
|
||||||
|
type: "string",
|
||||||
|
title: "Alternative Text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "bio",
|
||||||
|
title: "Bio",
|
||||||
|
type: "array",
|
||||||
|
of: [
|
||||||
|
{
|
||||||
|
title: "Block",
|
||||||
|
type: "block",
|
||||||
|
styles: [{ title: "Normal", value: "normal" }],
|
||||||
|
lists: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
preview: {
|
||||||
|
select: {
|
||||||
|
title: "name",
|
||||||
|
media: "image",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
75
sanity/schemaTypes/blockContent.ts
Normal file
75
sanity/schemaTypes/blockContent.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { defineType, defineArrayMember } from "sanity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the schema type for block content used in the post document type
|
||||||
|
* Importing this type into the studio configuration's `schema` property
|
||||||
|
* lets you reuse it in other document types with:
|
||||||
|
* {
|
||||||
|
* name: 'someName',
|
||||||
|
* title: 'Some title',
|
||||||
|
* type: 'blockContent'
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default defineType({
|
||||||
|
title: "Block Content",
|
||||||
|
name: "blockContent",
|
||||||
|
type: "array",
|
||||||
|
of: [
|
||||||
|
defineArrayMember({
|
||||||
|
title: "Block",
|
||||||
|
type: "block",
|
||||||
|
// Styles let you define what blocks can be marked up as. The default
|
||||||
|
// set corresponds with HTML tags, but you can set any title or value
|
||||||
|
// you want, and decide how you want to deal with it where you want to
|
||||||
|
// use your content.
|
||||||
|
styles: [
|
||||||
|
{ title: "Normal", value: "normal" },
|
||||||
|
{ title: "H1", value: "h1" },
|
||||||
|
{ title: "H2", value: "h2" },
|
||||||
|
{ title: "H3", value: "h3" },
|
||||||
|
{ title: "H4", value: "h4" },
|
||||||
|
{ title: "Quote", value: "blockquote" },
|
||||||
|
],
|
||||||
|
lists: [{ title: "Bullet", value: "bullet" }],
|
||||||
|
// Marks let you mark up inline text in the Portable Text Editor
|
||||||
|
marks: {
|
||||||
|
// Decorators usually describe a single property – e.g. a typographic
|
||||||
|
// preference or highlighting
|
||||||
|
decorators: [
|
||||||
|
{ title: "Strong", value: "strong" },
|
||||||
|
{ title: "Emphasis", value: "em" },
|
||||||
|
],
|
||||||
|
// Annotations can be any object structure – e.g. a link or a footnote.
|
||||||
|
annotations: [
|
||||||
|
{
|
||||||
|
title: "URL",
|
||||||
|
name: "link",
|
||||||
|
type: "object",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
title: "URL",
|
||||||
|
name: "href",
|
||||||
|
type: "url",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// You can add additional types here. Note that you can't use
|
||||||
|
// primitive types such as 'string' and 'number' in the same array
|
||||||
|
// as a block type.
|
||||||
|
defineArrayMember({
|
||||||
|
type: "image",
|
||||||
|
options: { hotspot: true },
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "alt",
|
||||||
|
type: "string",
|
||||||
|
title: "Alternative Text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
19
sanity/schemaTypes/category.ts
Normal file
19
sanity/schemaTypes/category.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineField, defineType } from "sanity";
|
||||||
|
|
||||||
|
export default defineType({
|
||||||
|
name: "category",
|
||||||
|
title: "Category",
|
||||||
|
type: "document",
|
||||||
|
fields: [
|
||||||
|
defineField({
|
||||||
|
name: "title",
|
||||||
|
title: "Title",
|
||||||
|
type: "string",
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "description",
|
||||||
|
title: "Description",
|
||||||
|
type: "text",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
77
sanity/schemaTypes/post.ts
Normal file
77
sanity/schemaTypes/post.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { defineField, defineType } from "sanity";
|
||||||
|
|
||||||
|
export default defineType({
|
||||||
|
name: "post",
|
||||||
|
title: "Post",
|
||||||
|
type: "document",
|
||||||
|
fields: [
|
||||||
|
defineField({
|
||||||
|
name: "title",
|
||||||
|
title: "Title",
|
||||||
|
type: "string",
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "description",
|
||||||
|
title: "Description",
|
||||||
|
type: "string",
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "slug",
|
||||||
|
title: "Slug",
|
||||||
|
type: "slug",
|
||||||
|
options: {
|
||||||
|
source: "title",
|
||||||
|
maxLength: 96,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "author",
|
||||||
|
title: "Author",
|
||||||
|
type: "reference",
|
||||||
|
to: { type: "author" },
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "mainImage",
|
||||||
|
title: "Main image",
|
||||||
|
type: "image",
|
||||||
|
options: {
|
||||||
|
hotspot: true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "alt",
|
||||||
|
type: "string",
|
||||||
|
title: "Alternative Text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "categories",
|
||||||
|
title: "Categories",
|
||||||
|
type: "array",
|
||||||
|
of: [{ type: "reference", to: { type: "category" } }],
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "publishedAt",
|
||||||
|
title: "Published at",
|
||||||
|
type: "datetime",
|
||||||
|
}),
|
||||||
|
defineField({
|
||||||
|
name: "body",
|
||||||
|
title: "Body",
|
||||||
|
type: "blockContent",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
preview: {
|
||||||
|
select: {
|
||||||
|
title: "title",
|
||||||
|
author: "author.name",
|
||||||
|
media: "mainImage",
|
||||||
|
},
|
||||||
|
prepare(selection) {
|
||||||
|
const { author } = selection;
|
||||||
|
return { ...selection, subtitle: author && `by ${author}` };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
@ -12,6 +12,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"target": "ES2017",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
|
Loading…
Reference in New Issue
Block a user