CLAUDE.md
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
bundle install # Install dependenciesbundle exec jekyll serve # Run local dev server with live reloadbundle exec jekyll build # Build to _site/pkill -f "jekyll serve" # Kill the serverNote: _config.yml changes require a server restart — livereload does not pick them up.
Architecture
This is a Jekyll static site deployed to GitHub Pages at prlic.io (CNAME). It uses a remote theme (iprlic/creative-theme-jekyll-new via jekyll-remote-theme plugin), so layout and styling files are not present locally — they are downloaded at build time to a temp directory (/var/folders/.../jekyll-remote-theme-*/).
One page per language, plus two redirect stubs:
| File | URL | Lang | ref | Purpose |
|---|---|---|---|---|
index.md | / | en | home | The whole offer, one page |
hr/index.md | /hr/ | hr | home | Croatian equivalent |
ai-advisory.md | /ai-advisory/ | — | — | Redirect to /#packages |
hr/ai-savjetovanje.md | /hr/ai-savjetovanje/ | — | — | Redirect to /hr/#packages |
The site used to be split into an engineering homepage and a separate /ai-advisory/ landing page. They were merged in Sept 2026: the split duplicated four sections (clients, contact, who, crosslink) across two pages and doubled the translation burden for a solo consultancy. The two old URLs are kept as redirect stubs because they were given out in outreach — do not delete them. They use a meta-refresh plus window.location.replace, not jekyll-redirect-from, so no extra plugin is needed on GitHub Pages’ classic build. Both carry sitemap: false and noindex, and have no ref, so they stay out of the sitemap and the hreflang graph.
Editing the site means editing index.md and hr/index.md. Both are single-page layouts defined entirely via YAML frontmatter sections, where each section’s type maps to a local _includes/ override.
Section-level parity between languages is required — any section added to one language must be added to the other, or the language switcher drops the visitor onto a page missing content they were just reading. Item-level divergence inside a section is intentional and should not be “fixed”: the Croatian page targets a different market (see below). It currently carries 9 FAQ questions to the English page’s 7.
The Croatian page targets Croatia specifically
The English page sells to regulated EU/US mid-market on a data-residency wedge. The Croatian page sells to Croatian companies and public sector, where that wedge is much weaker: only ~15% of Croatian firms with 10+ employees use any AI technology, so the buyer’s question is “where do we start and does it pay off”, not “can you keep our data in the EU”.
Consequences baked into the Croatian page, all deliberate:
- Sectors follow the draft national AI plan to 2032 (public administration, healthcare, energy, transport, tourism, financial services), not the English page’s regulated-industry list.
- The Assessment lists a deliverable framed as technical groundwork an EU-funds consultant can use. Never claim a call will fund the assessment itself. On the SME digitalisation calls, advisory is an eligible cost but capped very low — €2,000 in the last such call, against grants of €30,000–€120,000. The money is in implementation (tools, software, equipment). The FAQ says this plainly with the figure; keep it that way, and update the figure when a new call publishes its terms.
- Croatian copy uses the vocabulary the market already uses, harvested from PwC Croatia, the EDIH network, HGK and the grant call documents:
digitalna zrelost,slučajevi primjene,prihvatljivi troškovi,savjetodavne usluge,projektni prijedlog,MSP,računalni vid,strojno učenje,uvođenje AI-ja u poslovanje,smanjite rizik od neuspjeha. Do not “improve” these into more literal translations of the English — they are the terms buyers and funds consultants search for and recognise. - Two extra FAQ entries: EU co-financing, and working in Croatian / on-site in Croatia.
- “Fractional AI vodstvo” was renamed “AI vodstvo bez stalnog zaposlenja” — “fractional” does not parse for a Croatian mid-market buyer.
- Prices are identical to the English page and stay that way. The Croatian budget problem is solved through co-financing and EDIH routes, not discounting.
Internationalisation
No plugin. GitHub Pages builds this repo with its classic build, so only whitelisted plugins are available and jekyll-polyglot is not one of them. i18n is done natively:
- Each page declares
lang: en|hrandref: <translation key>. Pages sharing arefare translations of each other. _includes/nav.htmlfinds the sibling withsite.pages | where: "ref" | where_exp: "p.lang == alt_lang"and points the language switcher at it, so switching from/ai-advisory/lands on/hr/ai-savjetovanje/rather than the homepage. It falls back to that language’s home page if no sibling exists._layouts/base.htmlsets<html lang>and emitshreflangalternates plusx-default(English) from the samerefgrouping._data/menus.yml,_data/footer.ymland_data/i18n.ymlare all keyed by language code.i18n.ymlholds only strings hardcoded in includes; anything in page frontmatter is translated there instead.- English lives at
/, Croatian under/hr/. Croatian slugs are translated too (ai-savjetovanje, notai-advisory).
Croatian YAML gotcha
A list item containing : is parsed by YAML as a mapping, not a string, and renders as {"key"=>"value"} on the page. Croatian copy hits this far more often than English because of constructions like Program za upravu: što financirati. Quote any list item containing a colon. To check all four pages at once, parse each file’s frontmatter and assert every entry in deliverables, fit, not_fit, outcomes and questions is a str.
Titles and SEO
jekyll-seo-tag owns the <title> tag. Do not add one to _layouts/base.html — that produced two <title> tags on every page for a long time. site.title is the site name only (Ivan Prlić); the role sits in site.tagline. Each page sets its own title, and seo-tag renders <page title> | <site title>.
Social preview images. seo-tag reads page.image, not site.image — setting only the latter silently emits no og:image at all, which is what happened here. The site-wide default is set via the defaults block in _config.yml; pages override it with their own image:. Cards are generated by tools/generate-og-images.py (needs rsvg-convert); edit the CARDS dict there and re-run rather than editing PNGs.
Frontend assets
There is no jQuery, no Bootstrap JS, no Font Awesome, and no CDN dependency except Google Fonts. That is deliberate — do not reintroduce them.
- JS is one local file,
assets/js/site.js: mobile nav toggle, navbar-scrolled class, and IntersectionObserver nav highlighting. It replaced the theme’screative.js, which pulled in jQuery, Bootstrap’s bundle,jquery-easing(foreaseInOutExpo) andmagnific-popup(for a#portfoliolightbox this site does not have). - Testing scroll behaviour in the browser tool: programmatic
window.scrollTodispatches no scroll events andIntersectionObservernever fires inside iframe probes, so anything scroll-driven reads as broken there. Use thecomputerscroll action (real input events) on a top-level page instead, and forcescroll-behavior: autofirst — smooth scrolling is suppressed when the window is unfocused. - The sticky CTA is visible by default and hidden by a class. Do not switch it to the
hiddenattribute: the UA stylesheet’s[hidden] { display: none }beats a class-leveldisplayrule and kills the slide transition, and default-visible degrades correctly without JS. - Anchor scrolling is CSS, not JS:
scroll-behavior: smoothplus[id] { scroll-margin-top: 72px }inmain.scss. This also fixes cold loads on a URL like/#work, which script-driven scrolling never handled. Don’t move it back into JS. - Icons are an inline SVG sprite,
_includes/icon-sprite.html, injected once at the top of<body>and referenced as<svg class="icon"><use href="#fa-brain"/></svg>. Symbol ids match thefa-*names already used in front matter. Regenerate withpython3 tools/build-icon-sprite.pyafter adding an icon — add it to theICONSdict first. Inline rather than an external sprite file because Safari does not support cross-file<use href="sprite.svg#id">. - Inter is loaded by a single non-blocking
<link>inbase.html. Do not add a CSS@importfor it inmain.scss— that fetches it twice and blocks rendering.
Page flow
Order follows the order buyers ask questions:hero → clients → packages → services → work → who → faq → contacts.
Prices land at ~1.9 screens because header.proof puts a price chip at the fold; without it thefirst price is three screens down. services sits after packages on purpose: the pricedadvisory ladder is the lead offer, and bespoke engineering is the “also available” tail.
Backgrounds must keep alternating (subtle / white / subtle / white / dark / subtle / dark) —check the neighbours before adding background_style: bg-subtle to a new section.
Sections are sized with padding, not min-height: 80vh. The old min-heights stretched ~450pxof content into 720px boxes, costing ~500px of dead space per page.
Keep the Calendly CTA count low: the featured package, the nav button, the sticky mobile bar andthe contact section. Five identical “Book an intro call” links inside one section gave the readerno signal about which to click.
The whole page is ~8.8 screens at 1440x900. The two pages it replaced were 7.1 + 11.2.
Accessibility
Every page has a <main id="main"> landmark and a skip link (.skip-link, off-screen untilfocused). Touch targets are forced to 44px minimum below 992px — nav, footer, contact chips,FAQ summaries, package CTAs and the badge links were all under it.
All body text meets WCAG AA (4.5:1). The greys are chosen for it: #5b6b81 is the lightest grey that passes on white, #f8fafc and #f1f5f9 alike, and #15803d is the green that passes on the #f0fdf4 chip background. #94a3b8 and #16a34a both fail — don’t reach for them. On the navy sections, white text needs at least 0.5 alpha. Public sector procurement in the EU can require conformance, and that is a target segment.
_config.yml holds global settings: site title, nav title, URL, OG image, social links, plugin config.
_data/menus.yml controls nav links, keyed by language. There is a single header menu now; a page selects its language with lang. The last item in a list renders as a primary CTA button; “Book a Call” is appended separately in _includes/nav.html and its label/URL can be overridden per page with nav_cta_label / nav_cta_url. Set highlight: true on an item to accent it.
Anchor-only URLs (#work) are emitted bare and get the theme’s smooth scroll; everything else goes through relative_url. Do not pipe #foo through relative_url — it becomes /#foo and jumps to the homepage from any subpage.
_data/footer.yml contains the footer copyright string.
The _site/ directory is build output — never edit it directly, it’s gitignored.
Local Overrides
All layout/include overrides live locally and take precedence over the remote theme:
| File | Purpose |
|---|---|
_layouts/base.html | Font (Inter), JSON-LD schema, deferred JS, active nav JS |
_layouts/home.html | Removes text-uppercase from hero h1, adds hero subtitle |
_includes/nav.html | Language-keyed menu, two CTA buttons, language switch, anchor-safe hrefs |
_includes/about.html | Two-column layout with photo, badges, tech stack, and the compact certs chip row (certifications no longer have their own section) |
_includes/services.html | Service cards with icon circles (text is markdownified, so links work) |
_includes/case-studies.html | Case study cards with outcome chips |
_includes/contact.html | Dark section with contact buttons; watermark sets the giant background word |
_includes/footer.html | Two-column: copyright left, links right |
_includes/packages.html | Priced offer cards + OfferCatalog JSON-LD. featured: true = full width, accent border, two-column deliverables |
_includes/testimonials.html | Quote cards. Renders nothing until real quotes are supplied — never invent them |
_includes/clients.html | Client name chips. Takes clients[] of { name } |
_includes/faq.html | Native <details> accordion + FAQPage JSON-LD. No JS; answers stay crawlable while collapsed |
Styling
assets/css/main.scss— all custom CSS. Compiled by Jekyll.- Design: dark navy (
#0f172a) + blue (#2563eb), Inter font - Bootstrap 4.5 is loaded via the theme — use Bootstrap grid/utilities freely
- CSS specificity: Bootstrap uses
!importantheavily. Use a.component-class .childselector or!importantoverrides when fighting Bootstrap rules. - The remote theme fights back. Creative v6.0.4 sets
font-family: "Merriweather Sans"(a font this site never loads, so it falls back to a system font) and its orange#f4623aon selectors more specific than plain element/class rules:h1-h6and.h1-.h6,.btn,#mainNav .navbar-brand,#mainNav .navbar-nav .nav-item .nav-link,.btn-outline-primary, and.nav-link.active(with!important). A “Remote theme overrides” block near the top ofmain.scssmatches that specificity — check it first if text renders in the wrong font or something turns orange. Watch forclass="h5"on an<h3>(services.html does this): the class beats a bare element selector. - The theme colours nav links
rgba(255,255,255,0.7)until.navbar-scrolledis applied, which is invisible against this site’s always-white navbar. The override block pins them for both states; any new nav rule must use the full#mainNav .navbar-nav .nav-item .nav-linkchain or it will silently lose. - Scope component CSS to the component’s own class, never to
#section-id— includes get reused across pages under differentsection_idvalues. (.about-section .about-title, not#about .about-title.) bg-darkclass appliesbackground-color: #343a40 !important— avoid it on custom dark sections, use.about-section { background-color: #0f172a !important }instead
Certifications YAML format
Certs live under the about.html section as a flat certs list of name + optional url pairs (not plain strings). They render as a compact chip row; there is no separate certifications section any more.
certs: - name: Solutions Architect – Professional url: https://www.credly.com/badges/... - name: Champion Innovator – Databases # no url = plain chip, no linkKey Details
- Ruby version: 2.6.5 (see
.ruby-version) - Jekyll ~4.2.2, minima theme ~2.5.1
- Plugins: jekyll-feed, jekyll-sitemap, jekyll-seo-tag, jekyll-remote-theme
- Google Analytics removed (no cookie consent needed)
- OG image:
assets/img/og-image.png(1200×630, generated from SVG) - Favicon:
assets/img/favicon.svg+favicon.png(IP initials, dark navy) - No blog posts under
_posts/
Pricing
Prices are “from €X” anchors, deliberately. They filter unqualified enquiries and signal a productized practice rather than hourly work. Keep them; do not soften to “price on enquiry”. Current ladder: Workshop €1,500 · Assessment €10,000 · Proof of Value €30,000 · Fractional €5,000/mo · Training €4,000.
The assessment’s fee-credit offer (credited in full against a build booked within 90 days) is the mechanism that converts assessments into builds. Keep it prominent.
To Do
- Croatian proofread — the Croatian copy was written by Claude, not by a native speaker, and has not been reviewed by Ivan. It was rewritten once to remove translationese, but still needs a native read before
/hr/is promoted anywhere. - Testimonials — highest impact remaining content gap. The
testimonials.htmlinclude and a commented-out section block sit in bothindex.mdandhr/index.md; uncomment and fill in 2-3 verbatim quotes from LinkedIn recommendations. Never invent quotes. - Case study numbers — outcome chips are qualitative. Any that Ivan can back with a real figure (hours saved, cost reduced, time to market) should be restated in money, especially on the advisory page.
- Blog / thought leadership — write for buyers, not engineers. Suggested topics: what a production RAG system costs to run for a year; why the first AI use case should be boring; keeping AI inside the perimeter under EU data residency.
- Five unanswered FAQ questions — the FAQ on both pages ships with seven answered questions and five stubbed out in comments at the bottom of the
faq.htmlsection block: NDA/DPA, professional indemnity insurance, IP ownership, payment terms, and third-party AI tooling on client material. These are the ones enterprise procurement asks last and that most often stall a deal. Only Ivan can answer them — do not invent answers. When filling them in, translate intohr/index.mdat the same time.
Sourcing facts about Ivan
/Users/ip/Projects/cv/ip holds the CV repo. README.md there is the most current consolidated summary; resume/*.tex has per-engagement detail, and prlic-consulting-company-presentation.html has the company boilerplate including OIB 84570551730. Use those rather than inferring. Facts already mined into the site: SOC 2 / financial-grade work at Q.ai, the Metro / Ricoh / TUI team-collaboration examples, company registration details, and the client list.
Note the CV contains no quantified outcomes — no percentages, cost reductions, or timings — which is why the case-study chips remain qualitative.