Produkt · AI · Analityka

Rok: 2026Rola: Projektowanie produktu, programowanie, AI i analityka

Osobny link do CV i portfolio dopasowane do każdej firmy. AI układa projekty pod rekrutera, a PostHog mówi, co zadziałało.

Przewiń, by zobaczyć, jak to działa
Przykład
Jakub Nowak — CVPDF · 4 strony

01 Bazowe CV — ten sam plik dla wszystkich

osobny PDF i link dla każdej firmy
1 : 1
kolejność projektów pod firmę
AI
wskaźników w panelu statystyk
7
drogi danych do PostHoga
3

01 · Rozwiązanie

Każda firma dostaje własne CV i portfolio.

Każde CV ma unikalny link do portfolio, w którym AI ustawia projekty pod konkretnego pracodawcę. Po wysłaniu widzę, czy ktoś je otworzył i które projekty przyciągnęły uwagę.

CV
Osobny plik z unikalnym linkiem dla każdej firmy
Portfolio
Kolejność projektów ustalona przez AI pod firmę
Wiedza
Otwarcia, przejścia do portfolio i projekty, które przyciągają uwagę

Problem

To samo CV i portfolio dla firmy medycznej, sklepu i banku — a po wysłaniu cisza.

  • 1wersja CV dla każdej firmy
  • 1kolejność projektów dla wszystkich
  • 0informacji, czy ktoś otworzył CV

02 · Cała ścieżka

Od formularza do pomiaru w sześciu krokach.

Kto i co robi na każdym etapie — ja w panelu, system na serwerze i rekruter po drugiej stronie.

Ja

Wypełniam formularz w panelu

Firma, osoba kontaktowa, wersja CV (PL/EN) i adres strony firmy. Nazwa firmy uzupełnia się ze strony, a panel ostrzega, jeśli CV mogło już trafić do tego pracodawcy.

Nowy link

03 · Panel

Para linków w kilka sekund — z postępem na żywo.

Panel jest tylko dla mnie: logowanie przez LinkedIn i klucz dostępu (passkey). Serwer nie odpowiada jednym JSON-em na końcu, tylko strumieniem NDJSON — panel czyta go na bieżąco i pokazuje, który krok właśnie trwa.

Ranking AI jest „best effort”: gdy się nie uda, link i tak powstaje, z domyślną kolejnością. Już podczas wpisywania panel sprawdza, czy CV mogło wcześniej trafić do tej firmy — w linkach generowanych ręcznie i w wysyłkach seryjnych.

  • 8kroków raportowanych przez serwer
  • 500 mspo ostatniej zmianie — kontrola duplikatów
  • 1 UUIDna parę tokenów …cv i …pf
jakubnowak.euDane fikcyjne

Nowy link

FirmaPrzykładowa Klinika Sp. z o.o.
Osoba kontaktowaAnna Przykładowa
Strona firmy (URL)https://klinika.example
Wersja CVPolska (PL)
CV mogło już trafić do tego pracodawcy

Link CV dla „Przykładowa Klinika Sp. z o.o.” — utworzony 21.08.2026, otwarcia CV: 2

Dopasowanie: nazwa firmy, e-mail albo domena
Personalizuj kolejność projektów przez AI
Gotowe ✓100%

Gotowe

Link do CVjakubnowak.eu/go/cv/3f9c2a7e…cv
Link do portfoliojakubnowak.eu/go/p/3f9c2a7e…pf
Początek kolejności AI
  1. 1 Ochrona zdrowia
  2. 2 Aplikacja pacjenta
  3. 3 Dane medyczne
