├── src ├── app │ ├── (payload) │ │ ├── custom.scss │ │ ├── api │ │ │ ├── graphql-playground │ │ │ │ └── route.ts │ │ │ ├── graphql │ │ │ │ └── route.ts │ │ │ └── [...slug] │ │ │ │ └── route.ts │ │ ├── admin │ │ │ ├── [[...segments]] │ │ │ │ ├── page.tsx │ │ │ │ └── not-found.tsx │ │ │ └── importMap.js │ │ └── layout.tsx │ ├── favicon.ico │ └── (frontend) │ │ ├── page.tsx │ │ ├── next │ │ ├── exit-preview │ │ │ └── route.ts │ │ └── preview │ │ │ └── route.ts │ │ ├── not-found.tsx │ │ ├── (sitemaps) │ │ ├── posts-sitemap.xml │ │ │ └── route.ts │ │ └── pages-sitemap.xml │ │ │ └── route.ts │ │ ├── layout.tsx │ │ ├── posts │ │ ├── page.tsx │ │ ├── page │ │ │ └── [pageNumber] │ │ │ │ └── page.tsx │ │ └── [slug] │ │ │ └── page.tsx │ │ ├── search │ │ └── page.tsx │ │ └── [slug] │ │ └── page.tsx ├── lib │ ├── access │ │ ├── anyone.ts │ │ ├── authenticated.ts │ │ └── authenticatedOrPublished.ts │ ├── utilities │ │ ├── canUseDOM.ts │ │ ├── toKebabCase.ts │ │ ├── useDebounce.ts │ │ ├── mergeOpenGraph.ts │ │ ├── generatePreviewPath.ts │ │ ├── generateMeta.ts │ │ ├── getGlobals.ts │ │ ├── getRedirects.ts │ │ ├── formatDateTime.ts │ │ ├── getURL.ts │ │ ├── getDocument.ts │ │ ├── formatAuthors.ts │ │ ├── deepMerge.ts │ │ ├── getMeUser.ts │ │ └── useClickableCard.ts │ ├── utils.ts │ ├── hooks │ │ ├── revalidateRedirects.ts │ │ ├── populatePublishedAt.ts │ │ └── formatSlug.ts │ ├── search │ │ ├── Component.tsx │ │ ├── fieldOverrides.ts │ │ └── beforeSync.ts │ └── plugins.ts ├── components │ ├── site │ │ ├── admin-bar │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── footer │ │ │ ├── hooks │ │ │ │ └── revalidateFooter.ts │ │ │ ├── row-label.tsx │ │ │ ├── config.ts │ │ │ └── index.tsx │ │ ├── header │ │ │ ├── hooks │ │ │ │ └── revalidateHeader.ts │ │ │ ├── row-label.tsx │ │ │ ├── config.ts │ │ │ ├── nav.tsx │ │ │ └── index.tsx │ │ ├── live-preview-listener.tsx │ │ ├── media │ │ │ ├── index.tsx │ │ │ ├── types.ts │ │ │ ├── video-media │ │ │ │ └── index.tsx │ │ │ └── image-media │ │ │ │ └── index.tsx │ │ ├── collection-archive.tsx │ │ ├── page-range.tsx │ │ ├── redirects.tsx │ │ ├── link.tsx │ │ ├── rich-text.tsx │ │ ├── pagination.tsx │ │ └── card.tsx │ ├── blocks │ │ ├── media-block │ │ │ ├── config.ts │ │ │ └── index.tsx │ │ ├── form-block │ │ │ ├── message │ │ │ │ └── index.tsx │ │ │ ├── width │ │ │ │ └── index.tsx │ │ │ ├── fields.tsx │ │ │ ├── error │ │ │ │ └── index.tsx │ │ │ ├── text │ │ │ │ └── index.tsx │ │ │ ├── number │ │ │ │ └── index.tsx │ │ │ ├── textarea │ │ │ │ └── index.tsx │ │ │ ├── config.ts │ │ │ ├── checkbox │ │ │ │ └── index.tsx │ │ │ ├── select │ │ │ │ └── index.tsx │ │ │ ├── email │ │ │ │ └── index.tsx │ │ │ ├── state │ │ │ │ ├── index.tsx │ │ │ │ └── options.ts │ │ │ ├── country │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── code-block │ │ │ ├── index.tsx │ │ │ ├── config.ts │ │ │ ├── copy-button.tsx │ │ │ └── index.client.tsx │ │ ├── cta-block │ │ │ ├── index.tsx │ │ │ └── config.ts │ │ ├── banner-block │ │ │ ├── config.ts │ │ │ └── index.tsx │ │ ├── related-posts.tsx │ │ ├── render-blocks.tsx │ │ ├── content-block │ │ │ ├── index.tsx │ │ │ └── config.ts │ │ └── archive-block │ │ │ ├── index.tsx │ │ │ └── config.ts │ ├── theme-provider.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── card.tsx │ │ ├── button.tsx │ │ ├── pagination.tsx │ │ └── select.tsx │ ├── heros │ │ ├── render-hero.tsx │ │ ├── low-impact.tsx │ │ ├── medium-impact.tsx │ │ ├── high-impact.tsx │ │ ├── config.ts │ │ └── post-hero.tsx │ ├── theme-toggle.tsx │ └── layout.tsx ├── collections │ ├── fields │ │ ├── slug │ │ │ ├── index.scss │ │ │ ├── formatSlug.ts │ │ │ ├── index.ts │ │ │ └── slug-component.tsx │ │ ├── linkGroup.ts │ │ ├── defaultLexical.ts │ │ └── link.ts │ ├── Users │ │ └── index.ts │ ├── Categories.ts │ ├── Posts │ │ ├── hooks │ │ │ ├── populateAuthors.ts │ │ │ └── revalidatePost.ts │ │ └── index.ts │ ├── Pages │ │ ├── hooks │ │ │ └── revalidatePage.ts │ │ └── index.ts │ └── Media.ts ├── site.config.ts └── payload.config.ts ├── .npmrc ├── public ├── twitter-image.jpg └── opengraph-image.jpg ├── .prettierrc.json ├── postcss.config.js ├── .prettierignore ├── .gitignore ├── next-env.d.ts ├── components.json ├── .env.example ├── redirects.js ├── next-sitemap.config.cjs ├── next.config.js ├── eslint.config.mjs ├── tsconfig.json └── package.json /src/app/(payload)/custom.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | enable-pre-post-scripts=true 3 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brijr/payload-site-starter/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/twitter-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brijr/payload-site-starter/HEAD/public/twitter-image.jpg -------------------------------------------------------------------------------- /public/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brijr/payload-site-starter/HEAD/public/opengraph-image.jpg -------------------------------------------------------------------------------- /src/lib/access/anyone.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload' 2 | 3 | export const anyone: Access = () => true 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "semi": false 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | 7 | export default config 8 | -------------------------------------------------------------------------------- /src/lib/utilities/canUseDOM.ts: -------------------------------------------------------------------------------- 1 | export default !!(typeof window !== 'undefined' && window.document && window.document.createElement) 2 | -------------------------------------------------------------------------------- /src/app/(frontend)/page.tsx: -------------------------------------------------------------------------------- 1 | import PageTemplate, { generateMetadata } from './[slug]/page' 2 | 3 | export default PageTemplate 4 | 5 | export { generateMetadata } 6 | -------------------------------------------------------------------------------- /src/components/site/admin-bar/index.scss: -------------------------------------------------------------------------------- 1 | @import '~@payloadcms/ui/scss'; 2 | 3 | .admin-bar { 4 | @include small-break { 5 | display: none; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utilities/toKebabCase.ts: -------------------------------------------------------------------------------- 1 | export const toKebabCase = (string: string): string => 2 | string 3 | ?.replace(/([a-z])([A-Z])/g, '$1-$2') 4 | .replace(/\s+/g, '-') 5 | .toLowerCase() 6 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/payload-types.ts 2 | .tmp 3 | **/.git 4 | **/.hg 5 | **/.pnp.* 6 | **/.svn 7 | **/.yarn/** 8 | **/build 9 | **/dist/** 10 | **/node_modules 11 | **/temp 12 | **/docs/** 13 | tsconfig.json 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist / media 3 | node_modules 4 | .DS_Store 5 | .env 6 | .next 7 | .vercel 8 | 9 | # Payload default media upload directory 10 | public/media/ 11 | 12 | public/robots.txt 13 | public/sitemap*.xml 14 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/app/(frontend)/next/exit-preview/route.ts: -------------------------------------------------------------------------------- 1 | import { draftMode } from 'next/headers' 2 | 3 | export async function GET(): Promise { 4 | const draft = await draftMode() 5 | draft.disable() 6 | return new Response('Draft mode is disabled') 7 | } 8 | -------------------------------------------------------------------------------- /src/collections/fields/slug/index.scss: -------------------------------------------------------------------------------- 1 | .slug-field-component { 2 | .label-wrapper { 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | 8 | .lock-button { 9 | margin: 0; 10 | padding-bottom: 0.3125rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/access/authenticated.ts: -------------------------------------------------------------------------------- 1 | import type { AccessArgs } from 'payload' 2 | 3 | import type { User } from '@/payload-types' 4 | 5 | type isAuthenticated = (args: AccessArgs) => boolean 6 | 7 | export const authenticated: isAuthenticated = ({ req: { user } }) => { 8 | return Boolean(user) 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/access/authenticatedOrPublished.ts: -------------------------------------------------------------------------------- 1 | import type { Access } from 'payload' 2 | 3 | export const authenticatedOrPublished: Access = ({ req: { user } }) => { 4 | if (user) { 5 | return true 6 | } 7 | 8 | return { 9 | _status: { 10 | equals: 'published', 11 | }, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/blocks/media-block/config.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from 'payload' 2 | 3 | export const MediaBlockConfig: Block = { 4 | slug: 'mediaBlock', 5 | interfaceName: 'MediaBlock', 6 | fields: [ 7 | { 8 | name: 'media', 9 | type: 'upload', 10 | relationTo: 'media', 11 | required: true, 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /src/app/(payload)/api/graphql-playground/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes' 6 | 7 | export const GET = GRAPHQL_PLAYGROUND_GET(config) 8 | -------------------------------------------------------------------------------- /src/lib/hooks/revalidateRedirects.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionAfterChangeHook } from 'payload' 2 | 3 | import { revalidateTag } from 'next/cache' 4 | 5 | export const revalidateRedirects: CollectionAfterChangeHook = ({ doc, req: { payload } }) => { 6 | payload.logger.info(`Revalidating redirects`) 7 | 8 | revalidateTag('redirects') 9 | 10 | return doc 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(payload)/api/graphql/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes' 5 | 6 | export const POST = GRAPHQL_POST(config) 7 | 8 | export const OPTIONS = REST_OPTIONS(config) 9 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children} 11 | } 12 | -------------------------------------------------------------------------------- /src/components/blocks/form-block/message/index.tsx: -------------------------------------------------------------------------------- 1 | import RichText from '@/components/site/rich-text' 2 | import React from 'react' 3 | 4 | import { Width } from '../width' 5 | import { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' 6 | 7 | export const Message: React.FC<{ message: SerializedEditorState }> = ({ message }) => { 8 | return {message && } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/site/footer/hooks/revalidateFooter.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalAfterChangeHook } from 'payload' 2 | 3 | import { revalidateTag } from 'next/cache' 4 | 5 | export const revalidateFooter: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => { 6 | if (!context.disableRevalidate) { 7 | payload.logger.info(`Revalidating footer`) 8 | 9 | revalidateTag('global_footer') 10 | } 11 | 12 | return doc 13 | } 14 | -------------------------------------------------------------------------------- /src/components/site/header/hooks/revalidateHeader.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalAfterChangeHook } from 'payload' 2 | 3 | import { revalidateTag } from 'next/cache' 4 | 5 | export const revalidateHeader: GlobalAfterChangeHook = ({ doc, req: { payload, context } }) => { 6 | if (!context.disableRevalidate) { 7 | payload.logger.info(`Revalidating header`) 8 | 9 | revalidateTag('global_header') 10 | } 11 | 12 | return doc 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/hooks/populatePublishedAt.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionBeforeChangeHook } from 'payload' 2 | 3 | export const populatePublishedAt: CollectionBeforeChangeHook = ({ data, operation, req }) => { 4 | if (operation === 'create' || operation === 'update') { 5 | if (req.data && !req.data.publishedAt) { 6 | const now = new Date() 7 | return { 8 | ...data, 9 | publishedAt: now, 10 | } 11 | } 12 | } 13 | 14 | return data 15 | } 16 | -------------------------------------------------------------------------------- /src/components/blocks/form-block/width/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Width: React.FC<{ 4 | children: React.ReactNode 5 | width?: number 6 | }> = ({ children, width }) => { 7 | if (width) { 8 | return ( 9 |
10 |
{children}
11 |
12 | ) 13 | } 14 | 15 | return
{children}
16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utilities/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | export function useDebounce(value: T, delay = 200): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value) 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value) 9 | }, delay) 10 | 11 | return () => { 12 | clearTimeout(handler) 13 | } 14 | }, [value, delay]) 15 | 16 | return debouncedValue 17 | } 18 | -------------------------------------------------------------------------------- /src/components/site/live-preview-listener.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { getClientSideURL } from '@/lib/utilities/getURL' 4 | import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react' 5 | import { useRouter } from 'next/navigation' 6 | import React from 'react' 7 | 8 | export const LivePreviewListener: React.FC = () => { 9 | const router = useRouter() 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /src/components/site/footer/row-label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Header } from '@/payload-types' 3 | import { RowLabelProps, useRowLabel } from '@payloadcms/ui' 4 | 5 | export const RowLabel: React.FC = () => { 6 | const data = useRowLabel[number]>() 7 | 8 | const label = data?.data?.link?.label 9 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}` 10 | : 'Row' 11 | 12 | return
{label}
13 | } 14 | -------------------------------------------------------------------------------- /src/components/site/header/row-label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Header } from '@/payload-types' 3 | import { RowLabelProps, useRowLabel } from '@payloadcms/ui' 4 | 5 | export const RowLabel: React.FC = () => { 6 | const data = useRowLabel[number]>() 7 | 8 | const label = data?.data?.link?.label 9 | ? `Nav item ${data.rowNumber !== undefined ? data.rowNumber + 1 : ''}: ${data?.data?.link?.label}` 10 | : 'Row' 11 | 12 | return
{label}
13 | } 14 | -------------------------------------------------------------------------------- /src/site.config.ts: -------------------------------------------------------------------------------- 1 | type Config = { 2 | name: string 3 | url: string 4 | description: string 5 | logo: { 6 | path: string 7 | width: number 8 | height: number 9 | } 10 | } 11 | 12 | export const config: Config = { 13 | name: 'Payload Site Starter ✴︎', 14 | url: 'https://payload-site-starter.vercel.app', 15 | description: 'Opinionated starter for building websites with Payload and Next.js', 16 | logo: { 17 | path: '/logo.svg', 18 | width: 120, 19 | height: 22.02, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/(frontend)/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Database Connection 2 | DATABASE_URI=postgresql://username:password@hostname/database?sslmode=require 3 | 4 | # Application Secrets 5 | PAYLOAD_SECRET=your_payload_secret_here 6 | NEXT_PUBLIC_SERVER_URL=http://localhost:3000 7 | CRON_SECRET=your_cron_secret_here 8 | PREVIEW_SECRET=your_preview_secret_here 9 | 10 | # R2 Storage Configuration 11 | R2_ACCESS_KEY_ID=your_r2_access_key_id 12 | R2_SECRET_ACCESS_KEY=your_r2_secret_access_key 13 | R2_BUCKET=your_bucket_name 14 | R2_ENDPOINT=https://your-r2-endpoint.cloudflarestorage.com 15 | -------------------------------------------------------------------------------- /redirects.js: -------------------------------------------------------------------------------- 1 | const redirects = async () => { 2 | const internetExplorerRedirect = { 3 | destination: '/ie-incompatible.html', 4 | has: [ 5 | { 6 | type: 'header', 7 | key: 'user-agent', 8 | value: '(.*Trident.*)', // all ie browsers 9 | }, 10 | ], 11 | permanent: false, 12 | source: '/:path((?!ie-incompatible.html$).*)', // all pages except the incompatibility page 13 | } 14 | 15 | const redirects = [internetExplorerRedirect] 16 | 17 | return redirects 18 | } 19 | 20 | export default redirects 21 | -------------------------------------------------------------------------------- /src/components/blocks/code-block/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Code } from './index.client' 4 | 5 | export type CodeBlockProps = { 6 | code: string 7 | language?: string 8 | blockType: 'code' 9 | } 10 | 11 | type Props = CodeBlockProps & { 12 | className?: string 13 | } 14 | 15 | export const CodeBlock: React.FC = ({ className, code, language }) => { 16 | return ( 17 |
18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/utilities/mergeOpenGraph.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { config } from '@/site.config' 3 | 4 | const defaultOpenGraph: Metadata['openGraph'] = { 5 | type: 'website', 6 | description: config.description, 7 | siteName: config.name, 8 | title: config.name, 9 | images: [`${config.url}/opengraph-image.jpg`], 10 | } 11 | 12 | export const mergeOpenGraph = (og?: Metadata['openGraph']): Metadata['openGraph'] => { 13 | return { 14 | ...defaultOpenGraph, 15 | ...og, 16 | images: og?.images ? og.images : defaultOpenGraph.images, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(frontend)/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | import { Section, Container } from '@/components/layout' 5 | import { Button } from '@/components/ui/button' 6 | 7 | export default function NotFound() { 8 | return ( 9 |
10 | 11 |
12 |

404

13 |

This page could not be found.

14 |
15 | 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/blocks/form-block/fields.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from './checkbox' 2 | import { Country } from './country' 3 | import { Email } from './email' 4 | import { Message } from './message' 5 | import { Number } from './number' 6 | import { Select } from './select' 7 | import { State } from './state' 8 | import { Text } from './text' 9 | import { Textarea } from './textarea' 10 | 11 | export const fields = { 12 | checkbox: Checkbox, 13 | country: Country, 14 | email: Email, 15 | message: Message, 16 | number: Number, 17 | select: Select, 18 | state: State, 19 | text: Text, 20 | textarea: Textarea, 21 | } 22 | -------------------------------------------------------------------------------- /next-sitemap.config.cjs: -------------------------------------------------------------------------------- 1 | const SITE_URL = 2 | process.env.NEXT_PUBLIC_SERVER_URL || 3 | process.env.VERCEL_PROJECT_PRODUCTION_URL || 4 | 'https://example.com' 5 | 6 | /** @type {import('next-sitemap').IConfig} */ 7 | module.exports = { 8 | siteUrl: SITE_URL, 9 | generateRobotsTxt: true, 10 | exclude: ['/posts-sitemap.xml', '/pages-sitemap.xml', '/*', '/posts/*'], 11 | robotsTxtOptions: { 12 | policies: [ 13 | { 14 | userAgent: '*', 15 | disallow: '/admin/*', 16 | }, 17 | ], 18 | additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/collections/Users/index.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | import { authenticated } from '@/lib/access/authenticated' 4 | 5 | export const Users: CollectionConfig = { 6 | slug: 'users', 7 | access: { 8 | admin: authenticated, 9 | create: authenticated, 10 | delete: authenticated, 11 | read: authenticated, 12 | update: authenticated, 13 | }, 14 | admin: { 15 | defaultColumns: ['name', 'email'], 16 | useAsTitle: 'name', 17 | }, 18 | auth: true, 19 | fields: [ 20 | { 21 | name: 'name', 22 | type: 'text', 23 | }, 24 | ], 25 | timestamps: true, 26 | } 27 | -------------------------------------------------------------------------------- /src/app/(payload)/api/[...slug]/route.ts: -------------------------------------------------------------------------------- 1 | /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ 2 | /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ 3 | import config from '@payload-config' 4 | import '@payloadcms/next/css' 5 | import { 6 | REST_DELETE, 7 | REST_GET, 8 | REST_OPTIONS, 9 | REST_PATCH, 10 | REST_POST, 11 | REST_PUT, 12 | } from '@payloadcms/next/routes' 13 | 14 | export const GET = REST_GET(config) 15 | export const POST = REST_POST(config) 16 | export const DELETE = REST_DELETE(config) 17 | export const PATCH = REST_PATCH(config) 18 | 19 | export const PUT = REST_PUT(config) 20 | export const OPTIONS = REST_OPTIONS(config) 21 | -------------------------------------------------------------------------------- /src/collections/Categories.ts: -------------------------------------------------------------------------------- 1 | import type { CollectionConfig } from 'payload' 2 | 3 | import { anyone } from '@/lib/access/anyone' 4 | import { authenticated } from '@/lib/access/authenticated' 5 | import { slugField } from '@/collections/fields/slug' 6 | 7 | export const Categories: CollectionConfig = { 8 | slug: 'categories', 9 | access: { 10 | create: authenticated, 11 | delete: authenticated, 12 | read: anyone, 13 | update: authenticated, 14 | }, 15 | admin: { 16 | useAsTitle: 'title', 17 | }, 18 | fields: [ 19 | { 20 | name: 'title', 21 | type: 'text', 22 | required: true, 23 | }, 24 | ...slugField(), 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils' 4 | import * as LabelPrimitive from '@radix-ui/react-label' 5 | import { type VariantProps, cva } from 'class-variance-authority' 6 | import * as React from 'react' 7 | 8 | const labelVariants = cva( 9 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 10 | ) 11 | 12 | const Label: React.FC< 13 | { ref?: React.Ref } & React.ComponentProps & 14 | VariantProps 15 | > = ({ className, ref, ...props }) => ( 16 | 17 | ) 18 | 19 | export { Label } 20 | -------------------------------------------------------------------------------- /src/lib/utilities/generatePreviewPath.ts: -------------------------------------------------------------------------------- 1 | import { PayloadRequest, CollectionSlug } from 'payload' 2 | 3 | const collectionPrefixMap: Partial> = { 4 | posts: '/posts', 5 | pages: '', 6 | } 7 | 8 | type Props = { 9 | collection: keyof typeof collectionPrefixMap 10 | slug: string 11 | req: PayloadRequest 12 | } 13 | 14 | export const generatePreviewPath = ({ collection, slug }: Props) => { 15 | const encodedParams = new URLSearchParams({ 16 | slug, 17 | collection, 18 | path: `${collectionPrefixMap[collection]}/${slug}`, 19 | previewSecret: process.env.PREVIEW_SECRET || '', 20 | }) 21 | 22 | const url = `/next/preview?${encodedParams.toString()}` 23 | 24 | return url 25 | } 26 | -------------------------------------------------------------------------------- /src/collections/fields/slug/formatSlug.ts: -------------------------------------------------------------------------------- 1 | import type { FieldHook } from 'payload' 2 | 3 | export const formatSlug = (val: string): string => 4 | val 5 | .replace(/ /g, '-') 6 | .replace(/[^\w-]+/g, '') 7 | .toLowerCase() 8 | 9 | export const formatSlugHook = 10 | (fallback: string): FieldHook => 11 | ({ data, operation, value }) => { 12 | if (typeof value === 'string') { 13 | return formatSlug(value) 14 | } 15 | 16 | if (operation === 'create' || !data?.slug) { 17 | const fallbackData = data?.[fallback] || data?.[fallback] 18 | 19 | if (fallbackData && typeof fallbackData === 'string') { 20 | return formatSlug(fallbackData) 21 | } 22 | } 23 | 24 | return value 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/hooks/formatSlug.ts: -------------------------------------------------------------------------------- 1 | import type { FieldHook } from 'payload' 2 | 3 | const format = (val: string): string => 4 | val 5 | .replace(/ /g, '-') 6 | .replace(/[^\w-]+/g, '') 7 | .toLowerCase() 8 | 9 | const formatSlug = 10 | (fallback: string): FieldHook => 11 | ({ data, operation, originalDoc, value }) => { 12 | if (typeof value === 'string') { 13 | return format(value) 14 | } 15 | 16 | if (operation === 'create') { 17 | const fallbackData = data?.[fallback] || originalDoc?.[fallback] 18 | 19 | if (fallbackData && typeof fallbackData === 'string') { 20 | return format(fallbackData) 21 | } 22 | } 23 | 24 | return value 25 | } 26 | 27 | export default formatSlug 28 | -------------------------------------------------------------------------------- /src/components/site/media/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | 3 | import { ImageMedia } from './image-media' 4 | import { VideoMedia } from './video-media' 5 | 6 | import type { Props } from './types' 7 | 8 | export const Media: React.FC = (props) => { 9 | const { className, htmlElement = 'div', resource } = props 10 | 11 | const isVideo = typeof resource === 'object' && resource?.mimeType?.includes('video') 12 | const Tag = htmlElement || Fragment 13 | 14 | return ( 15 | 22 | {isVideo ? : } 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/components/heros/render-hero.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import type { Page } from '@/payload-types' 4 | 5 | import { HighImpactHero } from '@/components/heros/high-impact' 6 | import { LowImpactHero } from '@/components/heros/low-impact' 7 | import { MediumImpactHero } from '@/components/heros/medium-impact' 8 | 9 | const heroes = { 10 | highImpact: HighImpactHero, 11 | lowImpact: LowImpactHero, 12 | mediumImpact: MediumImpactHero, 13 | } 14 | 15 | export const RenderHero: React.FC = (props) => { 16 | const { type } = props || {} 17 | 18 | if (!type || type === 'none') return null 19 | 20 | const HeroToRender = heroes[type] 21 | 22 | if (!HeroToRender) return null 23 | 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/utilities/generateMeta.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | 3 | import type { Page, Post } from '@/payload-types' 4 | 5 | import { mergeOpenGraph } from './mergeOpenGraph' 6 | import { config } from '@/site.config' 7 | 8 | export const generateMeta = async (args: { 9 | doc: Partial | Partial | null 10 | }): Promise => { 11 | const { doc } = args 12 | 13 | const title = doc?.meta?.title ? doc?.meta?.title + ' | ' + config.name : config.name 14 | 15 | return { 16 | description: doc?.meta?.description, 17 | openGraph: mergeOpenGraph({ 18 | description: doc?.meta?.description || '', 19 | title, 20 | url: Array.isArray(doc?.slug) ? doc?.slug.join('/') : '/', 21 | }), 22 | title, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/blocks/code-block/config.ts: -------------------------------------------------------------------------------- 1 | import type { Block } from 'payload' 2 | 3 | export const CodeBlockConfig: Block = { 4 | slug: 'code', 5 | interfaceName: 'CodeBlock', 6 | fields: [ 7 | { 8 | name: 'language', 9 | type: 'select', 10 | defaultValue: 'typescript', 11 | options: [ 12 | { 13 | label: 'Typescript', 14 | value: 'typescript', 15 | }, 16 | { 17 | label: 'Javascript', 18 | value: 'javascript', 19 | }, 20 | { 21 | label: 'CSS', 22 | value: 'css', 23 | }, 24 | ], 25 | }, 26 | { 27 | name: 'code', 28 | type: 'code', 29 | label: false, 30 | required: true, 31 | }, 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /src/collections/fields/linkGroup.ts: -------------------------------------------------------------------------------- 1 | import type { ArrayField, Field } from 'payload' 2 | 3 | import type { LinkAppearances } from './link' 4 | 5 | import deepMerge from '@/lib/utilities/deepMerge' 6 | import { link } from './link' 7 | 8 | type LinkGroupType = (options?: { 9 | appearances?: LinkAppearances[] | false 10 | overrides?: Partial 11 | }) => Field 12 | 13 | export const linkGroup: LinkGroupType = ({ appearances, overrides = {} } = {}) => { 14 | const generatedLinkGroup: Field = { 15 | name: 'links', 16 | type: 'array', 17 | fields: [ 18 | link({ 19 | appearances, 20 | }), 21 | ], 22 | admin: { 23 | initCollapsed: true, 24 | }, 25 | } 26 | 27 | return deepMerge(generatedLinkGroup, overrides) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import * as React from 'react' 3 | 4 | const Textarea: React.FC< 5 | { 6 | ref?: React.Ref 7 | } & React.TextareaHTMLAttributes 8 | > = ({ className, ref, ...props }) => { 9 | return ( 10 |