diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2da257c744c39..414afee868ffa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,8 @@ /apps/studio/ @supabase/Dashboard +/apps/cms/ @supabase/marketing + /apps/www/ @supabase/marketing /apps/www/public/images/blog @supabase/marketing /apps/www/lib/redirects.js diff --git a/apps/cms/.env b/apps/cms/.env index 94f021dce4775..5972f7950940e 100644 --- a/apps/cms/.env +++ b/apps/cms/.env @@ -11,7 +11,5 @@ S3_SECRET_ACCESS_KEY=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda373074 S3_REGION=local S3_ENDPOINT=http://127.0.0.1:34321/storage/v1/s3 -BLOG_APP_URL=http://localhost:3000 -NEXT_PUBLIC_SERVER_URL=http://localhost:3000/blog CRON_SECRET=secret PREVIEW_SECRET=secret \ No newline at end of file diff --git a/apps/cms/next.config.mjs b/apps/cms/next.config.mjs index 8c87e87b2f05e..3317297c82500 100644 --- a/apps/cms/next.config.mjs +++ b/apps/cms/next.config.mjs @@ -19,20 +19,24 @@ const redirects = async () => { return redirects } -const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL - ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` - : undefined || process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000' +const WWW_SITE_ORIGIN = + process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' + ? 'https://supabase.com' + : process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL && + typeof process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL === 'string' + ? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL.replace('cms-git-', 'zone-www-dot-com-git-')}` + : 'http://localhost:3000' /** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [ - ...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => { + ...[WWW_SITE_ORIGIN /* 'https://example.com' */].map((item) => { const url = new URL(item) return { hostname: url.hostname, - protocol: url.protocol.replace(':', ''), + protocol: url.protocol?.replace(':', ''), } }), ], @@ -43,6 +47,12 @@ const nextConfig = { // We are already running linting via GH action, this will skip linting during production build on Vercel ignoreDuringBuilds: true, }, + experimental: { + // Ensure compatibility with Turbopack and Sharp + serverComponentsExternalPackages: ['sharp'], + }, + // Configure Sharp as an external package for server-side rendering + serverExternalPackages: ['sharp'], } export default withPayload(nextConfig, { devBundleServerPackages: false }) diff --git a/apps/cms/package.json b/apps/cms/package.json index ea0d762bd1aff..13e5841a04e9f 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -20,23 +20,26 @@ "typecheck_IGNORED": "tsc --noEmit" }, "dependencies": { - "@payloadcms/admin-bar": "^3.50.0", - "@payloadcms/db-postgres": "^3.50.0", - "@payloadcms/live-preview-react": "^3.50.0", - "@payloadcms/next": "3.50.0", - "@payloadcms/payload-cloud": "3.50.0", - "@payloadcms/plugin-form-builder": "3.50.0", - "@payloadcms/plugin-nested-docs": "3.50.0", - "@payloadcms/plugin-seo": "3.50.0", - "@payloadcms/richtext-lexical": "3.50.0", - "@payloadcms/storage-s3": "3.50.0", - "@payloadcms/ui": "3.50.0", + "@payloadcms/admin-bar": "^3.52.0", + "@payloadcms/db-postgres": "^3.52.0", + "@payloadcms/live-preview-react": "^3.52.0", + "@payloadcms/next": "3.52.0", + "@payloadcms/payload-cloud": "3.52.0", + "@payloadcms/plugin-form-builder": "3.52.0", + "@payloadcms/plugin-nested-docs": "3.52.0", + "@payloadcms/plugin-seo": "3.52.0", + "@payloadcms/richtext-lexical": "3.52.0", + "@payloadcms/storage-s3": "3.52.0", + "@payloadcms/ui": "3.52.0", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.2.3", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", "class-variance-authority": "^0.7.1", - "clsx": "^1.2.1", + "clsx": "^2.1.1", "common": "workspace:*", "config": "workspace:*", "cross-env": "^7.0.3", @@ -45,19 +48,17 @@ "image-size": "2.0.2", "lucide-react": "^0.511.0", "next": "catalog:", - "payload": "3.50.0", + "payload": "3.52.0", "pg": "^8.16.3", "prism-react-renderer": "^2.3.1", "react": "catalog:", "react-dom": "catalog:", "sharp": "0.32.6", - "tailwind-merge": "^1.13.2" - }, - "devDependencies": { - "@types/node": "catalog:", - "@types/react": "catalog:", - "@types/react-dom": "catalog:", + "tailwind-merge": "^1.13.2", "tsx": "^4.19.3", - "typescript": "5.7.3" + "typescript": "~5.5.0" + }, + "externals": { + "sharp": "commonjs sharp" } } diff --git a/apps/cms/src/blocks/Code/config.ts b/apps/cms/src/blocks/Code/config.ts index 7b26f805db2ef..98a81114197b8 100644 --- a/apps/cms/src/blocks/Code/config.ts +++ b/apps/cms/src/blocks/Code/config.ts @@ -10,16 +10,40 @@ export const Code: Block = { defaultValue: 'typescript', options: [ { - label: 'Typescript', - value: 'typescript', + label: 'SQL', + value: 'sql', + }, + { + label: 'JSON', + value: 'json', + }, + { + label: 'bash', + value: 'bash', }, { label: 'Javascript', - value: 'javascript', + value: 'js', + }, + { + label: 'Typescript', + value: 'ts', + }, + { + label: 'tsx', + value: 'tsx', + }, + { + label: 'Python', + value: 'py', + }, + { + label: 'kotlin', + value: 'kotlin', }, { - label: 'CSS', - value: 'css', + label: 'yaml', + value: 'yaml', }, ], }, diff --git a/apps/cms/src/collections/Authors.ts b/apps/cms/src/collections/Authors.ts index 44cb98321cd61..b302059b041b9 100644 --- a/apps/cms/src/collections/Authors.ts +++ b/apps/cms/src/collections/Authors.ts @@ -4,6 +4,7 @@ export const Authors: CollectionConfig = { slug: 'authors', admin: { useAsTitle: 'author', + defaultColumns: ['author', 'position', 'author_id'], }, access: { read: () => true, @@ -13,28 +14,54 @@ export const Authors: CollectionConfig = { name: 'author', type: 'text', required: true, + label: 'Author Name', + index: true, }, { name: 'author_id', type: 'text', + required: true, + unique: true, + label: 'Author ID', + admin: { + description: 'Unique identifier for the author', + }, + }, + { + name: 'username', + type: 'text', + label: 'Username', + admin: { + description: 'GitHub/social username', + }, }, { name: 'position', type: 'text', + label: 'Position', + }, + { + name: 'company', + type: 'text', + label: 'Company', + admin: { + description: 'Company name (for external/guest authors)', + }, }, { name: 'author_url', type: 'text', + label: 'Profile URL', + admin: { + description: 'Link to GitHub, Twitter, LinkedIn, etc.', + }, }, { name: 'author_image_url', type: 'upload', relationTo: 'media', required: false, - }, - { - name: 'username', - type: 'text', + label: 'Profile Image', }, ], timestamps: true, diff --git a/apps/cms/src/collections/Categories.ts b/apps/cms/src/collections/Categories.ts index 7fc90e96c2aef..d5b20b6d0e384 100644 --- a/apps/cms/src/collections/Categories.ts +++ b/apps/cms/src/collections/Categories.ts @@ -1,4 +1,5 @@ import type { CollectionConfig } from 'payload' +import { isAdmin } from '../access/isAdmin' export const Categories: CollectionConfig = { slug: 'categories', @@ -6,13 +7,17 @@ export const Categories: CollectionConfig = { useAsTitle: 'name', }, access: { + create: isAdmin, read: () => true, + update: isAdmin, + delete: isAdmin, }, fields: [ { name: 'name', type: 'text', required: true, + index: true, }, ], timestamps: true, diff --git a/apps/cms/src/collections/Customers/index.ts b/apps/cms/src/collections/Customers/index.ts index a7108d200e359..56bf14bf3187c 100644 --- a/apps/cms/src/collections/Customers/index.ts +++ b/apps/cms/src/collections/Customers/index.ts @@ -27,6 +27,7 @@ import { PreviewField, } from '@payloadcms/plugin-seo/fields' import { slugField } from '../../fields/slug/index.ts' +import { WWW_SITE_ORIGIN } from '../../utilities/constants.ts' const industryOptions = [ { label: 'Healthcare', value: 'healthcare' }, @@ -73,16 +74,20 @@ export const Customers: CollectionConfig = { useAsTitle: 'name', defaultColumns: ['name', 'slug', 'updatedAt'], preview: (data) => { - const baseUrl = process.env.BLOG_APP_URL || 'http://localhost:3000' + const baseUrl = WWW_SITE_ORIGIN || 'http://localhost:3000' const isDraft = data?._status === 'draft' return `${baseUrl}/customers/${data?.slug}${isDraft ? '?preview=true' : ''}` }, }, access: { - create: isAuthenticated, - delete: isAuthenticated, - read: isAnyone, - update: isAuthenticated, + // create: isAuthenticated, + // delete: isAuthenticated, + // read: isAnyone, + // update: isAuthenticated, + create: () => false, + delete: () => false, + read: () => false, + update: () => false, }, defaultPopulate: { name: true, @@ -303,9 +308,9 @@ export const Customers: CollectionConfig = { }, versions: { drafts: { - autosave: { - interval: 100, - }, + // autosave: { + // interval: 100, + // }, schedulePublish: true, }, maxPerDoc: 50, diff --git a/apps/cms/src/collections/Events/index.ts b/apps/cms/src/collections/Events/index.ts index ad99e840e2d2d..de909c08e8cac 100644 --- a/apps/cms/src/collections/Events/index.ts +++ b/apps/cms/src/collections/Events/index.ts @@ -28,6 +28,7 @@ import { } from '@payloadcms/plugin-seo/fields' import { slugField } from '../../fields/slug/index.ts' import { timezoneOptions } from '../../utilities/timezones.ts' +import { WWW_SITE_ORIGIN } from '../../utilities/constants.ts' const eventTypeOptions = [ { label: 'Conference', value: 'conference' }, @@ -47,16 +48,20 @@ export const Events: CollectionConfig = { useAsTitle: 'title', defaultColumns: ['title', 'slug', 'updatedAt'], preview: (data) => { - const baseUrl = process.env.BLOG_APP_URL || 'http://localhost:3000' - const isDraft = data?._status === 'draft' - return `${baseUrl}/events/${data?.slug}${isDraft ? '?preview=true' : ''}` + const baseUrl = WWW_SITE_ORIGIN || 'http://localhost:3000' + // Always use the preview route to ensure draft mode is enabled + return `${baseUrl}/api-v2/cms/preview?slug=${data?.slug}&path=events&secret=${process.env.PREVIEW_SECRET || 'secret'}` }, }, access: { - create: isAuthenticated, - delete: isAuthenticated, - read: isAnyone, - update: isAuthenticated, + // create: isAuthenticated, + // delete: isAuthenticated, + // read: isAnyone, + // update: isAuthenticated, + create: () => false, + delete: () => false, + read: () => false, + update: () => false, }, defaultPopulate: { title: true, @@ -393,9 +398,9 @@ export const Events: CollectionConfig = { }, versions: { drafts: { - autosave: { - interval: 100, - }, + // autosave: { + // interval: 100, + // }, schedulePublish: true, }, maxPerDoc: 50, diff --git a/apps/cms/src/collections/Posts/index.ts b/apps/cms/src/collections/Posts/index.ts index 7c20c7988672d..72e8830bb3993 100644 --- a/apps/cms/src/collections/Posts/index.ts +++ b/apps/cms/src/collections/Posts/index.ts @@ -28,6 +28,7 @@ import { PreviewField, } from '@payloadcms/plugin-seo/fields' import { slugField } from '../../fields/slug/index.ts' +import { WWW_SITE_ORIGIN } from '../../utilities/constants.ts' const launchweekOptions = [ { label: '6', value: '6' }, @@ -45,38 +46,11 @@ export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', - defaultColumns: ['title', 'slug', 'updatedAt'], - livePreview: { - url: ({ data }) => { - const baseUrl = process.env.BLOG_APP_URL || 'http://localhost:3000' - // Always use the preview route for live preview to ensure draft mode is enabled - return `${baseUrl}/api/preview?slug=${data?.slug}&secret=${process.env.PREVIEW_SECRET || 'preview-secret'}` - }, - breakpoints: [ - { - label: 'Desktop', - name: 'desktop', - width: 1920, - height: 1080, - }, - { - label: 'Tablet', - name: 'tablet', - width: 768, - height: 1024, - }, - { - label: 'Mobile', - name: 'mobile', - width: 375, - height: 667, - }, - ], - }, + defaultColumns: ['title', 'slug', 'updatedAt', 'publishedAt'], preview: (data) => { - const baseUrl = process.env.BLOG_APP_URL || 'http://localhost:3000' + const baseUrl = WWW_SITE_ORIGIN || 'http://localhost:3000' // Always use the preview route to ensure draft mode is enabled - return `${baseUrl}/api/preview?slug=${data?.slug}&secret=${process.env.PREVIEW_SECRET || 'preview-secret'}` + return `${baseUrl}/api-v2/cms/preview?slug=${data?.slug}&secret=${process.env.PREVIEW_SECRET || 'secret'}` }, }, access: { @@ -99,8 +73,17 @@ export const Posts: CollectionConfig = { name: 'title', type: 'text', required: true, + index: true, }, ...slugField(), + { + name: 'description', + type: 'textarea', + label: 'Description / subtitle', + admin: { + description: 'Appears as subheading in the blog post preview.', + }, + }, { type: 'tabs', tabs: [ @@ -134,29 +117,26 @@ export const Posts: CollectionConfig = { type: 'upload', relationTo: 'media', required: false, + admin: { + description: 'Will show up as the blog post cover. Required.', + }, }, { - name: 'image', - type: 'upload', - relationTo: 'media', - required: false, + name: 'authors', + type: 'relationship', + relationTo: 'authors', + hasMany: true, + admin: { + description: 'Authors must be one or more. Required.', + }, }, { name: 'categories', type: 'relationship', - admin: { - position: 'sidebar', - }, hasMany: true, relationTo: 'categories', - }, - { - name: 'launchweek', - type: 'select', - options: launchweekOptions, admin: { - description: - 'Select a launch week to show launch week summary at the bottom of the blog post.', + description: 'Select only one category. Required.', }, }, { @@ -167,40 +147,29 @@ export const Posts: CollectionConfig = { }, }, { - name: 'date', - type: 'date', + name: 'tags', + type: 'relationship', + relationTo: 'tags', + hasMany: true, admin: { - position: 'sidebar', + description: 'Tags can be one or more. Optional.', }, }, { name: 'toc_depth', type: 'number', - defaultValue: 2, + defaultValue: 3, admin: { - position: 'sidebar', - }, - }, - { - name: 'description', - type: 'textarea', - }, - { - name: 'authors', - type: 'relationship', - relationTo: 'authors', - hasMany: true, - admin: { - position: 'sidebar', + hidden: true, }, }, { - name: 'tags', - type: 'relationship', - relationTo: 'tags', - hasMany: true, + name: 'launchweek', + type: 'select', + options: launchweekOptions, admin: { - position: 'sidebar', + description: + 'Select a launch week to show launch week summary at the bottom of the blog post. Optional.', }, }, ], @@ -217,12 +186,28 @@ export const Posts: CollectionConfig = { }), MetaTitleField({ hasGenerateFn: true, + overrides: { + admin: { + description: 'Defaults to the title of the post, if not set.', + }, + }, }), MetaImageField({ relationTo: 'media', + overrides: { + admin: { + description: 'Defaults to the "thumb" image, if not set.', + }, + }, }), - MetaDescriptionField({}), + MetaDescriptionField({ + overrides: { + admin: { + description: 'Defaults to the description of the post, if not set.', + }, + }, + }), PreviewField({ // if the `generateUrl` function is configured hasGenerateFn: true, @@ -235,6 +220,9 @@ export const Posts: CollectionConfig = { }, ], }, + /** + * "publishedAt" is only internal to cms to determine if the blog post is published or not, but it's not used for sorting blog posts in www + * */ { name: 'publishedAt', type: 'date', @@ -243,10 +231,17 @@ export const Posts: CollectionConfig = { pickerAppearance: 'dayAndTime', }, position: 'sidebar', + hidden: true, }, hooks: { beforeChange: [ ({ siblingData, value }) => { + /** + * Set the "date" field to the current date if user doesn't set it + */ + if (!siblingData.date) { + siblingData.date = new Date() + } if (siblingData._status === 'published' && !value) { return new Date() } @@ -255,6 +250,21 @@ export const Posts: CollectionConfig = { ], }, }, + /** + * "date" is used to determine the chronological order of the blog post in www + */ + { + name: 'date', + type: 'date', + label: 'Blog Post Date', + admin: { + date: { + pickerAppearance: 'dayAndTime', + }, + description: 'This date will determine the chronological order of the blog post. Required.', + position: 'sidebar', + }, + }, ], timestamps: true, hooks: { @@ -264,10 +274,12 @@ export const Posts: CollectionConfig = { }, versions: { drafts: { - autosave: { - interval: 200, - }, - schedulePublish: true, + // NOTE: disabled autosave as it might overload connections if many users are editing at the same time + // autosave: { + // interval: 200, + // }, + // TODO: enable schedulePublish to work with cron job + // schedulePublish: true, }, maxPerDoc: 50, }, diff --git a/apps/cms/src/collections/Tags.ts b/apps/cms/src/collections/Tags.ts index d21dd5a4728bd..c3570155bdd07 100644 --- a/apps/cms/src/collections/Tags.ts +++ b/apps/cms/src/collections/Tags.ts @@ -13,6 +13,7 @@ export const Tags: CollectionConfig = { name: 'name', type: 'text', required: true, + index: true, }, ], timestamps: true, diff --git a/apps/cms/src/components/BeforeDashboard/SeedButton/index.scss b/apps/cms/src/components/BeforeDashboard/SeedButton/index.scss deleted file mode 100644 index 3ed9bcb109196..0000000000000 --- a/apps/cms/src/components/BeforeDashboard/SeedButton/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -.seedButton { - appearance: none; - background: none; - border: none; - padding: 0; - text-decoration: underline; - - &:hover { - cursor: pointer; - opacity: 0.85; - } -} diff --git a/apps/cms/src/components/BeforeDashboard/SeedButton/index.tsx b/apps/cms/src/components/BeforeDashboard/SeedButton/index.tsx deleted file mode 100644 index 707c183db5aac..0000000000000 --- a/apps/cms/src/components/BeforeDashboard/SeedButton/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -'use client' - -import React, { Fragment, useCallback, useState } from 'react' -import { toast } from '@payloadcms/ui' - -import './index.scss' - -const SuccessMessage: React.FC = () => ( -
- Database seeded! You can now{' '} - - visit your website - -
-) - -export const SeedButton: React.FC = () => { - const [loading, setLoading] = useState(false) - const [seeded, setSeeded] = useState(false) - const [error, setError] = useState(null) - - const handleClick = useCallback( - async (e: React.MouseEvent) => { - e.preventDefault() - - if (seeded) { - toast.info('Database already seeded.') - return - } - if (loading) { - toast.info('Seeding already in progress.') - return - } - if (error) { - toast.error(`An error occurred, please refresh and try again.`) - return - } - - setLoading(true) - - try { - toast.promise( - new Promise((resolve, reject) => { - try { - fetch('/next/seed', { method: 'POST', credentials: 'include' }) - .then((res) => { - if (res.ok) { - resolve(true) - setSeeded(true) - } else { - reject('An error occurred while seeding.') - } - }) - .catch((error) => { - reject(error) - }) - } catch (error) { - reject(error) - } - }), - { - loading: 'Seeding with data....', - success: , - error: 'An error occurred while seeding.', - } - ) - } catch (err) { - const error = err instanceof Error ? err.message : String(err) - setError(error) - } - }, - [loading, seeded, error] - ) - - let message = '' - if (loading) message = ' (seeding...)' - if (seeded) message = ' (done!)' - if (error) message = ` (error: ${error})` - - return ( - - - {message} - - ) -} diff --git a/apps/cms/src/components/BeforeDashboard/index.tsx b/apps/cms/src/components/BeforeDashboard/index.tsx index 907eae47fd6dc..78bb10241e6db 100644 --- a/apps/cms/src/components/BeforeDashboard/index.tsx +++ b/apps/cms/src/components/BeforeDashboard/index.tsx @@ -1,7 +1,6 @@ import { Banner } from '@payloadcms/ui/elements/Banner' import React from 'react' -import { SeedButton } from './SeedButton' import './index.scss' const baseClass = 'before-dashboard' @@ -15,7 +14,6 @@ const BeforeDashboard: React.FC = () => { Here's what to do next: