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ła01 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.
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 link03 · 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
Nowy link
Link CV dla „Przykładowa Klinika Sp. z o.o.” — utworzony 21.08.2026, otwarcia CV: 2
Dopasowanie: nazwa firmy, e-mail albo domenaGotowe
jakubnowak.eu/go/cv/3f9c2a7e…cvjakubnowak.eu/go/p/3f9c2a7e…pf- 1 Ochrona zdrowia
- 2 Aplikacja pacjenta
- 3 Dane medyczne
Prawdziwy panel
Formularz nowego linku
Dane fikcyjne// 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…" });
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.
- linkedin.com/in/… bez zmianhttps://linkedin.com/in/…
- jakubnowak.eu podmianahttps://www.jakubnowak.eu/go/p/3f9c…pf
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 domeny
bez 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
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));
}
}
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
- 01Aplikacja pacjentaOchrona zdrowia
- 02Dane medyczneOchrona zdrowia
- 03Sklep headlessE-commerce
- 04Onboarding bankowyFintech
- 05Program lojalnościowyCRM
- 06Identyfikacja markiBranding
- 07Aplikacja mobilnaMobile
- 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
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;
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.
- Rekruterklika link w PDF
- /go/p/<token>funkcja serverless
- Postgreszapis wejścia
- PostHogoutreach_link_visited
- 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
- 01Kolejność AI z linku w CVciasteczko cv_order (10 min)
- 02Projekt wyróżnionypole featured w Sanity
- 03Popularność z 30 dniwidok HogQL w PostHogu
- 04Kolejność z CMSprzy remisie i braku danych
// 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;
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
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ść.
- Skrypt ładuje się po zdarzeniu load
- Czekam na interakcję
- Interakcja
- PostHog działa
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()captureImmediate()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.
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 } : {}),
});
}
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.
outreach_/go/cv i /go/plink_ visited link_type, outreach_tokenSerwer$groupidentifyserwernazwy grup linku i firmySerwerservice_cała aplikacjadegraded component, reason, severitySerwerportfolio_wejście z parametrem ridlanded outreach_tokenPrzeglądarkawork_strona case studyproject_ viewed project_slug, project_categoryPrzeglądarkacontact_formularz kontaktowyform_ submitted message_length, sourcePrzeglądarkacopilot_Copilotmessage_ sent message_length, word_countCopilotcopilot_Copilotresponse_ received response_length, response_time_msCopilotcopilot_Copilotmessage_ feedback helpfulCopilot
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ą.
- Wejście na case studywork_project_vieweddane w PostHogu
- v_clean_project_views90 dni, ujednolicone slugidane w PostHogu
- v_popular_projects_30dmaterializacja co godzinędane w PostHogu
- /api/posthog-insightsLIMIT 10 · cache 300 sdane w aplikacji
- usePosthogInsightlocalStorage · 1 hdane w aplikacji
- WorkGridmalejąco, remis → CMSdane w aplikacji
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
{
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`,
},
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
Ślad jednego wywołania
- gemini-2.5-flash
- inputTokens
- outputTokens
- functionId: copilot-chat
treść promptutreść odpowiedziosoba
after(flushPostHogOtel)
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,
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_degradedcopilot.systemPromptprompt_load_failedmediumcms.projectssanity_fetch_failedhigh / mediumcms.blogsanity_fetch_failedmediumemail.sendresend_send_failedhighcontact.ownerEmailowner_email_failedmediumcron.generateBlogPostcron_blog_failedmedium
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),
]);
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-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
Porozmawiajmy
kontakt@jakubnowak.eu