From 8a45bac798f76a7262153c8822c53e7830ae2cc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 23:54:19 +0000 Subject: [PATCH 1/2] fix: remove lazy loading from LCP images - CookingCTA: add `priority` prop (defaults false) so callers can opt in; home page passes priority={!heroEbook} so the profile image is eagerly loaded when there is no hero section above it - categorias/[slug]: add `priority` to category header image and pass `firstRecipePriority` to RecipesList so the first recipe card (the LCP on category pages) is eagerly loaded Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0136fxwkjZZJMTda5rH9vpar --- src/components/CookingCTA.tsx | 5 +++-- src/pages/categorias/[slug].tsx | 2 ++ src/pages/index.tsx | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/CookingCTA.tsx b/src/components/CookingCTA.tsx index 04a20c2..6cdb305 100644 --- a/src/components/CookingCTA.tsx +++ b/src/components/CookingCTA.tsx @@ -4,9 +4,10 @@ import Link from 'next/link'; type CookingCTAProps = { imageUrl?: string; + priority?: boolean; }; -export function CookingCTA({ imageUrl }: CookingCTAProps) { +export function CookingCTA({ imageUrl, priority = false }: CookingCTAProps) { return (
@@ -47,7 +48,7 @@ export function CookingCTA({ imageUrl }: CookingCTAProps) { fill sizes="(max-width: 640px) 208px, (max-width: 768px) 256px, 320px" className="object-cover" - priority={false} + priority={priority} />
diff --git a/src/pages/categorias/[slug].tsx b/src/pages/categorias/[slug].tsx index 52139bb..1ea3dbc 100644 --- a/src/pages/categorias/[slug].tsx +++ b/src/pages/categorias/[slug].tsx @@ -58,6 +58,7 @@ export default function CategoryPage({ category, recipes, pagination }: Props) { src={category.imagens[0].url} alt={category.nome} fill + priority className="object-cover" /> @@ -83,6 +84,7 @@ export default function CategoryPage({ category, recipes, pagination }: Props) { recipes={recipes} pagination={pagination ?? undefined} variant="compact" + firstRecipePriority />
diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2e10a7c..b3d54ca 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -163,7 +163,7 @@ export default function Home({ )} - + {categoriesWithCounts.length > 0 && (
From 86b4dbcb4809992dba3e90e5a9e7ad2301606cef Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 00:15:15 +0000 Subject: [PATCH 2/2] perf: remove MDX pipeline from client bundle (-255 KB gz) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace next-mdx-remote-client/rsc (an App Router RSC API) with server-side markdown-to-HTML processing using the remark/rehype pipeline already present as a direct dependency. Before: 1217 KB parsed / 398 KB gz (node_modules in client bundle) After: 368 KB parsed / 143 KB gz The entire MDX compile pipeline — next-mdx-remote-client (170 KB gz), acorn JS parser (66 KB gz), acorn-jsx, babel error tools — no longer ships to the browser. Markdown is now converted to HTML in getStaticProps and rendered via dangerouslySetInnerHTML with zero client-side JS overhead. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0136fxwkjZZJMTda5rH9vpar --- package.json | 2 ++ pnpm-lock.yaml | 50 ++++++++++++++++++++++++--- src/components/Markdown.tsx | 20 ++++------- src/methods/markdownToHtml.ts | 13 +++++++ src/pages/conheca-a-lets.tsx | 6 +++- src/pages/ebooks/[slug].tsx | 20 +++++++---- src/pages/politica-de-privacidade.tsx | 5 +-- src/pages/receitas/[slug].tsx | 6 +++- src/pages/termos-de-uso.tsx | 5 +-- 9 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 src/methods/markdownToHtml.ts diff --git a/package.json b/package.json index b36dc67..91aa635 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "react-copy-to-clipboard": "^5.1.1", "react-dom": "19.2.7", "react-share": "^5.3.0", + "rehype-stringify": "^10.0.1", "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", "rss": "^1.2.2", "schema-dts": "^2.0.0", "simple-xml-to-json": "^1.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dad29e..a2d9aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,9 +47,15 @@ importers: react-share: specifier: ^5.3.0 version: 5.3.0(react@19.2.7) + rehype-stringify: + specifier: ^10.0.1 + version: 10.0.1 remark-parse: specifier: ^11.0.0 version: 11.0.0 + remark-rehype: + specifier: ^11.1.2 + version: 11.1.2 rss: specifier: ^1.2.2 version: 1.2.2 @@ -2914,6 +2920,9 @@ packages: hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.0: resolution: {integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==} @@ -2929,6 +2938,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -3961,6 +3973,9 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proto3-json-serializer@3.0.4: resolution: {integrity: sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==} engines: {node: '>=18'} @@ -4075,6 +4090,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + remark-mdx-remove-esm@1.3.2: resolution: {integrity: sha512-BvL8VSdVXy9S7NlHP56nUJAHFc45h5E9HnHiLUGHe5tw3Yvm/3cVZvAzlkEEh2i+fkq2uKrf2xn5VmItBhMypA==} peerDependencies: @@ -4086,8 +4104,8 @@ packages: remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-rehype@11.1.0: - resolution: {integrity: sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==} + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} @@ -6280,7 +6298,7 @@ snapshots: rehype-recma: 1.0.0 remark-mdx: 3.0.1 remark-parse: 11.0.0 - remark-rehype: 11.1.0 + remark-rehype: 11.1.2 source-map: 0.7.4 unified: 11.0.5 unist-util-position-from-estree: 2.0.0 @@ -7923,6 +7941,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.0: dependencies: '@types/estree': 1.0.6 @@ -7955,6 +7987,8 @@ snapshots: html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + http-cache-semantics@4.1.1: {} http-proxy-agent@5.0.0: @@ -9336,6 +9370,8 @@ snapshots: property-information@6.5.0: {} + property-information@7.2.0: {} + proto3-json-serializer@3.0.4: dependencies: protobufjs: 7.6.3 @@ -9501,6 +9537,12 @@ snapshots: transitivePeerDependencies: - supports-color + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + remark-mdx-remove-esm@1.3.2(unified@11.0.5): dependencies: '@types/mdast': 4.0.4 @@ -9523,7 +9565,7 @@ snapshots: transitivePeerDependencies: - supports-color - remark-rehype@11.1.0: + remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 6553715..837b872 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -1,20 +1,12 @@ -import { MDXRemote, type MDXRemoteProps } from 'next-mdx-remote-client/rsc'; - -const components: MDXRemoteProps['components'] = { - // break-words evita overflow horizontal com palavras/URLs longas do CMS; - // tabelas viram blocos roláveis em vez de alargar a página no mobile - wrapper: ({ children }) => ( -
- {children} -
- ), +type MarkdownProps = { + html: string; }; -export function Markdown(props: MDXRemoteProps) { +export function Markdown({ html }: MarkdownProps) { return ( - ); } diff --git a/src/methods/markdownToHtml.ts b/src/methods/markdownToHtml.ts new file mode 100644 index 0000000..8bfa65e --- /dev/null +++ b/src/methods/markdownToHtml.ts @@ -0,0 +1,13 @@ +import { unified } from 'unified'; +import remarkParse from 'remark-parse'; +import remarkRehype from 'remark-rehype'; +import rehypeStringify from 'rehype-stringify'; + +export async function markdownToHtml(markdown: string): Promise { + const result = await unified() + .use(remarkParse) + .use(remarkRehype) + .use(rehypeStringify) + .process(markdown ?? ''); + return String(result); +} diff --git a/src/pages/conheca-a-lets.tsx b/src/pages/conheca-a-lets.tsx index b6cee2f..a893ffc 100644 --- a/src/pages/conheca-a-lets.tsx +++ b/src/pages/conheca-a-lets.tsx @@ -4,6 +4,7 @@ import type { GetStaticProps } from 'next'; import { JsonLd } from 'src/components/JsonLd'; import { Markdown } from 'src/components/Markdown'; import { RecipesList } from 'src/components/RecipesList'; +import { markdownToHtml } from 'src/methods/markdownToHtml'; import { getLetsCozinha, getLetsCozinhaLets } from 'src/cms/singleTypes'; import { getLetsSchema } from 'src/methods/getLetsSchema'; import { getPageTitle } from 'src/methods/getPageTitle'; @@ -22,6 +23,7 @@ type LetsProfile = { type Props = { letsProfile: LetsProfile; + textoCompletoHtml: string; favoriteRecipes: Recipe[]; letsSchema: any; asideData: AsideData; @@ -29,6 +31,7 @@ type Props = { export default function ConhecaALets({ letsProfile, + textoCompletoHtml, favoriteRecipes, letsSchema, }: Props) { @@ -88,7 +91,7 @@ export default function ConhecaALets({
- +
@@ -138,6 +141,7 @@ export const getStaticProps: GetStaticProps = async () => { return { props: { letsProfile, + textoCompletoHtml: await markdownToHtml(letsProfile.texto_completo ?? ''), favoriteRecipes, letsSchema: letsSchemaResult.status === 'fulfilled' && letsSchemaResult.value diff --git a/src/pages/ebooks/[slug].tsx b/src/pages/ebooks/[slug].tsx index 72fac87..65393d5 100644 --- a/src/pages/ebooks/[slug].tsx +++ b/src/pages/ebooks/[slug].tsx @@ -7,6 +7,7 @@ import { JsonLd } from 'src/components/JsonLd'; import { LinkButton } from 'src/components/LinkButton'; import { Markdown } from 'src/components/Markdown'; import { getEbook, getAllEbooks, type Ebook } from 'src/cms/ebooks'; +import { markdownToHtml } from 'src/methods/markdownToHtml'; import { getPageTitle } from 'src/methods/getPageTitle'; import { getUrl } from 'src/methods/getUrl'; import { getWebsiteName } from 'src/methods/getWebsiteName'; @@ -14,17 +15,19 @@ import { getAsideData, type AsideData } from 'src/methods/getAsideData'; type Props = { ebook: Ebook; + descricaoHtml: string; + paginaWebsiteHtml: string; asideData: AsideData; }; -function EbookHero({ ebook }: { ebook: Ebook }) { +function EbookHero({ ebook, descricaoHtml }: { ebook: Ebook; descricaoHtml: string }) { const imageSrc = ebook.imagem.formats.medium?.url || ebook.imagem.url; return (
- +
{ebook.preco && ( @@ -65,7 +68,7 @@ function EbookHero({ ebook }: { ebook: Ebook }) { ); } -export default function EbookPage({ ebook }: Props) { +export default function EbookPage({ ebook, descricaoHtml, paginaWebsiteHtml }: Props) { const url = getUrl(`/ebooks/${ebook.slug}`); const title = getPageTitle(ebook.titulo); @@ -114,12 +117,12 @@ export default function EbookPage({ ebook }: Props) { ]} > - +
- +
@@ -198,7 +201,12 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { : { featuredEbook: null, letsProfile: null, categoriesWithCounts: [], siteDescricao: null }; return { - props: { ebook, asideData }, + props: { + ebook, + descricaoHtml: await markdownToHtml(ebook.descricao ?? ''), + paginaWebsiteHtml: await markdownToHtml(ebook.pagina_website ?? ''), + asideData, + }, revalidate: 3600, }; }; diff --git a/src/pages/politica-de-privacidade.tsx b/src/pages/politica-de-privacidade.tsx index 9f6c3c5..303d933 100644 --- a/src/pages/politica-de-privacidade.tsx +++ b/src/pages/politica-de-privacidade.tsx @@ -2,6 +2,7 @@ import Head from 'next/head'; import type { GetStaticProps } from 'next'; import { Markdown } from 'src/components/Markdown'; import { getLetsCozinhaPoliticas } from 'src/cms/singleTypes'; +import { markdownToHtml } from 'src/methods/markdownToHtml'; import { getPageTitle } from 'src/methods/getPageTitle'; import { getUrl } from 'src/methods/getUrl'; import { getAsideData, type AsideData } from 'src/methods/getAsideData'; @@ -22,7 +23,7 @@ export default function PoliticaDePrivacidade({ content }: Props) { /> - + ); } @@ -44,7 +45,7 @@ export const getStaticProps: GetStaticProps = async () => { return { props: { - content: politicasResult.value.letsCozinhaPoliticas.politica_de_privacidade, + content: await markdownToHtml(politicasResult.value.letsCozinhaPoliticas.politica_de_privacidade), asideData, }, revalidate: 3600, diff --git a/src/pages/receitas/[slug].tsx b/src/pages/receitas/[slug].tsx index 928c188..feb91f3 100644 --- a/src/pages/receitas/[slug].tsx +++ b/src/pages/receitas/[slug].tsx @@ -5,6 +5,7 @@ import { Content } from 'src/components/Content'; import { CategoryTag } from 'src/components/CategoryTag'; import { JsonLd } from 'src/components/JsonLd'; import { Markdown } from 'src/components/Markdown'; +import { markdownToHtml } from 'src/methods/markdownToHtml'; import { type Recipe, getRecommendedEbook, @@ -29,6 +30,7 @@ import { getAsideData, type AsideData } from 'src/methods/getAsideData'; type Props = { recipe: Recipe; + receitaHtml: string; recipeSchema: ReturnType extends Promise ? T : never; @@ -41,6 +43,7 @@ type Props = { export default function RecipePage({ recipe, + receitaHtml, recipeSchema, similarRecipes, recommendedEbook, @@ -133,7 +136,7 @@ export default function RecipePage({ recipeName={recipe.nome} /> ) : ( - + )} @@ -239,6 +242,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => { return { props: { recipe, + receitaHtml: await markdownToHtml(recipe.receita ?? ''), recipeSchema, letsNome: letsResult.status === 'fulfilled' diff --git a/src/pages/termos-de-uso.tsx b/src/pages/termos-de-uso.tsx index aed65c6..27c58f6 100644 --- a/src/pages/termos-de-uso.tsx +++ b/src/pages/termos-de-uso.tsx @@ -2,6 +2,7 @@ import Head from 'next/head'; import type { GetStaticProps } from 'next'; import { Markdown } from 'src/components/Markdown'; import { getLetsCozinhaPoliticas } from 'src/cms/singleTypes'; +import { markdownToHtml } from 'src/methods/markdownToHtml'; import { getPageTitle } from 'src/methods/getPageTitle'; import { getUrl } from 'src/methods/getUrl'; import { getAsideData, type AsideData } from 'src/methods/getAsideData'; @@ -22,7 +23,7 @@ export default function TermosDeUso({ content }: Props) { /> - + ); } @@ -44,7 +45,7 @@ export const getStaticProps: GetStaticProps = async () => { return { props: { - content: politicasResult.value.letsCozinhaPoliticas.termos_de_uso, + content: await markdownToHtml(politicasResult.value.letsCozinhaPoliticas.termos_de_uso), asideData, }, revalidate: 3600,