feat(servicos): topo, entregáveis e cases por serviço, assunto próprio no WhatsApp e títulos curtos
This commit is contained in:
1 parent
cf360041bd
commit
2fd8b063d9
33 files changed
+1578
-300
No files matched your search
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Nome de cada uma das três ofertas e a frase que a mensagem de WhatsApp usa como assunto ("Quero conversar sobre {assunto}.").
|
||||
* É a única fonte desses textos: a home, a página da oferta, os dados estruturados, o formulário de contato e as mensagens de
|
||||
* WhatsApp importam daqui. Fica à parte de lib/site-content.ts, que reúne o texto de todas as páginas e não deve ir inteiro para
|
||||
* o navegador: este módulo é pequeno, não tem dependência e não usa nada do Node, então componentes de cliente podem importá-lo.
|
||||
*/
|
||||
export const offerLabels = {
|
||||
site: {
|
||||
name: "Site que traz pedidos pelo WhatsApp",
|
||||
whatsappSubject: "um site que traz pedidos pelo WhatsApp",
|
||||
},
|
||||
sistema: {
|
||||
name: "Sistema para tirar a operação da planilha",
|
||||
whatsappSubject: "um sistema para tirar a operação da planilha",
|
||||
},
|
||||
revisao: {
|
||||
name: "Revisão de sistema que já existe",
|
||||
whatsappSubject: "a revisão de um sistema que já existe",
|
||||
},
|
||||
} as const;
|
||||
+44
-37
@@ -2,9 +2,9 @@ import sitemap from "../app/sitemap";
|
||||
import {
|
||||
buildBreadcrumbList,
|
||||
buildCaseJsonLd,
|
||||
buildOfferServicesJsonLd,
|
||||
buildPageSocialMetadata,
|
||||
buildPersonJsonLd,
|
||||
buildProfessionalServiceJsonLd,
|
||||
buildProfilePageJsonLd,
|
||||
buildServiceJsonLd,
|
||||
buildWebSiteJsonLd,
|
||||
@@ -29,7 +29,6 @@ it("uses one canonical identity across website, profile and service", () => {
|
||||
website: `${siteUrl}/#website`,
|
||||
person: `${siteUrl}/#person`,
|
||||
profilePage: `${siteUrl}/curriculo#profile-page`,
|
||||
professionalService: `${siteUrl}/#professional-service`,
|
||||
});
|
||||
expect(buildWebSiteJsonLd().publisher).toEqual({ "@id": SEO_IDS.person });
|
||||
expect(buildProfilePageJsonLd()).toMatchObject({
|
||||
@@ -74,6 +73,7 @@ it("keeps the root graph limited to verified website and person fields", () => {
|
||||
image: `${siteUrl}/edev.jpg`,
|
||||
jobTitle: "Engenheiro da Computação e Desenvolvedor de Software",
|
||||
homeLocation: { "@type": "Place", name: "Garuva, Santa Catarina, Brasil" },
|
||||
// só o LinkedIn: a conta do GitHub foi suspensa (404) e o Instagram fica fora por política
|
||||
sameAs: ["https://www.linkedin.com/in/evandrodevbr/"],
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ it("keeps the root graph limited to verified website and person fields", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("builds a canonical service with only the verified service area", () => {
|
||||
it("builds a canonical service whose service area is the visible one: Garuva, Joinville, Santa Catarina and Brazil", () => {
|
||||
const service = representativeService;
|
||||
const pageUrl = `${siteUrl}/servicos/${service.slug}`;
|
||||
|
||||
@@ -96,49 +96,56 @@ it("builds a canonical service with only the verified service area", () => {
|
||||
areaServed: [
|
||||
{ "@type": "City", name: "Garuva" },
|
||||
{ "@type": "City", name: "Joinville" },
|
||||
{ "@type": "AdministrativeArea", name: "Norte de Santa Catarina" },
|
||||
{ "@type": "AdministrativeArea", name: "Santa Catarina" },
|
||||
{ "@type": "Country", name: "Brasil" },
|
||||
],
|
||||
});
|
||||
// a mesma área das três ofertas da home: um valor só em todo o JSON-LD
|
||||
for (const offer of buildOfferServicesJsonLd()) {
|
||||
expect(offer.areaServed).toEqual(buildServiceJsonLd(service).areaServed);
|
||||
}
|
||||
});
|
||||
|
||||
it("describes the business as a ProfessionalService with the service area and the three offers as a catalog", () => {
|
||||
const business = buildProfessionalServiceJsonLd();
|
||||
it("describes the three offers as Service nodes tied to the person, with the service area and the page of each offer", () => {
|
||||
const services = buildOfferServicesJsonLd();
|
||||
|
||||
expect(business).toMatchObject({
|
||||
"@type": "ProfessionalService",
|
||||
"@id": SEO_IDS.professionalService,
|
||||
name: siteName,
|
||||
url: siteUrl,
|
||||
founder: { "@id": SEO_IDS.person },
|
||||
});
|
||||
expect(business.areaServed.map(({ name }) => name)).toEqual([
|
||||
"Garuva",
|
||||
"Joinville",
|
||||
"Santa Catarina",
|
||||
"Brasil",
|
||||
]);
|
||||
|
||||
const catalog = business.hasOfferCatalog;
|
||||
expect(catalog["@type"]).toBe("OfferCatalog");
|
||||
// as mesmas três ofertas da home, com o mesmo nome e a página de cada uma
|
||||
expect(
|
||||
catalog.itemListElement.map(({ itemOffered }) => itemOffered.name),
|
||||
).toEqual([
|
||||
expect(services).toHaveLength(3);
|
||||
// as mesmas três ofertas da home, com o mesmo nome e a página de cada uma (a revisão aponta para /ia)
|
||||
expect(services.map(({ name }) => name)).toEqual([
|
||||
"Site que traz pedidos pelo WhatsApp",
|
||||
"Sistema para tirar a operação da planilha",
|
||||
"Revisão de sistema que já existe, inclusive feito com IA",
|
||||
"Revisão de sistema que já existe",
|
||||
]);
|
||||
expect(
|
||||
catalog.itemListElement.map(({ itemOffered }) => itemOffered.url),
|
||||
).toEqual(serviceOffers.map(({ href }) => `${siteUrl}${href}`));
|
||||
for (const offer of catalog.itemListElement) {
|
||||
expect(offer["@type"]).toBe("Offer");
|
||||
expect(offer.itemOffered["@type"]).toBe("Service");
|
||||
expect(offer.itemOffered.description.length).toBeGreaterThan(30);
|
||||
expect(services.map(({ url }) => url)).toEqual([
|
||||
`${siteUrl}/servicos/criacao-de-sites`,
|
||||
`${siteUrl}/servicos/engenharia-de-software`,
|
||||
`${siteUrl}/ia`,
|
||||
]);
|
||||
expect(services.map(({ url }) => url)).toEqual(
|
||||
serviceOffers.map(({ href }) => `${siteUrl}${href}`),
|
||||
);
|
||||
for (const service of services) {
|
||||
expect(service["@type"]).toBe("Service");
|
||||
expect(service.provider).toEqual({ "@id": SEO_IDS.person });
|
||||
expect(service.areaServed.map(({ name }) => name)).toEqual([
|
||||
"Garuva",
|
||||
"Joinville",
|
||||
"Santa Catarina",
|
||||
"Brasil",
|
||||
]);
|
||||
expect(service.description.length).toBeGreaterThan(30);
|
||||
}
|
||||
// nada que o site não publique: sem preço, prazo, endereço de rua, telefone ou e-mail
|
||||
expect(JSON.stringify(business)).not.toMatch(
|
||||
/price|priceCurrency|availability|telephone|email|streetAddress|PostalAddress|geo|aggregateRating|review/i,
|
||||
// ids próprios e estáveis, que não repetem o `#service` que a página de cada serviço emite
|
||||
const ids = services.map((service) => service["@id"]);
|
||||
expect(new Set(ids).size).toBe(3);
|
||||
expect(ids).toEqual([
|
||||
`${siteUrl}/#service-site`,
|
||||
`${siteUrl}/#service-sistema`,
|
||||
`${siteUrl}/#service-revisao`,
|
||||
]);
|
||||
// política de docs/seo-geo-operations.md: nenhum tipo de negócio local e nada que o site não publique
|
||||
expect(JSON.stringify(services)).not.toMatch(
|
||||
/ProfessionalService|LocalBusiness|OfferCatalog|price|priceCurrency|availability|telephone|email|streetAddress|PostalAddress|geo|aggregateRating|review/i,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+29
-41
@@ -14,7 +14,6 @@ export const SEO_IDS = {
|
||||
website: `${siteUrl}/#website`,
|
||||
person: `${siteUrl}/#person`,
|
||||
profilePage: `${siteUrl}/curriculo#profile-page`,
|
||||
professionalService: `${siteUrl}/#professional-service`,
|
||||
} as const;
|
||||
|
||||
export function buildWebSiteJsonLd() {
|
||||
@@ -51,6 +50,7 @@ export function buildPersonJsonLd() {
|
||||
"@type": "Place",
|
||||
name: "Garuva, Santa Catarina, Brasil",
|
||||
},
|
||||
// perfis profissionais públicos e no ar; a conta do GitHub foi suspensa (404) e o Instagram fica fora por política
|
||||
sameAs: ["https://www.linkedin.com/in/evandrodevbr/"],
|
||||
} as const;
|
||||
}
|
||||
@@ -67,39 +67,34 @@ export function buildProfilePageJsonLd() {
|
||||
}
|
||||
|
||||
/**
|
||||
* O negócio como `ProfessionalService`: onde atende (Garuva, Joinville, Santa Catarina e o Brasil, de forma remota) e as três ofertas
|
||||
* da home como catálogo. Sem endereço de rua, preço, prazo, telefone ou e-mail: o que não está publicado no site não entra nos
|
||||
* dados estruturados. Só o construtor: quem o coloca na página é a home.
|
||||
* Área de atendimento de todo nó `Service` (as ofertas da home e as páginas de serviço): a mesma do texto visível, "Garuva e
|
||||
* Joinville (SC), atendimento remoto em todo o Brasil". Um valor só, para os dados estruturados não divergirem entre páginas.
|
||||
*/
|
||||
export function buildProfessionalServiceJsonLd() {
|
||||
return {
|
||||
"@type": "ProfessionalService",
|
||||
"@id": SEO_IDS.professionalService,
|
||||
name: siteName,
|
||||
url: siteUrl,
|
||||
description:
|
||||
"Sites, sistemas web e aplicativos sob medida, e revisão de sistemas que já existem, inclusive feitos com IA. Garuva e Joinville (SC), remoto em todo o Brasil.",
|
||||
founder: { "@id": SEO_IDS.person },
|
||||
areaServed: [
|
||||
{ "@type": "City", name: "Garuva" },
|
||||
{ "@type": "City", name: "Joinville" },
|
||||
{ "@type": "AdministrativeArea", name: "Santa Catarina" },
|
||||
{ "@type": "Country", name: "Brasil" },
|
||||
],
|
||||
hasOfferCatalog: {
|
||||
"@type": "OfferCatalog",
|
||||
name: "Serviços de desenvolvimento de software",
|
||||
itemListElement: serviceOffers.map((offer) => ({
|
||||
"@type": "Offer",
|
||||
itemOffered: {
|
||||
"@type": "Service",
|
||||
name: offer.name,
|
||||
description: offer.summary,
|
||||
url: `${siteUrl}${offer.href}`,
|
||||
},
|
||||
})),
|
||||
},
|
||||
} as const;
|
||||
const serviceAreaServed = [
|
||||
{ "@type": "City", name: "Garuva" },
|
||||
{ "@type": "City", name: "Joinville" },
|
||||
{ "@type": "AdministrativeArea", name: "Santa Catarina" },
|
||||
{ "@type": "Country", name: "Brasil" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* As três ofertas da home como nós `Service`, ligados à pessoa (`provider`) e com a área de atendimento (`serviceAreaServed`).
|
||||
* Nada de `ProfessionalService`, `LocalBusiness` ou catálogo de ofertas: a política
|
||||
* de docs/seo-geo-operations.md os exclui sem entidade ou local comprovado. Sem endereço, preço, prazo, telefone ou e-mail: o que
|
||||
* não está publicado no site não entra nos dados estruturados. Só o construtor: quem o coloca na página é a home.
|
||||
* O `url` é a página da oferta (a revisão abre /ia). O `@id` é da home, para não repetir o do nó que a página do serviço emite
|
||||
* com outra descrição.
|
||||
*/
|
||||
export function buildOfferServicesJsonLd() {
|
||||
return serviceOffers.map((offer) => ({
|
||||
"@type": "Service",
|
||||
"@id": `${siteUrl}/#service-${offer.id}`,
|
||||
name: offer.name,
|
||||
description: offer.summary,
|
||||
url: `${siteUrl}${offer.href}`,
|
||||
provider: { "@id": SEO_IDS.person },
|
||||
areaServed: serviceAreaServed,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildServiceJsonLd(service: ServicePageContent) {
|
||||
@@ -112,14 +107,7 @@ export function buildServiceJsonLd(service: ServicePageContent) {
|
||||
name: service.navLabel,
|
||||
description: service.metaDescription,
|
||||
provider: { "@id": SEO_IDS.person },
|
||||
areaServed: [
|
||||
{ "@type": "City", name: "Garuva" },
|
||||
{ "@type": "City", name: "Joinville" },
|
||||
{
|
||||
"@type": "AdministrativeArea",
|
||||
name: "Norte de Santa Catarina",
|
||||
},
|
||||
],
|
||||
areaServed: serviceAreaServed,
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* Os dados reais só têm os quatro clientes da lista; o caminho do cliente fora da lista só existe aqui.
|
||||
*/
|
||||
|
||||
const LISTED = ["lavacao-047", "sc-plus", "as-servicos", "sev-modaintima"];
|
||||
const LISTED = ["sev-modaintima", "lavacao-047", "sc-plus", "as-servicos"];
|
||||
|
||||
function loadWithClients(clientIds: readonly string[] | "all") {
|
||||
let loaded: typeof import("./site-content") | undefined;
|
||||
@@ -25,14 +25,14 @@ function loadWithClients(clientIds: readonly string[] | "all") {
|
||||
}
|
||||
|
||||
describe("clientCasesForListing: ordem", () => {
|
||||
it("path: only listed clients -> the fixed order, the SEV last, whatever order the cases come in", () => {
|
||||
it("path: only listed clients -> the fixed order, the SEV first, whatever order the cases come in", () => {
|
||||
const { clientCasesForListing, clientCases } = loadWithClients(LISTED);
|
||||
|
||||
expect(clientCasesForListing.map(({ id }) => id)).toEqual(LISTED);
|
||||
expect(clientCasesForListing).toHaveLength(clientCases.length);
|
||||
});
|
||||
|
||||
it("path: a client that is not in the order list -> goes after all the listed ones, the SEV included", () => {
|
||||
it("path: a client that is not in the order list -> goes after all the listed ones", () => {
|
||||
const { clientCasesForListing, caseStudies } = loadWithClients([
|
||||
...LISTED,
|
||||
"gcc-installer",
|
||||
@@ -67,11 +67,11 @@ describe("clientCasesForListing: ordem", () => {
|
||||
expect(clientCasesForListing).toEqual([]);
|
||||
});
|
||||
|
||||
it("path: the real data -> the SEV is the last client, as the mosaic and /cases expect", () => {
|
||||
it("path: the real data -> the SEV is the first client, as in the home showcase", () => {
|
||||
jest.resetModules();
|
||||
const { clientCasesForListing } =
|
||||
jest.requireActual<typeof import("./site-content")>("./site-content");
|
||||
|
||||
expect(clientCasesForListing.at(-1)?.id).toBe("sev-modaintima");
|
||||
expect(clientCasesForListing[0]?.id).toBe("sev-modaintima");
|
||||
});
|
||||
});
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
caseStudies,
|
||||
clientCases,
|
||||
clientCasesForListing,
|
||||
generalFirstStep,
|
||||
getRelatedCases,
|
||||
getRelatedServices,
|
||||
getServiceOffer,
|
||||
isClientCase,
|
||||
labCases,
|
||||
pageUpdatedAt,
|
||||
@@ -93,12 +95,18 @@ it("mentions the region and a benefit in the title and description of every serv
|
||||
expect(new Set(descriptions).size).toBe(descriptions.length);
|
||||
});
|
||||
|
||||
it("maps each service to its client cases, with the SEV only where it is the proof: the systems service", () => {
|
||||
it("maps each service to its client cases, with the SEV as proof of both offers: the site and the system", () => {
|
||||
// a Lavação abre a página, a SEV é o segundo trabalho e os dois sites institucionais vão para o bloco menor
|
||||
expect(getService("criacao-de-sites").relatedCaseIds).toEqual([
|
||||
"lavacao-047",
|
||||
"sev-modaintima",
|
||||
"sc-plus",
|
||||
"as-servicos",
|
||||
]);
|
||||
expect(getService("criacao-de-sites").alsoCases).toEqual({
|
||||
title: "Também faço sites institucionais",
|
||||
caseIds: ["sc-plus", "as-servicos"],
|
||||
});
|
||||
expect(getService("engenharia-de-software").relatedCaseIds).toEqual([
|
||||
"sev-modaintima",
|
||||
"lavacao-047",
|
||||
@@ -111,7 +119,12 @@ it("maps each service to its client cases, with the SEV only where it is the pro
|
||||
servicePages
|
||||
.filter(({ relatedCaseIds }) => relatedCaseIds.includes("sev-modaintima"))
|
||||
.map(({ slug }) => slug),
|
||||
).toEqual(["engenharia-de-software"]);
|
||||
).toEqual(["criacao-de-sites", "engenharia-de-software"]);
|
||||
// o bloco menor só cita cases que o serviço já relaciona
|
||||
for (const { relatedCaseIds, alsoCases } of servicePages) {
|
||||
for (const id of alsoCases?.caseIds ?? [])
|
||||
expect(relatedCaseIds).toContain(id);
|
||||
}
|
||||
// nenhum case de cliente é um diagnóstico de sistema: o serviço fica só com a experiência na Apolo
|
||||
expect(getService("arquitetura-de-software").relatedCaseIds).toEqual([]);
|
||||
expect(getService("arquitetura-de-software").caseRelevance).toEqual({});
|
||||
@@ -195,12 +208,12 @@ it("gives each service without a client cover a flow diagram instead, and never
|
||||
}
|
||||
});
|
||||
|
||||
it("lists the client cases for the mosaic and /cases with the SEV last, never first", () => {
|
||||
it("lists the client cases for the mosaic and /cases in the order of the home showcase, the SEV first", () => {
|
||||
expect(clientCasesForListing.map(({ id }) => id)).toEqual([
|
||||
"sev-modaintima",
|
||||
"lavacao-047",
|
||||
"sc-plus",
|
||||
"as-servicos",
|
||||
"sev-modaintima",
|
||||
]);
|
||||
expect(clientCasesForListing).toHaveLength(clientCases.length);
|
||||
});
|
||||
@@ -222,7 +235,12 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
|
||||
expect(serviceOffers.map(({ name }) => name)).toEqual([
|
||||
"Site que traz pedidos pelo WhatsApp",
|
||||
"Sistema para tirar a operação da planilha",
|
||||
"Revisão de sistema que já existe, inclusive feito com IA",
|
||||
"Revisão de sistema que já existe",
|
||||
]);
|
||||
expect(serviceOffers.map(({ whatsappSubject }) => whatsappSubject)).toEqual([
|
||||
"um site que traz pedidos pelo WhatsApp",
|
||||
"um sistema para tirar a operação da planilha",
|
||||
"a revisão de um sistema que já existe",
|
||||
]);
|
||||
expect(serviceOffers.map(({ href }) => href)).toEqual([
|
||||
"/servicos/criacao-de-sites",
|
||||
@@ -232,6 +250,13 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
|
||||
for (const offer of serviceOffers) {
|
||||
expect(offer.deliverables.length).toBeGreaterThanOrEqual(3);
|
||||
expect(offer.deliverables.length).toBeLessThanOrEqual(4);
|
||||
expect(offer.firstStep).toMatch(
|
||||
/^Primeira conversa .* proposta por escrito\.$/,
|
||||
);
|
||||
// a frase geral da home não contradiz a da oferta: as duas terminam igual
|
||||
expect(offer.firstStep.endsWith(generalFirstStep.split(". ")[1])).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
offer.proof.caseIds.every((id) => caseStudies.some((c) => c.id === id)),
|
||||
).toBe(true);
|
||||
@@ -240,6 +265,7 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
|
||||
offer.name,
|
||||
offer.summary,
|
||||
...offer.deliverables,
|
||||
offer.firstStep,
|
||||
offer.proof.note ?? "",
|
||||
].join(" ");
|
||||
expect(text).not.toMatch(/R\$|\d+\s?%|\d+\s?dias|["“”]/);
|
||||
@@ -254,6 +280,48 @@ it("explains the three offers of the home with the same names, 3 or 4 deliverabl
|
||||
expect(serviceOffers[2].proof.caseIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses one name per offer: the service page, the breadcrumb label and the WhatsApp subject match the offer", () => {
|
||||
for (const offer of serviceOffers) {
|
||||
const service = servicePages.find(({ slug }) => offer.href.endsWith(slug));
|
||||
// a revisão abre /ia, que não é uma página de serviço
|
||||
if (!service) {
|
||||
expect(offer.id).toBe("revisao");
|
||||
continue;
|
||||
}
|
||||
expect(service.navLabel).toBe(offer.name);
|
||||
expect(service.headline).toBe(offer.name);
|
||||
expect(service.whatsappSubject).toBe(offer.whatsappSubject);
|
||||
expect(getServiceOffer(service.slug)).toBe(offer);
|
||||
}
|
||||
expect(getServiceOffer("consultoria-nextjs")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("gives every service a short name and a lowercase WhatsApp subject with its article, and no two names alike", () => {
|
||||
expect(servicePages.map(({ navLabel }) => navLabel)).toEqual([
|
||||
"Site que traz pedidos pelo WhatsApp",
|
||||
"Sistema para tirar a operação da planilha",
|
||||
"Consultoria em Next.js",
|
||||
"Aplicativos desktop",
|
||||
"Integração de equipamentos",
|
||||
"Diagnóstico de arquitetura",
|
||||
]);
|
||||
for (const service of servicePages) {
|
||||
expect(service.whatsappSubject).toMatch(/^(um|uma|a) [a-zà-ú]/);
|
||||
expect(service.whatsappSubject).not.toMatch(/[.!?]$/);
|
||||
}
|
||||
expect(getService("consultoria-nextjs").whatsappSubject).toBe(
|
||||
"uma consultoria em Next.js",
|
||||
);
|
||||
const names = servicePages.map(({ navLabel }) => navLabel);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
});
|
||||
|
||||
it("says the systems service in terms of the pain: spreadsheets and rework", () => {
|
||||
expect(getService("engenharia-de-software").answer).toMatch(
|
||||
/planilhas.*retrabalho/,
|
||||
);
|
||||
});
|
||||
|
||||
it("writes 2 or 3 capabilities for every client case, without numbers, and none for the laboratory", () => {
|
||||
for (const caseStudy of clientCases) {
|
||||
expect(caseStudy.capabilities?.length).toBeGreaterThanOrEqual(2);
|
||||
@@ -323,7 +391,9 @@ it("derives the exact related service set from the existing relationship", () =>
|
||||
expect(getRelatedServices("twins-lab").map(({ slug }) => slug)).toEqual([
|
||||
"integracao-hardware-software-iot",
|
||||
]);
|
||||
// a SEV liga à oferta de site e à de sistema, nesta ordem
|
||||
expect(getRelatedServices("sev-modaintima").map(({ slug }) => slug)).toEqual([
|
||||
"criacao-de-sites",
|
||||
"engenharia-de-software",
|
||||
]);
|
||||
expect(
|
||||
@@ -373,7 +443,7 @@ it("writes one sentence on why each related case matters to its service", () =>
|
||||
}
|
||||
});
|
||||
|
||||
it("cites the Apolo Energia experience, from 2022, only on the IoT and architecture services", () => {
|
||||
it("cites the Apolo Energia experience, from 2022, only on the systems, IoT and architecture services", () => {
|
||||
const apolo = portfolioData.experiences.find(
|
||||
({ id }) => id === "apolo-energia",
|
||||
);
|
||||
@@ -383,6 +453,7 @@ it("cites the Apolo Energia experience, from 2022, only on the IoT and architect
|
||||
.filter(({ experienceNote }) => experienceNote)
|
||||
.map(({ slug }) => slug);
|
||||
expect(withNote).toEqual([
|
||||
"engenharia-de-software",
|
||||
"integracao-hardware-software-iot",
|
||||
"arquitetura-de-software",
|
||||
]);
|
||||
@@ -390,12 +461,24 @@ it("cites the Apolo Energia experience, from 2022, only on the IoT and architect
|
||||
const note = getService(slug).experienceNote ?? "";
|
||||
expect(note).toMatch(/Apolo Energia/);
|
||||
expect(note).toMatch(/desde 2022/i);
|
||||
expect(note).toMatch(/sistemas internos/i);
|
||||
expect(note).toMatch(/aplicativos/i);
|
||||
expect(note).toMatch(/APIs/);
|
||||
// sem link e sem detalhe de operação do cliente
|
||||
expect(note).not.toMatch(/https?:|www\.|\.com/i);
|
||||
}
|
||||
for (const slug of [
|
||||
"integracao-hardware-software-iot",
|
||||
"arquitetura-de-software",
|
||||
]) {
|
||||
const note = getService(slug).experienceNote ?? "";
|
||||
expect(note).toMatch(/sistemas internos/i);
|
||||
expect(note).toMatch(/aplicativos/i);
|
||||
}
|
||||
// o sistema diz o que o currículo diz da Apolo: aplicações web e mobile para rotinas internas, APIs e painéis de operação
|
||||
const systemsNote = getService("engenharia-de-software").experienceNote ?? "";
|
||||
expect(systemsNote).toMatch(/aplicações web e mobile para rotinas internas/);
|
||||
expect(systemsNote).toMatch(/painéis de operação/);
|
||||
expect(apolo?.description).toMatch(/aplicações Web e Mobile/);
|
||||
expect(apolo?.description).toMatch(/rotinas internas/);
|
||||
});
|
||||
|
||||
it("keeps the problem of every client case concrete, with no invented figure or quote", () => {
|
||||
@@ -449,6 +532,7 @@ it("keeps AS Serviços without a public site and states the author's participati
|
||||
it("keeps site-content and the modules it imports free of Node built-ins, so client components can import it", () => {
|
||||
for (const file of [
|
||||
"site-content",
|
||||
"offer-labels",
|
||||
"portfolio-data",
|
||||
"proof",
|
||||
"project-health",
|
||||
|
||||
+59
-19
@@ -1,3 +1,4 @@
|
||||
import { offerLabels } from "./offer-labels";
|
||||
import { portfolio } from "./portfolio-data";
|
||||
import { isClientProject } from "./proof";
|
||||
|
||||
@@ -25,7 +26,10 @@ export const buttonLabels = {
|
||||
|
||||
export interface ServicePageContent {
|
||||
slug: string;
|
||||
/** Nome curto do serviço: o mesmo na trilha, nos títulos, nos links e nos dados estruturados. Nas ofertas da home é o nome da oferta. */
|
||||
navLabel: string;
|
||||
/** Assunto da conversa de WhatsApp, em minúsculas e com artigo, para a frase "Quero conversar sobre {assunto}.". */
|
||||
whatsappSubject: string;
|
||||
eyebrow: string;
|
||||
/** Título para busca, separado do h1: até 43 caracteres, e 60 com o sufixo " | Evandro.dev.br" (17) que o layout acrescenta. */
|
||||
metaTitle: string;
|
||||
@@ -36,6 +40,7 @@ export interface ServicePageContent {
|
||||
intro: string;
|
||||
answer: string;
|
||||
audience: string[];
|
||||
/** "O que podemos entregar". Nas páginas que explicam uma oferta da home a seção não aparece: vale o "O que você recebe" da oferta (`serviceOffers`), uma lista de entregáveis só. */
|
||||
outcomes: string[];
|
||||
process: string[];
|
||||
examples: string[];
|
||||
@@ -43,6 +48,8 @@ export interface ServicePageContent {
|
||||
stack: string[];
|
||||
/** Ids dos cases relacionados; os de cliente aparecem primeiro na página. */
|
||||
relatedCaseIds: string[];
|
||||
/** Cases relacionados que saem de "Onde já apliquei isso" e vão para um bloco menor com este título, sem capa (ids de `relatedCaseIds`). */
|
||||
alsoCases?: { title: string; caseIds: string[] };
|
||||
/** Capa do topo: o case (um dos relacionados) e o aparelho da captura. Sem ela, o topo mostra o esquema `diagram`. */
|
||||
heroCase?: { id: string; device: HeroDevice };
|
||||
/** Esquema de fluxo do topo, para o serviço sem case com captura: nunca leva dado real nem logo de cliente. */
|
||||
@@ -101,12 +108,13 @@ export interface CaseStudyContent {
|
||||
export const servicePages: ServicePageContent[] = [
|
||||
{
|
||||
slug: "criacao-de-sites",
|
||||
navLabel: "Sites profissionais",
|
||||
navLabel: offerLabels.site.name,
|
||||
whatsappSubject: offerLabels.site.whatsappSubject,
|
||||
eyebrow: "Presença digital",
|
||||
metaTitle: "Criação de sites em Joinville e SC",
|
||||
metaDescription:
|
||||
"Site profissional para apresentar sua empresa e levar o cliente ao WhatsApp. Atendimento em Garuva e Joinville (SC) e remoto em todo o Brasil.",
|
||||
headline: "Site que traz pedidos pelo WhatsApp",
|
||||
headline: offerLabels.site.name,
|
||||
answer:
|
||||
"Crio sites para apresentar sua empresa, mostrar o que você oferece e facilitar o contato. O projeto considera leitura no celular e no computador, conteúdo, páginas e funcionalidades combinadas antes do desenvolvimento.",
|
||||
intro:
|
||||
@@ -138,12 +146,19 @@ export const servicePages: ServicePageContent[] = [
|
||||
"Metadados, URLs canônicas, sitemap e dados estruturados ajudam os buscadores a entender o site.",
|
||||
],
|
||||
stack: ["Next.js", "TypeScript", "Tailwind CSS", "SEO técnico", "Vercel"],
|
||||
relatedCaseIds: ["lavacao-047", "sc-plus", "as-servicos"],
|
||||
// a Lavação abre a página (capa), a SEV é o segundo trabalho e os dois sites institucionais ficam no bloco menor
|
||||
relatedCaseIds: ["lavacao-047", "sev-modaintima", "sc-plus", "as-servicos"],
|
||||
heroCase: { id: "lavacao-047", device: "desktop" },
|
||||
alsoCases: {
|
||||
title: "Também faço sites institucionais",
|
||||
caseIds: ["sc-plus", "as-servicos"],
|
||||
},
|
||||
updatedAt: "2026-10-14",
|
||||
caseRelevance: {
|
||||
"lavacao-047":
|
||||
"Um site local com preço por porte do veículo e pedido pelo WhatsApp, que o dono atualiza sozinho.",
|
||||
"sev-modaintima":
|
||||
"Um catálogo por tamanho com pedido pelo WhatsApp, e um painel para a loja cuidar de produtos e estoque.",
|
||||
"sc-plus":
|
||||
"Uma página institucional que reúne áreas, projetos, apoiadores e contato em um endereço próprio.",
|
||||
"as-servicos":
|
||||
@@ -169,14 +184,15 @@ export const servicePages: ServicePageContent[] = [
|
||||
},
|
||||
{
|
||||
slug: "engenharia-de-software",
|
||||
navLabel: "Sistemas web e aplicativos mobile",
|
||||
navLabel: offerLabels.sistema.name,
|
||||
whatsappSubject: offerLabels.sistema.whatsappSubject,
|
||||
eyebrow: "Engenharia de software",
|
||||
metaTitle: "Sistemas web e aplicativos sob medida",
|
||||
metaDescription:
|
||||
"Sistema web ou aplicativo sob medida para organizar pedidos, clientes e rotinas. Atendimento em Garuva e Joinville (SC) e remoto em todo o Brasil.",
|
||||
headline: "Sistema para tirar a operação da planilha",
|
||||
headline: offerLabels.sistema.name,
|
||||
answer:
|
||||
"Desenvolvo sistemas web, usados no navegador, e aplicativos mobile para celular. Também conecto ferramentas e evoluo sistemas existentes. Definimos juntos os usuários, as funções, as plataformas atendidas e a primeira entrega útil para o negócio.",
|
||||
"Quando a operação vive em planilhas, mensagens e retrabalho, desenvolvo um sistema web ou um aplicativo mobile feito para a sua equipe. Também conecto ferramentas e evoluo sistemas existentes. Definimos juntos os usuários, as funções e a primeira entrega útil.",
|
||||
intro:
|
||||
"Organize a operação com um sistema web ou aplicativo mobile sob medida. Conecte ferramentas, reduza tarefas repetidas ou melhore um sistema que já existe.",
|
||||
audience: [
|
||||
@@ -215,6 +231,8 @@ export const servicePages: ServicePageContent[] = [
|
||||
"lavacao-047":
|
||||
"Um painel em que o dono altera preços, textos e fotos, e a mudança aparece no site sem nova publicação.",
|
||||
},
|
||||
experienceNote:
|
||||
"Desde 2022 trabalho com isso na Apolo Energia: aplicações web e mobile para rotinas internas, APIs e painéis de operação. Por serem sistemas de uso interno, não há link nem detalhes públicos.",
|
||||
faq: [
|
||||
{
|
||||
question: "Preciso saber qual tecnologia usar?",
|
||||
@@ -235,7 +253,8 @@ export const servicePages: ServicePageContent[] = [
|
||||
},
|
||||
{
|
||||
slug: "consultoria-nextjs",
|
||||
navLabel: "Melhorias em sites e sistemas Next.js",
|
||||
navLabel: "Consultoria em Next.js",
|
||||
whatsappSubject: "uma consultoria em Next.js",
|
||||
eyebrow: "Consultoria Next.js",
|
||||
metaTitle: "Consultoria Next.js para site ou sistema",
|
||||
metaDescription:
|
||||
@@ -301,7 +320,8 @@ export const servicePages: ServicePageContent[] = [
|
||||
},
|
||||
{
|
||||
slug: "tauri-rust",
|
||||
navLabel: "Aplicativos desktop para Windows e Linux",
|
||||
navLabel: "Aplicativos desktop",
|
||||
whatsappSubject: "um aplicativo desktop",
|
||||
eyebrow: "Aplicativos desktop",
|
||||
metaTitle: "Aplicativos desktop para Windows e Linux",
|
||||
metaDescription:
|
||||
@@ -370,7 +390,8 @@ export const servicePages: ServicePageContent[] = [
|
||||
},
|
||||
{
|
||||
slug: "integracao-hardware-software-iot",
|
||||
navLabel: "Integração de equipamentos e sistemas",
|
||||
navLabel: "Integração de equipamentos",
|
||||
whatsappSubject: "uma integração de equipamentos",
|
||||
eyebrow: "Hardware, software e IoT",
|
||||
metaTitle: "Integração de equipamentos e sistemas (IoT)",
|
||||
metaDescription:
|
||||
@@ -436,7 +457,8 @@ export const servicePages: ServicePageContent[] = [
|
||||
},
|
||||
{
|
||||
slug: "arquitetura-de-software",
|
||||
navLabel: "Diagnóstico e arquitetura de software",
|
||||
navLabel: "Diagnóstico de arquitetura",
|
||||
whatsappSubject: "um diagnóstico de arquitetura",
|
||||
eyebrow: "Diagnóstico e decisão",
|
||||
metaTitle: "Diagnóstico e arquitetura de software",
|
||||
metaDescription:
|
||||
@@ -816,14 +838,14 @@ export const labCases = caseStudies.filter(
|
||||
);
|
||||
|
||||
/**
|
||||
* Ordem dos clientes na lista de /cases e no mosaico de /servicos: a SEV Moda Íntima, que já abre a página de sistemas
|
||||
* e é a prova da oferta de sistema, vem por último e fica fora das três capas do mosaico. Cliente fora desta lista vai para o fim.
|
||||
* Ordem dos clientes na lista de /cases e no mosaico de /servicos: a mesma da vitrine da home (SHOWCASE em
|
||||
* lib/project-showcase.ts), com a SEV Moda Íntima primeiro. Cliente fora desta lista vai para o fim.
|
||||
*/
|
||||
const clientListingOrder = [
|
||||
"sev-modaintima",
|
||||
"lavacao-047",
|
||||
"sc-plus",
|
||||
"as-servicos",
|
||||
"sev-modaintima",
|
||||
];
|
||||
|
||||
function listingRank({ id }: { id: string }) {
|
||||
@@ -848,18 +870,28 @@ export const pageUpdatedAt = {
|
||||
|
||||
export interface ServiceOffer {
|
||||
id: "site" | "sistema" | "revisao";
|
||||
/** Nome da oferta, o mesmo da home. */
|
||||
/** Nome da oferta, o mesmo da home, da página da oferta, dos dados estruturados e do WhatsApp. */
|
||||
name: string;
|
||||
/** Assunto da conversa de WhatsApp, em minúsculas e com artigo, para a frase "Quero conversar sobre {assunto}.". */
|
||||
whatsappSubject: string;
|
||||
/** Uma frase sobre a oferta (descrição dos dados estruturados). */
|
||||
summary: string;
|
||||
/** Página que explica a oferta. */
|
||||
href: string;
|
||||
/** O que a pessoa recebe, em 3 ou 4 itens, só com o que os cases ou a página da oferta já sustentam. */
|
||||
/** O que a pessoa recebe, em 3 ou 4 itens, só com o que os cases ou a página da oferta já sustentam. A home e a página da oferta mostram esta lista. */
|
||||
deliverables: string[];
|
||||
/** Primeiro passo: uma conversa e, depois dela, o escopo e a proposta por escrito (sem prazo nem preço). A página da oferta mostra esta frase; a home, que serve às três, mostra `generalFirstStep`. */
|
||||
firstStep: string;
|
||||
/** Onde ver a oferta funcionando: o texto que abre a linha, os cases (links) e uma observação opcional. */
|
||||
proof: { label: string; caseIds: string[]; note?: string };
|
||||
}
|
||||
|
||||
/** O que vem depois da primeira conversa: o mesmo em cada oferta e na frase geral da home. */
|
||||
const FIRST_STEP_NEXT = "Depois, escopo e proposta por escrito.";
|
||||
|
||||
/** O primeiro passo da home, que vale para as três ofertas: igual ao de cada uma, só sem dizer o que se entende na conversa. */
|
||||
export const generalFirstStep = `Primeira conversa para entender o seu caso. ${FIRST_STEP_NEXT}`;
|
||||
|
||||
/**
|
||||
* As três ofertas da home, na ordem do H1, com o mesmo nome. Cada texto só diz o que os cases citados, a experiência
|
||||
* profissional ou a página /ia já sustentam; sem prazo nem preço.
|
||||
@@ -867,7 +899,7 @@ export interface ServiceOffer {
|
||||
export const serviceOffers: ServiceOffer[] = [
|
||||
{
|
||||
id: "site",
|
||||
name: "Site que traz pedidos pelo WhatsApp",
|
||||
...offerLabels.site,
|
||||
summary:
|
||||
"Catálogo ou tabela de preços, mensagem pronta no WhatsApp e um painel para você editar preços, textos e fotos sozinho.",
|
||||
href: "/servicos/criacao-de-sites",
|
||||
@@ -877,6 +909,7 @@ export const serviceOffers: ServiceOffer[] = [
|
||||
"Painel para você mudar preços, textos e fotos sem pedir a ninguém.",
|
||||
"Leitura boa no celular e informações organizadas para o Google.",
|
||||
],
|
||||
firstStep: `Primeira conversa para entender o problema. ${FIRST_STEP_NEXT}`,
|
||||
proof: {
|
||||
label: "Veja funcionando",
|
||||
caseIds: ["lavacao-047", "sev-modaintima"],
|
||||
@@ -884,7 +917,7 @@ export const serviceOffers: ServiceOffer[] = [
|
||||
},
|
||||
{
|
||||
id: "sistema",
|
||||
name: "Sistema para tirar a operação da planilha",
|
||||
...offerLabels.sistema,
|
||||
summary:
|
||||
"Pedidos, estoque e rotinas que hoje vivem em planilhas e mensagens passam para um painel próprio, feito para a sua equipe.",
|
||||
href: "/servicos/engenharia-de-software",
|
||||
@@ -894,6 +927,7 @@ export const serviceOffers: ServiceOffer[] = [
|
||||
"Acesso por perfil: cada pessoa vê só o que precisa.",
|
||||
"Uma primeira versão com as funções prioritárias, ampliada em etapas.",
|
||||
],
|
||||
firstStep: `Primeira conversa para entender a operação. ${FIRST_STEP_NEXT}`,
|
||||
proof: {
|
||||
label: "Veja funcionando",
|
||||
caseIds: ["sev-modaintima"],
|
||||
@@ -902,9 +936,9 @@ export const serviceOffers: ServiceOffer[] = [
|
||||
},
|
||||
{
|
||||
id: "revisao",
|
||||
name: "Revisão de sistema que já existe, inclusive feito com IA",
|
||||
...offerLabels.revisao,
|
||||
summary:
|
||||
"Avaliação do que já existe, teste das falhas de acesso a dados e separação do que dá para aproveitar do que é melhor refazer.",
|
||||
"Avaliação de um sistema que já existe, inclusive feito com IA: teste das falhas de acesso a dados e separação do que dá para aproveitar do que é melhor refazer.",
|
||||
href: "/ia",
|
||||
deliverables: [
|
||||
"As falhas de acesso a dados: quem consegue ver ou alterar o dado de quem.",
|
||||
@@ -912,6 +946,7 @@ export const serviceOffers: ServiceOffer[] = [
|
||||
"O que é melhor refazer, com o motivo de cada parte.",
|
||||
"Um orçamento por escrito para pôr o sistema em produção.",
|
||||
],
|
||||
firstStep: `Primeira conversa para entender o app e o que ele precisa fazer. ${FIRST_STEP_NEXT}`,
|
||||
proof: {
|
||||
label: "Veja como funciona",
|
||||
caseIds: [],
|
||||
@@ -930,6 +965,11 @@ export function getServicePage(slug: string) {
|
||||
return servicePages.find((service) => service.slug === slug);
|
||||
}
|
||||
|
||||
/** A oferta que a página de serviço explica; os serviços que não são oferta (e a revisão, que abre /ia) não têm. */
|
||||
export function getServiceOffer(slug: string) {
|
||||
return serviceOffers.find(({ href }) => href === `/servicos/${slug}`);
|
||||
}
|
||||
|
||||
/** Cases na ordem de `caseIds`, com os de cliente à frente dos de laboratório. */
|
||||
export function getRelatedCases(caseIds: string[]) {
|
||||
const cases = caseIds.flatMap((id) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
*/
|
||||
|
||||
const contexts: WhatsAppContexts = {
|
||||
services: { "criacao-de-sites": "Sites profissionais" },
|
||||
services: { "criacao-de-sites": "um site que traz pedidos pelo WhatsApp" },
|
||||
cases: { "sev-modaintima": "SEV Moda Íntima", vazio: "" },
|
||||
};
|
||||
|
||||
|
||||
+42
-6
@@ -1,3 +1,4 @@
|
||||
import { servicePages } from "./site-content";
|
||||
import {
|
||||
AI_REVIEW_SUBJECT,
|
||||
buildWhatsAppUrl,
|
||||
@@ -5,6 +6,7 @@ import {
|
||||
HOME_SUBJECT,
|
||||
headerOpensWhatsApp,
|
||||
isServiceOrCasePath,
|
||||
OFFER_SUBJECTS,
|
||||
RESUME_SUBJECT,
|
||||
SERVICES_SUBJECT,
|
||||
similarProjectSubject,
|
||||
@@ -13,15 +15,39 @@ import {
|
||||
} from "./whatsapp";
|
||||
|
||||
const contexts: WhatsAppContexts = {
|
||||
services: { "criacao-de-sites": "Sites profissionais" },
|
||||
// o whatsappSubject do serviço, como app/layout.tsx o monta: uma frase em minúsculas, e não o nome do serviço
|
||||
services: { "criacao-de-sites": "um site que traz pedidos pelo WhatsApp" },
|
||||
cases: { "sev-modaintima": "SEV Moda Íntima" },
|
||||
};
|
||||
|
||||
describe("whatsAppSubjectForPath", () => {
|
||||
it("cites the service on /servicos/<slug>", () => {
|
||||
expect(whatsAppSubjectForPath("/servicos/criacao-de-sites", contexts)).toBe(
|
||||
"Sites profissionais",
|
||||
it("cites the service on /servicos/<slug> with its whatsappSubject, a lowercase phrase, so the message reads as a sentence", () => {
|
||||
const subject = whatsAppSubjectForPath(
|
||||
"/servicos/criacao-de-sites",
|
||||
contexts,
|
||||
);
|
||||
expect(subject).toBe("um site que traz pedidos pelo WhatsApp");
|
||||
expect(`Quero conversar sobre ${subject}.`).not.toMatch(
|
||||
/sobre Sites profissionais/,
|
||||
);
|
||||
});
|
||||
|
||||
it("has a whatsappSubject, in lowercase and with no final period, for every service page and offer of site-content", () => {
|
||||
expect(servicePages.length).toBeGreaterThan(0);
|
||||
for (const { slug, navLabel, whatsappSubject } of servicePages) {
|
||||
// não é o nome do serviço (que tem inicial maiúscula): é uma frase que entra depois de "sobre"
|
||||
expect(whatsappSubject).toMatch(/^[a-zà-ú].*[^.]$/);
|
||||
expect(whatsappSubject).not.toBe(navLabel);
|
||||
expect(
|
||||
whatsAppSubjectForPath(`/servicos/${slug}`, {
|
||||
services: { [slug]: whatsappSubject },
|
||||
cases: {},
|
||||
}),
|
||||
).toBe(whatsappSubject);
|
||||
}
|
||||
for (const subject of Object.values(OFFER_SUBJECTS)) {
|
||||
expect(subject).toMatch(/^[a-zà-ú].*[^.]$/);
|
||||
}
|
||||
});
|
||||
|
||||
it("cites the case on /cases/<slug>, with the phrase every page uses for a similar project", () => {
|
||||
@@ -40,7 +66,17 @@ describe("whatsAppSubjectForPath", () => {
|
||||
expect(whatsAppSubjectForPath("/servicos", contexts)).toBe(
|
||||
SERVICES_SUBJECT,
|
||||
);
|
||||
expect(SERVICES_SUBJECT).toBe("Sites, sistemas e aplicativos");
|
||||
expect(SERVICES_SUBJECT).toBe("sites, sistemas e aplicativos");
|
||||
// nenhum assunto fixo começa em maiúscula: "Quero conversar sobre Sites, sistemas..." era o erro
|
||||
for (const subject of [
|
||||
AI_REVIEW_SUBJECT,
|
||||
HOME_SUBJECT,
|
||||
CASES_SUBJECT,
|
||||
SERVICES_SUBJECT,
|
||||
RESUME_SUBJECT,
|
||||
]) {
|
||||
expect(subject).toMatch(/^[a-zà-ú]/);
|
||||
}
|
||||
expect(whatsAppSubjectForPath("/curriculo", contexts)).toBe(RESUME_SUBJECT);
|
||||
// sem contextos, a página-índice continua falando de si
|
||||
expect(whatsAppSubjectForPath("/cases/")).toBe(CASES_SUBJECT);
|
||||
@@ -55,7 +91,7 @@ describe("whatsAppSubjectForPath", () => {
|
||||
it("ignores a trailing slash", () => {
|
||||
expect(
|
||||
whatsAppSubjectForPath("/servicos/criacao-de-sites/", contexts),
|
||||
).toBe("Sites profissionais");
|
||||
).toBe("um site que traz pedidos pelo WhatsApp");
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
+19
-3
@@ -1,3 +1,5 @@
|
||||
import { offerLabels } from "./offer-labels";
|
||||
|
||||
const COUNTRY = ["5", "5"];
|
||||
const AREA = ["4", "7"];
|
||||
const NINE = ["9"];
|
||||
@@ -17,11 +19,14 @@ export function buildWhatsAppUrl(message: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Nomes dos serviços e dos cases, montados no servidor (app/layout.tsx) a partir de lib/site-content.ts:
|
||||
* Assuntos dos serviços e nomes dos cases, montados no servidor (app/layout.tsx) a partir de lib/site-content.ts:
|
||||
* esse arquivo reúne o texto de todas as páginas e não deve ir inteiro para o navegador.
|
||||
*/
|
||||
export interface WhatsAppContexts {
|
||||
/** slug do serviço -> nome do serviço */
|
||||
/**
|
||||
* slug do serviço -> `whatsappSubject` dele: uma frase em minúsculas que entra em "Quero conversar sobre {assunto}."
|
||||
* (e não o nome do serviço, que daria "sobre Sites profissionais.").
|
||||
*/
|
||||
services: Record<string, string>;
|
||||
/** id do case -> nome do projeto ou do cliente */
|
||||
cases: Record<string, string>;
|
||||
@@ -36,9 +41,20 @@ export const AI_REVIEW_SUBJECT = "a revisão de um aplicativo feito com IA";
|
||||
*/
|
||||
export const HOME_SUBJECT = "um site ou sistema para a minha empresa";
|
||||
export const CASES_SUBJECT = "um projeto parecido com os cases do portfólio";
|
||||
export const SERVICES_SUBJECT = "Sites, sistemas e aplicativos";
|
||||
export const SERVICES_SUBJECT = "sites, sistemas e aplicativos";
|
||||
export const RESUME_SUBJECT = "um projeto, depois de ver o seu currículo";
|
||||
|
||||
/**
|
||||
* Assunto de cada oferta da home, para o contato direto de WhatsApp do formulário quando o tipo de projeto já está
|
||||
* escolhido ("Quero conversar sobre um site que traz pedidos pelo WhatsApp."). Os nomes das ofertas, em minúsculas e com artigo,
|
||||
* que vêm de `offerLabels` (lib/offer-labels.ts), a mesma fonte do nome da oferta e da página dela.
|
||||
*/
|
||||
export const OFFER_SUBJECTS = {
|
||||
site: offerLabels.site.whatsappSubject,
|
||||
sistema: offerLabels.sistema.whatsappSubject,
|
||||
revisao: offerLabels.revisao.whatsappSubject,
|
||||
} as const;
|
||||
|
||||
/** Frase padrão para quem quer algo como um projeto publicado (case de cliente ou de laboratório). "O case" concorda com qualquer título: sem ele, saía "o Lavação 047" e "o SEV Moda Íntima". */
|
||||
export function similarProjectSubject(title: string) {
|
||||
return `um projeto parecido com o case ${title}`;
|
||||
|
||||
Reference in new issue
Block a user