diff --git a/apps/payload/.env.example b/apps/payload/.env.example new file mode 100644 index 0000000..8eac48f --- /dev/null +++ b/apps/payload/.env.example @@ -0,0 +1,17 @@ +# Database connection string +DATABASE_URI=mongodb://127.0.0.1/your-database-name + +# Or use a PG connection string +#DATABASE_URI=postgresql://127.0.0.1:5432/your-database-name + +# Used to encrypt JWT tokens +PAYLOAD_SECRET=YOUR_SECRET_HERE + +# Used to configure CORS, format links and more. No trailing slash +NEXT_PUBLIC_SERVER_URL=http://localhost:3000 + +# Secret used to authenticate cron jobs +CRON_SECRET=YOUR_CRON_SECRET_HERE + +# Used to validate preview requests +PREVIEW_SECRET=YOUR_SECRET_HERE diff --git a/apps/payload/.eslintrc.js b/apps/payload/.eslintrc.js new file mode 100644 index 0000000..3541483 --- /dev/null +++ b/apps/payload/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + root: true, + extends: ["@shared/eslint-config", "plugin:tailwindcss/recommended"], + plugins: ["tailwindcss"], + rules: { + "tailwindcss/no-custom-classname": "off", + "tailwindcss/classnames-order": "off", + }, + settings: { + tailwindcss: { + callees: ["cn", "cva"], + config: "tailwind.config.ts", + }, + }, +}; diff --git a/apps/payload/.gitignore b/apps/payload/.gitignore new file mode 100644 index 0000000..071bc4f --- /dev/null +++ b/apps/payload/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +build +dist / media + +# misc +.DS_Store +.vscode +.next +.vercel + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env.local + +# Payload default media upload directory +public/media/ + +public/robots.txt +public/sitemap*.xml diff --git a/apps/payload/README.md b/apps/payload/README.md new file mode 100644 index 0000000..014b77b --- /dev/null +++ b/apps/payload/README.md @@ -0,0 +1 @@ +# payload-next.js-boilerplate diff --git a/apps/payload/next-env.d.ts b/apps/payload/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/apps/payload/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/payload/next-sitemap.config.cjs b/apps/payload/next-sitemap.config.cjs new file mode 100644 index 0000000..db945b0 --- /dev/null +++ b/apps/payload/next-sitemap.config.cjs @@ -0,0 +1,20 @@ +const SITE_URL = + process.env.NEXT_PUBLIC_SERVER_URL || + process.env.VERCEL_PROJECT_PRODUCTION_URL || + "https://example.com"; + +/** @type {import('next-sitemap').IConfig} */ +module.exports = { + siteUrl: SITE_URL, + generateRobotsTxt: true, + exclude: ["/pages-sitemap.xml", "/*"], + robotsTxtOptions: { + policies: [ + { + userAgent: "*", + disallow: "/admin/*", + }, + ], + additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`], + }, +}; diff --git a/apps/payload/next.config.js b/apps/payload/next.config.js new file mode 100644 index 0000000..69b2ee9 --- /dev/null +++ b/apps/payload/next.config.js @@ -0,0 +1,39 @@ +import { withPayload } from "@payloadcms/next/withPayload"; + +const extensionAlias = { + ".cjs": [".cts", ".cjs"], + ".js": [".ts", ".tsx", ".js", ".jsx"], + ".mjs": [".mts", ".mjs"], +}; + +const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL + ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` + : undefined || process.env.__NEXT_PRIVATE_ORIGIN || "http://localhost:3000"; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + ...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => { + const url = new URL(item); + + return { + hostname: url.hostname, + protocol: url.protocol.replace(":", ""), + }; + }), + ], + }, + webpack: (webpackConfig) => { + webpackConfig.resolve.extensionAlias = extensionAlias; + return webpackConfig; + }, + reactStrictMode: true, + + turbopack: { + resolveAlias: extensionAlias, + resolveExtensions: [".ts", ".tsx", ".js", ".jsx", ".json"], + }, +}; + +export default withPayload(nextConfig, { devBundleServerPackages: false }); diff --git a/apps/payload/package.json b/apps/payload/package.json new file mode 100644 index 0000000..beabcbe --- /dev/null +++ b/apps/payload/package.json @@ -0,0 +1,67 @@ +{ + "name": "payload", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "build": "next build", + "postbuild": "next-sitemap --config next-sitemap.config.cjs", + "dev": "next dev --turbopack --port=3000", + "dev:prod": "rm -rf .next && pnpm build && pnpm start", + "generate:importmap": "payload generate:importmap", + "generate:types": "payload generate:types", + "ii": "pnpm --ignore-workspace install", + "lint": "next lint", + "lint:fix": "next lint --fix", + "payload": "PAYLOAD_CONFIG_PATH=/src/lib/payload/payload.config.ts payload", + "reinstall": "rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install", + "start": "next start" + }, + "dependencies": { + "@payloadcms/db-postgres": "3.54.0", + "@payloadcms/live-preview-react": "3.54.0", + "@payloadcms/next": "3.54.0", + "@payloadcms/payload-cloud": "3.54.0", + "@payloadcms/plugin-seo": "3.54.0", + "@payloadcms/richtext-lexical": "3.54.0", + "@payloadcms/ui": "3.54.0", + "@shared/ui": "workspace:*", + "cross-env": "^7.0.3", + "dotenv": "16.4.7", + "next": "15.4.4", + "next-sitemap": "^4.2.3", + "payload": "3.54.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "sharp": "0.34.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@shared/eslint-config": "workspace:*", + "@shared/tailwind-config": "workspace:*", + "@shared/ts-config": "workspace:*", + "@tailwindcss/typography": "^0.5.13", + "@types/node": "22.5.4", + "@types/react": "19.1.8", + "@types/react-dom": "19.1.6", + "autoprefixer": "^10.4.19", + "eslint": "^9.16.0", + "eslint-config-next": "15.4.4", + "postcss": "^8.4.38", + "prettier": "^3.4.2", + "tailwindcss": "^3.4.3", + "typescript": "5.7.3" + }, + "engines": { + "node": "^18.20.2 || >=20.9.0", + "pnpm": "^9 || ^10" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp", + "esbuild", + "unrs-resolver" + ] + } +} diff --git a/apps/payload/postcss.config.js b/apps/payload/postcss.config.js new file mode 100644 index 0000000..393a10f --- /dev/null +++ b/apps/payload/postcss.config.js @@ -0,0 +1,8 @@ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + +export default config diff --git a/apps/payload/public/favicon.ico b/apps/payload/public/favicon.ico new file mode 100644 index 0000000..8cf8894 Binary files /dev/null and b/apps/payload/public/favicon.ico differ diff --git a/apps/payload/public/favicon.svg b/apps/payload/public/favicon.svg new file mode 100644 index 0000000..d7ccc5a --- /dev/null +++ b/apps/payload/public/favicon.svg @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/apps/payload/public/logo.jpg b/apps/payload/public/logo.jpg new file mode 100644 index 0000000..d4443c3 Binary files /dev/null and b/apps/payload/public/logo.jpg differ diff --git a/apps/payload/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts b/apps/payload/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts new file mode 100644 index 0000000..01b2393 --- /dev/null +++ b/apps/payload/src/app/(frontend)/(sitemaps)/pages-sitemap.xml/route.ts @@ -0,0 +1,68 @@ +import { unstable_cache } from "next/cache"; +import { getServerSideSitemap } from "next-sitemap"; +import { getPayload } from "payload"; + +import config from "@/lib/payload/payload.config"; + +const getPagesSitemap = unstable_cache( + async () => { + const payload = await getPayload({ config }); + const SITE_URL = + process.env.NEXT_PUBLIC_SERVER_URL || + process.env.VERCEL_PROJECT_PRODUCTION_URL || + "https://example.com"; + + const results = await payload.find({ + collection: "pages", + overrideAccess: false, + draft: false, + depth: 0, + limit: 1000, + pagination: false, + where: { + _status: { + equals: "published", + }, + }, + select: { + slug: true, + updatedAt: true, + }, + }); + + const dateFallback = new Date().toISOString(); + + const defaultSitemap = [ + { + loc: `${SITE_URL}/search`, + lastmod: dateFallback, + }, + ]; + + const sitemap = results.docs + ? results.docs + .filter((page) => Boolean(page?.slug)) + .map((page) => { + return { + loc: + page?.slug === "home" + ? `${SITE_URL}/` + : `${SITE_URL}/${page?.slug}`, + lastmod: page.updatedAt || dateFallback, + }; + }) + : []; + + return [...defaultSitemap, ...sitemap]; + }, + ["pages-sitemap"], + { + tags: ["pages-sitemap"], + }, +); + +export async function GET() { + const sitemap = await getPagesSitemap(); + + return getServerSideSitemap(sitemap); +} diff --git a/apps/payload/src/app/(frontend)/[slug]/page.tsx b/apps/payload/src/app/(frontend)/[slug]/page.tsx new file mode 100644 index 0000000..b034c84 --- /dev/null +++ b/apps/payload/src/app/(frontend)/[slug]/page.tsx @@ -0,0 +1,92 @@ +import React, { cache } from "react"; +import type { Metadata } from "next"; +import { draftMode } from "next/headers"; +import { getPayload } from "payload"; + +import configPromise from "@/lib/payload/payload.config"; +import { generateMeta } from "@/lib/utilities/generateMeta"; +import PageContainer from "@/components/Page"; + +export async function generateStaticParams() { + const payload = await getPayload({ config: configPromise }); + const pages = await payload.find({ + collection: "pages", + draft: false, + limit: 1000, + overrideAccess: false, + pagination: false, + select: { + slug: true, + }, + }); + + const params = pages.docs + ?.filter((doc) => { + return doc.slug !== "home"; + }) + .map(({ slug }) => { + return { slug }; + }); + + return params; +} + +type Args = { + params: Promise<{ + slug?: string; + }>; +}; + +export default async function Page({ params: paramsPromise }: Args) { + const { isEnabled: draft } = await draftMode(); + + const { slug = "home" } = await paramsPromise; + const url = "/" + slug; + + const page = await queryPageBySlug({ + slug, + }); + + // TO-DO: add default page template for case when there is no home page + if (!page) { + return ( +
+

