Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions public/llms.txt

This file was deleted.

13 changes: 0 additions & 13 deletions public/sitemap.xml

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 12 additions & 3 deletions src/components/BriefingForm.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
---
const ENGAGEMENT_OPTIONS = [
import type { EngagementKey } from "@/lib/engagements";

// Values are typed against the canonical EngagementKey so a renamed tier fails
// to compile here instead of silently mismatching the server validation.
const ENGAGEMENT_OPTIONS: { value: EngagementKey; label: string }[] = [
{ value: "expert-call", label: "Expert Call — 30-min call, $500" },
{ value: "second-opinion", label: "Second Opinion — short brief, $2,000" },
{ value: "technical-brief", label: "Technical Brief — in-depth brief, $3,500" },
Expand Down Expand Up @@ -90,8 +94,13 @@ const errorClass = "mt-2 text-xs text-accent font-bold";

<script>
import { z } from "zod";
import { ENGAGEMENT_KEYS } from "@/lib/engagements";

const TURNSTILE_SITE_KEY = "0x4AAAAAADXZiCqy8p3HZOMu";
// Prod sitekey is locked to decipher.ms; on localhost it errors 110200. Use
// Cloudflare's always-pass dummy sitekey in dev so the widget actually renders.
const TURNSTILE_SITE_KEY = import.meta.env.DEV
? "1x00000000000000000000AA"
: "0x4AAAAAADXZiCqy8p3HZOMu";

declare global {
interface Window {
Expand All @@ -115,7 +124,7 @@ const errorClass = "mt-2 text-xs text-accent font-bold";
name: z.string().trim().min(1, "Required").max(120),
email: z.string().trim().email("Invalid email").max(255),
role: z.string().trim().max(120).optional().or(z.literal("")),
engagementType: z.enum(["expert-call", "second-opinion", "technical-brief", "workshop"], {
engagementType: z.enum(ENGAGEMENT_KEYS, {
errorMap: () => ({ message: "Pick an engagement type" }),
}),
topic: z.string().trim().min(1, "Required").max(200),
Expand Down
12 changes: 6 additions & 6 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ const prefix = home ? "" : "/";
const pad = home ? "" : "px-6";

const services = [
{ label: "Expert Call", href: `${prefix}#services` },
{ label: "Second Opinion", href: `${prefix}#services` },
{ label: "Technical Brief", href: `${prefix}#services` },
{ label: "Expert Call", href: `${prefix}#pricing` },
{ label: "Second Opinion", href: `${prefix}#pricing` },
{ label: "Technical Brief", href: `${prefix}#pricing` },
];
const company = [
{ label: "Approach", href: `${prefix}#approach` },
{ label: "About", href: home ? "#founder" : "/about" },
{ label: "Briefing", href: `${prefix}#briefing` },
{ label: "About", href: home ? "#about" : "/about" },
{ label: "Contact", href: `${prefix}#contact` },
];
---

Expand All @@ -32,7 +32,7 @@ const company = [
</div>
<div class="grid grid-cols-2 gap-12">
<div>
<p class="text-[10px] font-black uppercase tracking-[0.3em] text-accent mb-6">Services</p>
<p class="text-[10px] font-black uppercase tracking-[0.3em] text-accent mb-6">Pricing</p>
<ul class="space-y-3 text-base font-bold">
{
services.map((i) => (
Expand Down
16 changes: 9 additions & 7 deletions src/components/Nav.astro
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
---
interface Props {
variant?: "home" | "page";
active?: "about";
active?: "about" | "work";
}

const { variant = "page", active } = Astro.props;
const home = variant === "home";

const links: { label: string; href: string; key?: "about" }[] = home
const links: { label: string; href: string; key?: "about" | "work" }[] = home
? [
{ label: "Services", href: "#services" },
{ label: "Pricing", href: "#pricing" },
{ label: "Topics", href: "#topics" },
{ label: "Approach", href: "#approach" },
{ label: "About", href: "#founder" },
{ label: "Briefing", href: "#briefing" },
{ label: "Samples", href: "/work", key: "work" },
{ label: "About", href: "#about" },
{ label: "Contact", href: "#contact" },
]
: [
{ label: "Services", href: "/#services" },
{ label: "Pricing", href: "/#pricing" },
{ label: "Topics", href: "/#topics" },
{ label: "Approach", href: "/#approach" },
{ label: "Samples", href: "/work", key: "work" },
{ label: "About", href: "/about", key: "about" },
{ label: "Briefing", href: "/#briefing" },
{ label: "Contact", href: "/#contact" },
];
---

Expand Down
16 changes: 16 additions & 0 deletions src/lib/engagements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Single source of truth for the four engagement-tier keys. Imported by the
// sample registry (samples.ts), the client briefing form (BriefingForm.astro),
// and the server-side validation (server/briefing.ts) so a renamed or added
// tier can't silently drift between the homepage links and the form/API.
//
// Keep this module data-only (no display strings, no sample data) so the client
// form bundle stays tiny and the server can import it without pulling in page code.

export const ENGAGEMENT_KEYS = [
"expert-call",
"second-opinion",
"technical-brief",
"workshop",
] as const;

export type EngagementKey = (typeof ENGAGEMENT_KEYS)[number];
57 changes: 57 additions & 0 deletions src/lib/pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Single source of truth for the site's public, indexable pages. Both the
// sitemap (sitemap.xml.ts) and the LLM index (llms.txt.ts) generate from this,
// so adding a page — or a new sample to samples.ts — updates both automatically.

import { samples } from "./samples";

export interface SitePage {
/** Absolute path from the site root, e.g. "/work". */
path: string;
/** Human title, used in llms.txt. */
title: string;
/** One-line description, used in llms.txt. */
description: string;
/** Sitemap hint for how often the page changes. */
changefreq: "weekly" | "monthly";
/** Sitemap relative priority, "0.0".."1.0". */
priority: string;
}

// Hand-maintained, non-sample pages. Sample pages are appended from the
// registry below so published briefs never need a manual entry here.
const staticPages: SitePage[] = [
{
path: "/",
title: "Home",
description: "Overview of services, pricing, approach, and how to request a briefing.",
changefreq: "weekly",
priority: "1.0",
},
{
path: "/about",
title: "About",
description:
"Background on founder Pedro Paulo Vezza Campos, his Microsoft career, and areas of expertise.",
changefreq: "monthly",
priority: "0.8",
},
{
path: "/work",
title: "Sample Work",
description:
"Published sample deliverables, grouped by engagement type, that you can read before commissioning your own.",
changefreq: "weekly",
priority: "0.8",
},
];

export function sitePages(): SitePage[] {
const samplePages: SitePage[] = samples.map((s) => ({
path: s.href,
title: s.title,
description: s.summary,
changefreq: "monthly",
priority: "0.7",
}));
return [...staticPages, ...samplePages];
}
57 changes: 57 additions & 0 deletions src/lib/samples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Registry of published sample deliverables. Each engagement type (the four
// service tiers on the home page) can accumulate multiple samples over time.
// The home page cards and the /work showcase both read from here, so adding a
// new sample is a single entry — no page edits.

import type { EngagementKey } from "./engagements";

export type { EngagementKey };

export interface EngagementType {
key: EngagementKey;
n: string;
name: string;
}

// Mirrors the four service tiers (n = "01".."04") on the home page.
export const engagementTypes: EngagementType[] = [
{ key: "expert-call", n: "01", name: "Expert Call" },
{ key: "second-opinion", n: "02", name: "Second Opinion" },
{ key: "technical-brief", n: "03", name: "Technical Brief" },
{ key: "workshop", n: "04", name: "Workshop" },
];

export interface Sample {
slug: string;
title: string;
summary: string;
href: string;
engagement: EngagementKey;
/** ISO date, used for sorting (newest first) and display. */
date: string;
/** Short label for the deliverable shape, e.g. "Technical brief". */
format: string;
}

export const samples: Sample[] = [
{
slug: "advanced-dataverse-and-dynamics-365-security-model",
title: "Advanced Dataverse & Dynamics 365 Security Model",
summary:
"A layer-by-layer walkthrough of the Dataverse security model — business units, access levels, Append vs Append To, owner and access teams, hierarchy security, and the sharing patterns that quietly wreck performance.",
href: "/briefs/advanced-dataverse-and-dynamics-365-security-model",
engagement: "technical-brief",
date: "2026-05-28",
format: "Technical brief",
},
];

export function samplesFor(key: EngagementKey): Sample[] {
return samples
.filter((s) => s.engagement === key)
.sort((a, b) => (a.date < b.date ? 1 : -1));
}

export function sampleCountFor(key: EngagementKey): number {
return samples.filter((s) => s.engagement === key).length;
}
12 changes: 9 additions & 3 deletions src/lib/server/briefing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import type { Env } from "./env";
import { sendBriefingEmail } from "./graph";
import type { Telemetry } from "./telemetry";
import { timed } from "./telemetry";
import { ENGAGEMENT_KEYS, type EngagementKey } from "../engagements";

const ENGAGEMENT_TYPES = ["expert-call", "second-opinion", "technical-brief", "workshop"] as const;
type EngagementType = (typeof ENGAGEMENT_TYPES)[number];
const ENGAGEMENT_TYPES = ENGAGEMENT_KEYS;
type EngagementType = EngagementKey;

interface BriefingInput {
name: string;
Expand Down Expand Up @@ -70,8 +71,13 @@ async function verifyTurnstile(
token: string,
remoteIp: string | null,
): Promise<TurnstileResult> {
// In dev the client uses Cloudflare's always-pass dummy sitekey; pair it with the
// matching dummy secret so siteverify succeeds without a real TURNSTILE_SECRET_KEY.
const secret = import.meta.env.DEV
? "1x0000000000000000000000000000000AA"
: env.TURNSTILE_SECRET_KEY;
const body = new FormData();
body.append("secret", env.TURNSTILE_SECRET_KEY);
body.append("secret", secret);
body.append("response", token);
if (remoteIp) body.append("remoteip", remoteIp);
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/about.astro
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ const personLd = {
on your Microsoft decision?
</h2>
<a
href="/#briefing"
href="/#contact"
class="inline-flex items-center gap-3 bg-foreground text-background px-8 py-4 text-sm font-black uppercase tracking-[0.2em] hover:bg-accent transition-colors"
>
Request a briefing <span>→</span>
Expand Down
Loading
Loading