feat(cms): add homepage global and wire frontend to Payload content

- add  global () with required media upload field
- register new global in  and include it in admin live preview
- replace hardcoded frontend landing content with CMS-driven homepage image
- add typed cached utilities for globals/documents + centralized revalidation helpers
- migrate components to new global utility module and remove deprecated  helper
- update  revalidation hook to use shared global revalidation
- refine frontend layout/header and simplify legacy homepage CSS
- regenerate  for new global/schema changes
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-02-15 16:19:57 -03:00
parent 375398af84
commit b955c48091
11 changed files with 122 additions and 136 deletions

View file

@ -18,7 +18,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
<body>
<RefreshRouteOnSave />
<Header />
<main>{children}</main>
{children}
<Footer />
</body>
</html>

View file

@ -1,61 +1,28 @@
import { headers as getHeaders } from 'next/headers.js'
import { getCachedGlobal } from '@/utilities/globals'
import Image from 'next/image'
import { getPayload } from 'payload'
import React from 'react'
import { fileURLToPath } from 'url'
import config from '@/payload.config'
import configPromise from '@payload-config'
import { Media } from '@/payload-types'
import { getCachedDocumentById } from '@/utilities/documents'
export default async function HomePage() {
const headers = await getHeaders()
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const { user } = await payload.auth({ headers })
const payload = await getCachedGlobal('home', 2)()
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
let image: Media
if (typeof payload.image === 'number') {
image = await getCachedDocumentById('media', `${payload.image}`)()
} else {
image = payload.image
}
return (
<div className="home">
<div className="content">
<picture>
<source srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-favicon.svg" />
<Image
alt="Payload Logo"
height={65}
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-favicon.svg"
width={65}
/>
</picture>
{!user && <h1>Boas-vindas ao seu novo projeto.</h1>}
{user && <h1>Que bom ver você de volta, {user.email}.</h1>}
<div className="links">
<a
className="admin"
href={payloadConfig.routes.admin}
rel="noopener noreferrer"
target="_blank"
>
Ir para o painel administrativo
</a>
<a
className="docs"
href="https://payloadcms.com/docs"
rel="noopener noreferrer"
target="_blank"
>
Documentação
</a>
</div>
</div>
<div className="footer">
<p>Atualize esta página editando</p>
<a className="codeLink" href={fileURL}>
<code>app/(frontend)/page.tsx</code>
</a>
</div>
<div className="mt-4 rounded-md border border-white bg-white px-3 py-1 text-sm text-black">
Tailwind está ativo: as classes utilitárias estão sendo aplicadas.
</div>
</div>
// Main element occupies the whole screen width and the height of screen minus height of header
<main className="w-screen h-screen overflow-hidden">
{image?.url ? (
<Image src={image.url} alt={image.alt} width={image.width!} height={image.height!} />
) : null}
</main>
)
}

View file