EMPTY PAGE

+
+ ); + } + + return ; +} + +export async function generateMetadata({ + params: paramsPromise, +}: Args): Promise { + const { slug = "home" } = await paramsPromise; + const page = await queryPageBySlug({ + slug, + }); + + return generateMeta({ doc: page }); +} + +const queryPageBySlug = cache(async ({ slug }: { slug: string }) => { + const { isEnabled: draft } = await draftMode(); + + const payload = await getPayload({ config: configPromise }); + + const result = await payload.find({ + collection: "pages", + draft, + limit: 1, + pagination: false, + overrideAccess: draft, + where: { + slug: { + equals: slug, + }, + }, + }); + + return result.docs?.[0] || null; +}); diff --git a/apps/payload/src/app/(frontend)/globals.css b/apps/payload/src/app/(frontend)/globals.css new file mode 100644 index 0000000..0018e59 --- /dev/null +++ b/apps/payload/src/app/(frontend)/globals.css @@ -0,0 +1,51 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --section-margin-base: 24px; + --section-margin-large: 72px; + + --section-padding-base: 16px; + --section-padding-large: 72px; +} + +.light { + --bg: #fff; + --text: #222222; + --text-secondary: #666666; + --primary: #4f46e5; + --primary-light: #e4e1ff; + --primary-2: #ff0765; + --primary-2-light: #ffe6f0; +} + +.dark { + --bg: #222222; + --text: #f6f6f6; + --text-secondary: #999999; + --primary: #4f46e5; + --primary-light: #e4e1ff; + --primary-2: #ff0765; + --primary-2-light: #ffe6f0; +} + +.light-gray { + --bg: #f6f6f6; + --text: #222222; + --text-secondary: #666666; + --primary: #4f46e5; + --primary-light: #e4e1ff; + --primary-2: #ff0765; + --primary-2-light: #ffe6f0; +} + +.dark-gray { + --bg: #5b5b5b; + --text: #f6f6f6; + --text-secondary: #bababa; + --primary: #4f46e5; + --primary-light: #e4e1ff; + --primary-2: #ff0765; + --primary-2-light: #ffe6f0; +} diff --git a/apps/payload/src/app/(frontend)/layout.tsx b/apps/payload/src/app/(frontend)/layout.tsx new file mode 100644 index 0000000..4a5db8b --- /dev/null +++ b/apps/payload/src/app/(frontend)/layout.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import type { Metadata } from "next"; + +import { mergeOpenGraph } from "@/lib/utilities/mergeOpenGraph"; + +import "./globals.css"; + +import { getServerSideURL } from "@/lib/utilities/getURL"; +import Footer from "@/components/Footer"; +import Header from "@/components/Header"; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + +
+ {children} +