Social profiles: type the username, with the service's address shown in front of it and fixed #678
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#678
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A social profile row asks for the whole address, and checks it against the service afterwards. The maintainer's list asks for the check to happen by construction: "Social profiles, for better validation, only typing username is necessary, prefixed url is already present, displayed and cannot be altered."
What exists today
Read from
origin/mainatf09b6010b, which holds the registry this issue builds on (#305, commit307f090d2).templates/partials/web_links.html:44-60; the same partial draws a contact's links). The address is aURLFieldwithassume_scheme="https"; the person types or pastes all of it (core/web_links.py:444-553,WebLinkForm, itscleanat:523).WebLink.service(blank means Other) (core/models.pyWebLink, migrationcore/0025_weblink_service.py). AServicesays its key, name, kind,hosts, apatternfor the path, an icon and anexample(core/link_services.py:100-130). The fifteen Postulo ships are thelink-servicepluginpostulo.plugins.link_services; an instance or a package may add more through the entry-point grouppostulo.link_services.hosts(linkedin.com, which also answers on subdomains and withoutwww.) andexample(“https://www.linkedin.com/in/name”,plugins/link_services/services.py:125). The pattern reads the path only, percent-decoded, “permissive at the end, strict at the start”: after/in/anything is let through (services.py, the module's own docstring).Service.handle(url)already pulls the name out of an address (link_services.py:168),guess()picks a service from a pasted address's host (:329), andsettle()is the one rule for what a row is stored as (:349): a named service has to accept the address, Other takes any, nothing chosen lets the address say.WebLink.urland the key inservice. The archive writes("kind", "service", "label", "url", "is_primary")(core/export.py:198,WEB_LINK_FIELDS), the candidate file reads the same block (resume/candidate.py:296-301), the API takesurlandservice(core/web_links.py:308,checked_rows). Format 29 of the archive and 4 of the candidate file addedservice.WebLink: “the kind is the sort of thing, and the service is where it is”). So a service that fixes the host is no longer against a standing decision; the decision is already taken, in #305's own text, that a chosen service is checked./in/), Bluesky (/profile/), X (x.com/), Xing (/profile/), Facebook, Instagram, Threads (/@), YouTube (/@). Mastodon is on any server (any_host=True), so it has no fixed front. A LinkedIn address may also be a company, a school or a showcase page (/company/,/school/,/showcase/), which are not a person's profile./in/namea trailing path, a query string or a tracking parameter is let through, andurlkeeps all of it. Two spellings of one profile (linkedin.com/in/xandwww.linkedin.com/in/x/) are two rows as far as the duplicate rule is concerned (core/web_links.py,BaseWebLinkFormSet.clean).add_single_boxes,core/web_links.py:386-406), which stores throughsave_only_link.Checked against
mainatf09b6010b.Proposal
LinkServicegains an optionalprefix: the address up to where the person's own name goes, as the service writes it today (https://www.linkedin.com/in/,https://bsky.app/profile/,https://x.com/,https://www.threads.com/@). Blank means the service has no fixed front (Mastodon, a Forgejo instance, Other). It is a new name on the plugin surface, so a plugin's own services can say theirs. Eight of the nine shipped social services get one, taken from theexampleeach already carries; Mastodon, which is on any server, gets none.input-group, with the prefix as a fixed start addon: text, not an input, so it cannot be edited and can still be selected and copied. The prefix is written left to right inside<bdi dir="ltr">on a right-to-left page; it is tied to the box byaria-describedby, so a screen reader hears the box's name and then where it goes; the group wraps on a narrow screen as the telephone field does ([data-phone-field],partials/phone_widget.html).prefix + name, through the samesettle()that checks every other address. A leading@typed where the prefix ends in@is taken as part of the prefix. A whole address pasted into the box is recognised (it starts withhttp) and read as an address, so pasting never breaks;Service.handle()takes the name out of it and the stored form is the same canonical one.WebLink.urlstays the full address andservicethe key: the API, the archive (29 now), the candidate file (4 now), a CV's printed link, the contact export and the duplicate rule all keep reading one thing. There is no migration and no bump.prefix + name(a company page,linkedin.comwithoutwww., an address with a query string or a trailing path) is shown as it is stored, in the whole-address box, with a line saying that it is kept as written; nothing is rewritten and nothing is refused in retrospect, the rule #305 set.app.jsalready swaps (data-service-select); choosing Other or a service with no prefix gives the whole-address box back. The label of the box says “Username” beside a prefix and “Address” otherwise.Decisions
github.com/plus an owner, or an owner and a project) could follow the same way, but a repository is often a project (owner/project) and not a username, and the list does not ask for it. Recommended: social profiles in this issue; theprefixfield is generic, so repositories are a later, one-service-at-a-time decision.@name@server, could be accepted as an alternative and turned intohttps://server/@name. It is small and it is how the account is written, so a second commit./in/;/company/,/school/and/showcase/are not one. Recommended: the prefix is/in/; a company page is chosen as Other or stays as an older stored row.urlstill takes the whole address. Recommended: no new field in this issue; a client that knows a handle can build the address from theexample, and ahandlefield would be a second way to say one thing.Builds on
#305 (the service registry, the
link-serviceplugin kind,settle), #213 (what the row is first, then the value), #189 (web links as rows), #303 (the star and the bin), #301 (Basecoat's select; the service choice lives in it), #638 (open; a name with accents must pass, which the path patterns already do). The prefix field is read by #654 if a brand mark is ever shown beside it.Done when
prefixinplugins/link_services/services.py, and a test intests/test_link_services.pyholds all of them: the prefix ishttps, is on one of the service'shosts,service.accepts(prefix + "name")is true,service.handle(prefix + "name")gives the name back, and a service with no fixed front says so.nameon a LinkedIn row storeshttps://www.linkedin.com/in/namewith the servicelinkedin; typing@nameon a Threads row storeshttps://www.threads.com/@name; a whole address pasted into the box stores the same canonical address; a name with an accent is accepted; a name that breaks the service's rule is refused beside the box with a sentence saying what the rule is.tests/test_link_services.pyandtests/test_web_links.py.tests/test_link_services.py.tests/e2e/test_link_services.py.tests/e2e/test_accessibility.py,tests/e2e/test_text_spacing.py,tests/e2e/test_direction.py).tests/test_wiki_surface.py) and a test intests/test_plugin_surface.py. The archive and the candidate file are unchanged, whichtests/test_export.pyand the candidate fingerprint intests/test_candidate_file.pyhold.From the list of improvements of 2026-10-02.