@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
@source "../../**/*.{js,ts,jsx,tsx,mdx}";
:root {
@ -92,76 +92,6 @@ svg {
vertical-align: middle;
}
.home {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100vh;
padding: 45px;
max-width: 1024px;
margin: 0 auto;
overflow: hidden;
@media (max-width: 400px) {
padding: 24px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
h1 {
text-align: center;
}
}
.links {
display: flex;
align-items: center;
gap: 12px;
a {
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.admin {
color: rgb(0, 0, 0);
background: rgb(1000, 1000, 1000);
border: 1px solid rgb(0, 0, 0);
}
.docs {
color: rgb(1000, 1000, 1000);
background: rgb(0, 0, 0);
border: 1px solid rgb(1000, 1000, 1000);
}
}
.footer {
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 1024px) {
flex-direction: column;
gap: 6px;
}
p {
margin: 0;
}
.codeLink {
text-decoration: none;
padding: 0 0.5rem;
background: rgb(60, 60, 60);
border-radius: 4px;
}
}
button {
cursor: pointer;
}

View file

@ -1,13 +1,11 @@
import { getCachedGlobal } from '@/utilities/getGlobals'
import { getCachedGlobal } from '@/utilities/globals'
import { SocialLinks } from './socialLinks'
export const Header = async () => {
const settings = await getCachedGlobal('site-settings', 2)()
console.log(settings)
return (
<header className="flex items-center justify-between p-4">
<header className="flex items-center justify-between p-4 absolute top-0 left-0 right-0 bg-black">
{settings.headerTitle ? <h2>{settings.headerTitle}</h2> : null}
<SocialLinks ariaLabel="Redes sociais" />
</header>

View file

@ -1,4 +1,4 @@
import { getCachedGlobal } from '@/utilities/getGlobals'
import { getCachedGlobal } from '@/utilities/globals'
import Link from 'next/link'
type IconProps = {

30
src/globals/HomePage.ts Normal file
View file

@ -0,0 +1,30 @@
import { revalidateGlobal } from '@/utilities/globals'
import { GlobalConfig } from 'payload'
const slug = 'home'
const revalidate = async () => revalidateGlobal(slug)
export const HomePage: GlobalConfig = {
slug,
label: 'Pagina Inicial',
admin: {
group: 'Paginas',
},
access: {
read: () => true,
update: ({ req: { user } }) => Boolean(user),
},
hooks: {
afterChange: [revalidate],
},
fields: [
{
name: 'image',
type: 'upload',
label: 'Imagem',
relationTo: 'media',
required: true,
},
],
}

View file

@ -1,10 +1,13 @@
import { revalidateGlobal } from '@/utilities/globals'
import { revalidateTag } from 'next/cache'
import type { GlobalConfig } from 'payload'
const revalidate = async () => revalidateTag('global_site-settings')
const slug = 'site-settings'
const revalidate = async () => revalidateGlobal(slug)
export const SiteSettings: GlobalConfig = {
slug: 'site-settings',
slug,
label: 'Configurações do site',
admin: {
group: 'Configurações',

View file

@ -91,9 +91,11 @@ export interface Config {
fallbackLocale: null;
globals: {
'site-settings': SiteSetting;
home: Home;
};
globalsSelect: {
'site-settings': SiteSettingsSelect<false> | SiteSettingsSelect<true>;
home: HomeSelect<false> | HomeSelect<true>;
};
locale: null;
user: User;
@ -473,6 +475,16 @@ export interface SiteSetting {
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "home".
*/
export interface Home {
id: number;
image: number | Media;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "site-settings_select".
@ -493,6 +505,16 @@ export interface SiteSettingsSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "home_select".
*/
export interface HomeSelect<T extends boolean = true> {
image?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".

View file

@ -9,6 +9,7 @@ import { Users } from './collections/Users'
import { Media } from './collections/Media'
import { SiteSettings } from './globals/SiteSettings'
import { pt } from 'payload/i18n/pt'
import { HomePage } from './globals/HomePage'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@ -21,11 +22,11 @@ export default buildConfig({
},
livePreview: {
url: 'http://localhost:3000',
globals: ['site-settings'],
globals: ['site-settings', 'home'],
},
},
collections: [Users, Media],
globals: [SiteSettings],
globals: [SiteSettings, HomePage],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {

View file

@ -0,0 +1,32 @@
import { Config } from '@/payload-types'
import configPromise from '@payload-config'
import { revalidateTag } from 'next/cache'
import { unstable_cache } from 'next/cache'
import { getPayload } from 'payload'
type Collection = keyof Config['collections']
export const getDocumentById = <Slug extends Collection>(
collection: Slug,
id: string,
depth?: number,
) =>
getPayload({ config: configPromise }).then((payload) =>
payload.findByID({ collection, id, depth }),
)
const getTag = <Slug extends Collection>(collection: Slug, id: string) =>
`document_${collection}:${id}`
export const getCachedDocumentById = <Slug extends Collection>(
collection: Slug,
id: string,
depth?: number,
) =>
unstable_cache(() => getDocumentById(collection, id, depth), [id], {
tags: [getTag(collection, id)],
})
export const revalidateDocumentById = <Slug extends Collection>(collection: Slug, id: string) =>
revalidateTag(getTag(collection, id))

View file

@ -3,10 +3,11 @@ import type { Config } from 'src/payload-types'
import configPromise from '@payload-config'
import { unstable_cache } from 'next/cache'
import { getPayload } from 'payload'
import { revalidateTag } from 'next/cache'
type Global = keyof Config['globals']
export async function getGlobal(slug: Global, depth = 0) {
export async function getGlobal<Slug extends Global>(slug: Slug, depth = 0) {
const payload = await getPayload({ config: configPromise })
const global = await payload.findGlobal({
@ -20,7 +21,9 @@ export async function getGlobal(slug: Global, depth = 0) {
/**
* Returns a unstable_cache function mapped with the cache tag for the slug
*/
export const getCachedGlobal = (slug: Global, depth = 0) =>
export const getCachedGlobal = <Slug extends Global>(slug: Slug, depth = 0) =>
unstable_cache(async () => getGlobal(slug, depth), [slug], {
tags: [`global_${slug}`],
})
export const revalidateGlobal = <Slug extends Global>(slug: Slug) => revalidateTag(`global_${slug}`)