src/app/api/crm/cv-links/route.tsTypeScript
// Stream NDJSON progress events so the admin UI can render a live progress bar
// and show exactly which step is running underneath (CMS fetch → company-site
// analysis → AI ranking → DB write). One JSON object per line.
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
  async start(controller) {
    const send = (event: Record<string, unknown>) =>
      controller.enqueue(encoder.encode(JSON.stringify(event) + "\n"));
 
    try {
      send({ type: "progress", progress: 12, message: "Walidacja danych odbiorcy…" });
Początek strumienia postępu

04 · PDF

Jeden plik, unikalny link.

Każdy PDF powstaje na żądanie z bazowej wersji CV. Serwer podmienia cel linku do jakubnowak.eu na adres tej jednej firmy — wygląd i treść CV zostają bez zmian.

Jakub NowakCV · 4 strony · PL lub EN
Przykład

Każde otwarcie w przeglądarce trafia na serwer (Cache-Control: private, no-store), więc wiem, czy i kiedy CV zostało otwarte. Pobranie PDF z panelu, żeby wysłać go jako załącznik, nie zawyża licznika.

  • jakubnowak.eu i subdomeny→ /go/p/<token>
  • LinkedIn i inne domenybez zmian
  • Linki już śledzone (/go/…)bez zmian
  • Bazowe PDF-y poza katalogiem publicznym
  • Wczytane raz na instancję funkcji
  • Personalizacja w pamięci
  • Błąd → statyczna polska wersja CV
src/lib/cvPdf.tsTypeScript
for (const page of pdfDoc.getPages()) {
  const annots = page.node.lookupMaybe(PDFName.of("Annots"), PDFArray);
  if (!annots) continue;
 
  for (let i = 0; i < annots.size(); i++) {
    const annot = annots.lookupMaybe(i, PDFDict);
    if (!annot) continue;
    if (annot.get(PDFName.of("Subtype")) !== PDFName.of("Link")) continue;
 
    const action = annot.lookupMaybe(PDFName.of("A"), PDFDict);
    if (!action) continue;
 
    const uri = decodeUri(action.lookup(PDFName.of("URI")));
    if (!uri || !isPortfolioUri(uri)) continue;
 
    action.set(PDFName.of("URI"), PDFString.of(trackedUrl));
  }
}
Przepisanie adnotacji linków na każdej stronie PDF

05 · Ranking AI

Najpierw to, co ważne dla tej firmy.

Ranking powstaje raz, przy generowaniu linku. Model dostaje tekst strony firmy i katalog projektów, a zwraca pełną kolejność wszystkich identyfikatorów — biorąc pod uwagę branżę, model biznesowy, kanały sprzedaży i prawdopodobne potrzeby.

Wybierz firmę i zobacz, jak zmienia się siatka. Firmy i kolejności w tej scenie są przykładowe.

„Nowoczesna opieka medyczna — umów wizytę online, wyniki badań w aplikacji pacjenta…”
Tekst strony
czysty tekst, do 4000 znaków, limit 6 s
Katalog (JSON)
id, title, subtitle, category, client, year
Wynik
tablica JSON, każde id dokładnie raz
Przykład
  1. 01Aplikacja pacjentaOchrona zdrowia
  2. 02Dane medyczneOchrona zdrowia
  3. 03Sklep headlessE-commerce
  4. 04Onboarding bankowyFintech
  5. 05Program lojalnościowyCRM
  6. 06Identyfikacja markiBranding
  7. 07Aplikacja mobilnaMobile
  8. 08Kampania społecznaKampania

Nie wierzę modelowi na słowo

Odpowiedź przechodzi przez walidację: nieznane i powtórzone identyfikatory odpadają, a pominięte projekty trafiają na koniec. Zapisana kolejność zawsze obejmuje cały katalog, a funkcja nigdy nie rzuca wyjątku.

  • health-app
  • med-data
  • made-up
  • mobile
  • health-app
  • loyalty
  • znane
  • nieznane — odpada
  • powtórzone — odpada
  • pominięte — na koniec
src/lib/projectRanking.tsTypeScript
const seen = new Set<string>();
const ordered: string[] = [];
for (const item of parsed) {
  if (typeof item === "string" && known.has(item) && !seen.has(item)) {
    seen.add(item);
    ordered.push(item);
  }
}
if (ordered.length === 0) return [];
 
// Append any ids the model omitted, preserving the catalog order, so the
// stored order always covers every project.
for (const id of allIds) {
  if (!seen.has(id)) ordered.push(id);
}
return ordered;
Walidacja odpowiedzi modelu

06 · Kliknięcie

Jedno kliknięcie, cztery rzeczy naraz.

Link z PDF prowadzi do /go/p/<token>. Serwer zapisuje wejście, wysyła zdarzenie do PostHoga, ustawia do dwóch ciasteczek ważnych 10 minut i przekierowuje na stronę główną z parametrem rid i tagami UTM.

Przykład
  1. Rekruterklika link w PDF
  2. /go/p/<token>funkcja serverless
  3. Postgreszapis wejścia
  4. PostHogoutreach_link_visited
  5. Strona główna?rid · utm_source=outreach

Błąd analityki nie psuje przekierowania — każdy krok ma własną obsługę błędów. Strona główna czyta ciasteczko dopiero po zamontowaniu w przeglądarce, więc HTML z serwera jest taki sam dla wszystkich.

Kto wygrywa przy sortowaniu siatki

  1. 01Kolejność AI z linku w CVciasteczko cv_order (10 min)
  2. 02Projekt wyróżnionypole featured w Sanity
  3. 03Popularność z 30 dniwidok HogQL w PostHogu
  4. 04Kolejność z CMSprzy remisie i braku danych
src/components/work/WorkGrid.tsxTSX
// Outreach override: when an AI-ranked order is present (CV link recipient),
// lead with those projects in that exact order, then the rest in their
// current order. Wins over popularity/featured/industry sorting.
const finalProjects =
  projectOrderIds && projectOrderIds.length > 0
    ? (() => {
        const rank = new Map(projectOrderIds.map((id, idx) => [id, idx]));
        const ranked = popularitySorted
          .filter((p) => rank.has(p.id))
          .sort((a, b) => rank.get(a.id)! - rank.get(b.id)!);
        const rest = popularitySorted.filter((p) => !rank.has(p.id));
        return [...ranked, ...rest];
      })()
    : popularitySorted;
Kolejność z linku nakładana na sortowanie po popularności

07 · PostHog

PostHog jako centrum analityki — nie tylko wykresy.

Panel oparty na własnej bazie danych odpowiada na pytanie, czy link zadziałał. PostHog (region UE) odpowiada na pytanie, co działa na stronie — i część danych wraca do produktu.

Najedź na źródło albo funkcję, żeby zobaczyć, co z czym jest połączone.

Skąd płyną dane

PostHog (UE)

Do czego to służy

08 · Zbieranie danych

Zdarzenia, które nie mogą zginąć.

Skrypt PostHoga i zdarzenia idą przez reverse proxy na mojej domenie, a inicjalizacja czeka na pierwszą interakcję albo 8 sekund — natychmiastowy start był jedną z przyczyn spadku mobilnego wyniku PageSpeed do 35/100.

Kiedy startuje PostHog

Przy wejściach z linku w CV (parametr rid) PostHog startuje od razu, żeby krótka wizyta nie przepadła. Jeśli interakcja nastąpi, zanim skrypt się załaduje, inicjalizacja czeka na niego, zamiast przepaść.

  1. Skrypt ładuje się po zdarzeniu load
  2. Czekam na interakcję
  3. Interakcja
  4. PostHog działa
8.0albo 8 s

Przewijanie celowo nie uruchamia startu — Lighthouse też przewija stronę.

Serwer: wyścig z zamrożeniem funkcji

Endpointy /go działają jako funkcje serverless, a instancja może zostać zamrożona tuż po odpowiedzi. Buforowane capture() gubiło outreach_link_visited. captureImmediate kończy się dopiero po dostarczeniu, a flush w bloku finally działa nawet po błędzie.

capture()
odpowiedź❄ zamrożenie
captureImmediate()
odpowiedź❄ zamrożenie

Grupy: link i firma, tylko w tej sesji

Każdy link to grupa outreach_link, każda firma to grupa company. Przy kolejnej wizycie bez parametru rid kod porównuje identyfikator sesji i w razie różnicy czyści grupy — zdarzyło się, że moje własne testy trafiały do firmy z linku.

Sesja z linku (rid)
outreach_linkcompany
Kolejna sesja
outreach_linkcompany∅ grupy wyczyszczone
src/lib/posthogNode.tsTypeScript
export async function captureServerEvent(
  distinctId: string,
  event: string,
  properties?: Record<string, any>,
  groups?: Record<string, string>
): Promise<void> {
  const client = getPostHogClient();
 
  await client.captureImmediate({
    distinctId,
    event,
    properties,
    ...(groups ? { groups } : {}),
  });
}
Wysyłka zdarzenia z serwera

09 · Katalog zdarzeń

Własne zdarzenia obok autocapture.

PostHog zbiera automatycznie odsłony, wyjścia ze strony, autocapture, heatmapy i nieobsłużone wyjątki. Resztę opisują własne zdarzenia — w strefie projektów specjalnych PostHog w przeglądarce nie działa.

9
  • outreach_link_visited/go/cv i /go/plink_type, outreach_tokenSerwer
  • $groupidentifyserwernazwy grup linku i firmySerwer
  • service_degradedcała aplikacjacomponent, reason, severitySerwer
  • portfolio_landedwejście z parametrem ridoutreach_tokenPrzeglądarka
  • work_project_viewedstrona case studyproject_slug, project_categoryPrzeglądarka
  • contact_form_submittedformularz kontaktowymessage_length, sourcePrzeglądarka
  • copilot_message_sentCopilotmessage_length, word_countCopilot
  • copilot_response_receivedCopilotresponse_length, response_time_msCopilot
  • copilot_message_feedbackCopilothelpfulCopilot

10 · Popularność

PostHog układa siatkę projektów.

Źródłem jest jedno zdarzenie, work_project_viewed — także przy wejściu bezpośrednim, bo zdarzenie czeka w kolejce, aż PostHog się uruchomi. Dwa zapisane widoki HogQL, zakładane przez API, liczą wejścia z 30 dni.

To pętla sprzężenia zwrotnego: popularne projekty są wyżej, więc częściej się je ogląda — dlatego kolejność z linku w CV ma pierwszeństwo przed popularnością.

  1. Wejście na case studywork_project_vieweddane w PostHogu
  2. v_clean_project_views90 dni, ujednolicone slugidane w PostHogu
  3. v_popular_projects_30dmaterializacja co godzinędane w PostHogu
  4. /api/posthog-insightsLIMIT 10 · cache 300 sdane w aplikacji
  5. usePosthogInsightlocalStorage · 1 hdane w aplikacji
  6. WorkGridmalejąco, remis → CMSdane w aplikacji
popularne wyżej → częściej oglądane

Jak długo każda warstwa trzyma dane (minuty)

  • Materializacja widoku60 minco godzinę
  • Cache danych Next.js5 minrevalidate: 300
  • CDN Vercel15 mins-maxage 300 + SWR 600
  • localStorage w przeglądarce60 minTTL 1 h

Warstwy się sumują (ok. 2 h 20 min) — świadomy kompromis między świeżością a szybkością i kosztem zapytań.

Odznaka „🔥 popularne dziś”

Endpoint live-activity czyta surowe zdarzenia z ostatnich 24 godzin. Siatka odpytuje go co 5 minut i oznacza najczęściej oglądany projekt. Pulsowanie zmienia tylko przezroczystość i wyłącza się przy prefers-reduced-motion.

  • Ochrona zdrowia popularne dziś
  • E-commerce
  • Fintech
  • Branding
scripts/setup-posthog-views.mjsJavaScript
{
  name: 'v_popular_projects_30d',
  materialize: true,
  syncFrequency: 'every_hour',
  description: 'Aggregated project click counts (last 30 days, deduplicated slugs)',
  query: `SELECT
  project_slug AS project_slug,
  count() AS clicks
FROM v_clean_project_views
WHERE timestamp > now() - INTERVAL 30 DAY
GROUP BY project_slug`,
},
Materializowany widok popularności w hurtowni PostHoga

11 · Copilot

Prompt w PostHogu, telemetria bez treści.

Asystent AI na stronie (Gemini 2.5 Flash) pobiera prompt systemowy z modułu zarządzania promptami w PostHogu — mogę zmienić jego treść bez wdrożenia. Wywołania modelu śledzi OpenTelemetry.

Spany zawierają model, zużycie tokenów i czas trwania — bez treści promptu i odpowiedzi i bez przypisania do osoby. Po każdej odpowiedzi bufor spanów opróżniam w after(), żeby nie przepadły przy zamrożeniu funkcji.

Skąd Copilot bierze prompt

CopilotPostHog
cache w pamięci: 5 min

Ślad jednego wywołania

ai.generateText
ai.generateText.doGenerate
  • gemini-2.5-flash
  • inputTokens
  • outputTokens
  • functionId: copilot-chat
  • treść promptu
  • treść odpowiedzi
  • osoba

after(flushPostHogOtel)

src/app/api/copilot/route.tsTypeScript
experimental_telemetry: {
  isEnabled: true,
  functionId: "copilot-chat",
  // Spans keep the model, token usage, latency and the metadata below,
  // not the text: the system prompt carries the visitor context and the
  // messages are the visitor's own words — and spans aren't tied to a
  // person.
  recordInputs: false,
  recordOutputs: false,
Telemetria w wywołaniu generateText, bez treści promptu i odpowiedzi (fragment)

12 · Monitoring

Wiem o awarii, zanim zauważy ją odwiedzający.

Tam, gdzie aplikacja przechodzi na plan awaryjny albo operacja po cichu się nie udaje, jedna funkcja zgłasza service_degraded z komponentem, powodem i wagą — pod jednym systemowym identyfikatorem, równolegle z wyjątkiem dla błędów o wysokiej wadze.

service_degraded
  • copilot.systemPromptprompt_load_failedmedium
  • cms.projectssanity_fetch_failedhigh / medium
  • cms.blogsanity_fetch_failedmedium
  • email.sendresend_send_failedhigh
  • contact.ownerEmailowner_email_failedmedium
  • cron.generateBlogPostcron_blog_failedmedium
src/lib/observability.tsTypeScript
const distinctId = d.distinctId ?? SYSTEM_DISTINCT_ID;
const properties = {
  component: d.component,
  reason: d.reason,
  severity: d.severity,
  error_message: errorMessage(d.error),
  ...d.meta,
};
 
// High-severity errors also surface in PostHog Error Tracking with a stack.
// Both are sent at once and awaited, so a frozen instance can't drop them.
await Promise.allSettled([
  d.severity === "high" && d.error instanceof Error
    ? captureServerException(d.error, distinctId, {
        component: d.component,
        reason: d.reason,
        ...d.meta,
      })
    : undefined,
  captureServerEvent(distinctId, "service_degraded", properties),
]);
Zdarzenie i wyjątek w jednym miejscu

13 · Technologia

Zbudowane, zmierzone, poprawione.

Jedna aplikacja Next.js 16 na Vercelu. Para tokenów z jednego losowego UUID nie zawiera danych firmy, a panel chroni logowanie przez LinkedIn i klucz dostępu.

Aplikacja
Next.js 16 · App Router · strumieniowanie
Baza danych
Vercel Postgres (Neon) · Drizzle ORM
Treści
Sanity
AI
Vercel AI SDK · Gemini 2.5 Flash · AI Gateway
PDF
pdf-lib
Analityka
PostHog EU · posthog-node · OpenTelemetry
Dostęp do panelu
LinkedIn (NextAuth) + klucz dostępu
  • 1 : 1osobny PDF i link dla każdej firmy
  • AIkolejność projektów pod stronę firmy
  • 7wskaźników w panelu statystyk
  • 3drogi danych do PostHoga