Skip to content

Public skipper profile

Rails
Status ✅
Code app/controllers/{profiles,profile_settings,profile_picks}_controller.rb, app/controllers/concerns/{profile_urls,public_replay_points}.rb, app/constraints/skipper_host_constraint.rb, app/models/{profile_trip,profile_handle_reservation}.rb, app/models/user.rb, app/helpers/profile_helper.rb, app/views/profiles/, app/views/accounts/_public_profile.html.erb, app/views/trips/_profile_pick.html.erb, app/views/public_trips/show.html.erb, app/javascript/controllers/{image_crop,globe}_controller.js, app/assets/stylesheets/components/profile.css, config/locales/profile.en.yml

Verified against code: 2026-11-01

Purpose

A skipper's public page: a sailing CV to send to a charter company, a crew, a sailing school or friends. It shows the logbook statistics over all trips and the trips the skipper picked. Off until the skipper turns it on. A PRO feature (User#pro?; every user during the beta). Decisions: ADR 019.

Address

https://skipper.sailever.com/<handle> (the skipper host, SAILEVER_SKIPPER_HOST). Every other host answers at /s/<handle>; with a skipper host configured, /s/<handle> redirects there (301). Without one (development, tests) /s/<handle> is the page. Trips on the profile: …/<handle>/trips/<trip id>, their replay …/trips/<trip id>/replay. The skipper host answers nothing but these; / sends to the web site.

Handle

  • 3–30 characters: lowercase letters, digits and single hyphens; starts with a letter, ends with a letter or digit. Stored lowercase, unique.
  • Reserved words are refused (admin, api, my, s, t, www, skipper, sailever, the locale codes and others in User::RESERVED_HANDLES).
  • The first handle is free to set. After a change, the handle can't change again for 30 days ("You changed your handle recently. You can change it again from ."; the field is read-only), and the old handle stays reserved for its owner for 30 days (profile_handle_reservations): nobody else gets it ("is taken"), the owner may take it back. The old address is not redirected. Deleting the account frees the handle at once.

Settings

Profile tab (/my/profile#public-profile), card Public skipper profile with a PRO badge, its own form and "Save profile":

Three groups, each with a small caps title:

Group Field Rule
Address Handle Text field after the address prefix (skipper.sailever.com/ or <host>/s/); the rules are behind a (?) (Handle). While locked, "You changed your handle recently. You can change it again from ." below it
Visibility Show my public profile Checkbox, off by default. Off: the page and its trips answer 404 at once. Settings and images are kept
Visibility Let search engines find my profile Checkbox, off by default; see Indexing
Images Avatar, Header image (4:1) See Images

Below them a line saying what the page shows, and, while the profile is visible, "View my profile" (opens it in a new tab). Saved: notice "Profile saved."; errors show above the fields.

Images

Two images: an avatar (square, always shown round by CSS) and a header image (4:1).

  • Upload opens the file picker (JPEG, PNG, WebP, HEIC; at most 15 MB, else "This image is larger than 15 MB."). The crop dialog opens on it ("Choose your avatar" / "Choose your header image"): the image under a fixed frame (1:1 with a round mask, or 4:1). The skipper drags the image and zooms (mouse wheel, pinch, the zoom buttons, + / −; arrow keys move it); a live preview shows the result. An original smaller than 400 × 400 (avatar) or 1600 × 400 (header) shows "This image is too small. It needs at least W × H pixels." and can't be used.
  • Use this part draws the chosen part in the browser at 512 × 512 (avatar) or 2400 × 600 (header) and encodes it as WebP. This drops all photo metadata (camera, date, GPS). The form then sends the cropped image, the original, and the part as {x, y, w, h} (fractions of the original).
  • Change crop opens the dialog on the stored original (/my/profile/images/<kind>/original, only for its owner, never cached) with the last part selected. Saving sends only the new cropped image and part.
  • Remove (confirm "Remove this image?") deletes the image and its original; notice "Image removed."
  • The server checks the type from the file's first bytes (shown image: WebP, JPEG or PNG, at most 3 MB; original: JPEG, PNG, WebP or HEIC, at most 15 MB).
  • Shown images are served through the Active Storage proxy. With SAILEVER_IMAGE_HOST set (production: images.sailever.com, Cloudflare in front), their URLs use that host; the proxy answers with a year-long public cache, and every upload is a new blob key, so a new image is a new URL and caches never need purging. Originals are never linked from any page.

Trips

A trip is on the profile only when its user picked it. On the trip page (owner, skipper and crew) the page-head buttons start with:

State Button
No handle yet "Set up your public profile" (→ Account → Profile)
Not picked "Show on my public profile"
Picked "On your public profile" (green); a click takes it off

Each user picks for their own profile: a crew member may show a trip they sailed on. Notices: "The trip is on your public profile." / "The trip is no longer on your public profile." Leaving or being removed from the crew takes the trip off that member's profile; deleting the trip or the account removes the pick.

The skipper header, also on the logbook:

  • the header image (4:1, rounded corners), or without one a dark band (8:1);
  • over its lower left edge the round avatar (104 px, 76 px on phones) with a border in the page colour, or the name's initial on orange;
  • the title "'s Logbook" (the name, e.g. "Jakob's Logbook"), then "@handle" and "Skipper since " (the year of the first counted day on a skippered trip) or "Sailing since " when the user never skippered;
  • below it the sailor bio, when written.

Name

The first name when the user shows it publicly (sharing.md), else the handle. On their own logbook before they have a handle: the first name or the start of their email address.

Layout

  1. Head, with the bio.
  2. The four totals and distance per year, as on the logbook.
  3. How the nights were spent and Boat length and type (logbook.md).
  4. Where sailed: the globe with a dot at the exact start of every trip with counted days. Picked trips are orange, sized by miles, and open a popup linking to the profile trip page. The others are small grey dots: not clickable, and the page has no title, date or link of them (the data holds only their position and miles). Hint: "Orange: trips shares, click one · grey: other trips · drag to turn, scroll or +/− to zoom".
  5. Trips: a card per picked trip, newest first: title (→ profile trip page), "Mon YYYY · N days · X.X NM · Skipper|Crew", the boat's model and length. None picked: " hasn't picked any trips for this page yet."
  6. Footer as on the public trip page.

Not shown anywhere: email, last name, crew names beyond the public trip page's rule, dates or titles of unpicked trips, devices, home port of boats.

Profile trip page

…/<handle>/trips/<trip id>: the public trip page for a picked trip, with a back link "'s profile" above the title and without the link end date. The replay and its points answer under the same address. The trip is found only among the profile's picks (ADR 015), so an unpicked or unknown id, or a profile that is off, answers 404. It doesn't depend on share links.

Indexing

The profile and its trips send X-Robots-Tag: noindex, nofollow and a robots meta tag, unless the skipper ticked "Let search engines find my profile": then the profile page (only) may be listed. robots.txt disallows /s/*/trips/ and /*/trips/ while indexing is on (SAILEVER_INDEXING).

Open Graph title " – skipper profile", description "X.X NM sailed, X.X NM as skipper, N days on the water. Logged with SailEver.", and the header image when there is one.

Data

users.profile_handle, profile_public, profile_indexable, profile_handle_changed_at, avatar_crop, header_crop; attachments avatar, avatar_original, header_image, header_original; tables profile_trips and profile_handle_reservations (data-model.md). All go with the account.

Known deviations

  • Every user is PRO until a plan model exists; a lapsed plan should answer 410 "This profile is not active" (not built).
  • No sitemap exists yet, so indexable profiles aren't listed in one.
  • The link preview uses the header image, not a drawn picture of the globe with the numbers.