Search: a career hit links to a fragment the career page does not have, so it opens at the top instead of at the section #707

Closed
opened 2026-10-03 02:11:26 +00:00 by tiagoagueda · 0 comments
Owner

A search hit in the career record (an experience, a qualification, a project, a certification or a skill) links to /career/#experience, /career/#education, /career/#projects, /career/#certifications or /career/#skills. None of those fragments exists on the page, so the browser opens the career page at the top and the person has to scroll to find what the search promised.

Found on 2026-10-03 while drafting #692, which depends on the career page's anchors. It is not on the list of improvements of 2026-10-02.

What exists today

  • search_career builds each hit's address as f"{overview}#{section}" (src/postulo/core/search.py:510), with the section named in the table of five models above it: "experience", "education", "projects", "certifications", "skills" (:467-490).
  • The career page draws each section as <section id="section-{{ section.spec.slug }}"> (src/postulo/templates/resume/overview.html:39), and its own sidebar links to #section-<slug> (src/postulo/resume/views.py:77-78). The slugs come from the registry: experience, education, project, skill-group, certification, link, language (src/postulo/resume/registry.py:28-80, OVERVIEW_ORDER at :83). The ids on the page are therefore section-experience, section-education, section-project, section-skill-group and section-certification.
  • Not one of the five fragments the search writes is an id anywhere in src/postulo (no template, script or view defines an element called experience, education, projects, certifications or skills), and static/js/app.js does not read location.hash. A browser ignores a fragment that matches nothing, which is the failure described above. The two that look right, #experience and #education, are missing the section- prefix; #projects and #certifications are also plural where the slugs are singular.
  • A skill has no section of its own on the page: skills are listed inside their group, and OVERVIEW_ORDER has skill-group and no skill. The target for a skill hit is section-skill-group.
  • The entries themselves have no ids (overview.html:71, the <li> of an entry carries none), so the section is the closest target the page offers today.
  • Nothing notices: the search test asserts the first career hit's title and not its address (tests/test_search.py:107), and the browser test of the career sections follows the sidebar's anchors, not a search hit (tests/e2e/test_career_sections.py:18-33).
  • Not the same defect as #506 (the "All N in …" links of the search page drop the query).

Checked against main at 0df9c342e.

Proposal

  1. Write the id the page has. experience → section-experience, education → section-education, project → section-project, certification → section-certification, skill → section-skill-group.
  2. Take the id from the registry instead of typing a second copy of the names: look up the section whose model is the hit's model in resume.registry.SECTIONS and build f"section-{spec.slug}" from it (a skill goes to its group's section). A section renamed in the registry then cannot leave the search pointing at nothing, which is how this one came about: the search was written with its own names beside the page's.
  3. A test that follows the link. For one row of each of the five models, find it by search, read the fragment of the hit's address and assert that the career page's response holds an element with that id.

Decisions

  • The section or the entry. The page gives its sections ids and its entries none. Recommended: the section now, which is what the page has and is one commit. Landing on the entry itself (an id per entry, with a scroll-margin-top that clears the sticky header, --header-height) is a better result and a separate step if the maintainer wants it.
  • The other career sections. The search covers five models and not links, skill groups or languages (search.py:467-490). Recommended: leave that out of this issue; it is a question about what the search should find, not about where it links.

Done when

  • A new test in tests/test_search.py: a row of each of the five models is found, the fragment of its hit's address is an id on the career page (/career/), and the test fails today on all five.
  • tests/e2e/test_career_sections.py gains one visit from a search hit: the target section is on screen below the sticky header, as it is for the sidebar's anchors.
  • No new strings and no migration; the CHANGELOG line ends (#N).

#506 (search: the "All N in …" links), #676 and #677 (the account menu and the sub-menu of the same page), #692 (the additional career sections, which add sections to the page and its anchors).

A search hit in the career record (an experience, a qualification, a project, a certification or a skill) links to `/career/#experience`, `/career/#education`, `/career/#projects`, `/career/#certifications` or `/career/#skills`. None of those fragments exists on the page, so the browser opens the career page at the top and the person has to scroll to find what the search promised. Found on 2026-10-03 while drafting #692, which depends on the career page's anchors. It is not on the list of improvements of 2026-10-02. ## What exists today - `search_career` builds each hit's address as `f"{overview}#{section}"` (`src/postulo/core/search.py:510`), with the section named in the table of five models above it: `"experience"`, `"education"`, `"projects"`, `"certifications"`, `"skills"` (`:467-490`). - The career page draws each section as `<section id="section-{{ section.spec.slug }}">` (`src/postulo/templates/resume/overview.html:39`), and its own sidebar links to `#section-<slug>` (`src/postulo/resume/views.py:77-78`). The slugs come from the registry: `experience`, `education`, `project`, `skill-group`, `certification`, `link`, `language` (`src/postulo/resume/registry.py:28-80`, `OVERVIEW_ORDER` at `:83`). The ids on the page are therefore `section-experience`, `section-education`, `section-project`, `section-skill-group` and `section-certification`. - Not one of the five fragments the search writes is an id anywhere in `src/postulo` (no template, script or view defines an element called `experience`, `education`, `projects`, `certifications` or `skills`), and `static/js/app.js` does not read `location.hash`. A browser ignores a fragment that matches nothing, which is the failure described above. The two that look right, `#experience` and `#education`, are missing the `section-` prefix; `#projects` and `#certifications` are also plural where the slugs are singular. - A skill has no section of its own on the page: skills are listed inside their group, and `OVERVIEW_ORDER` has `skill-group` and no `skill`. The target for a skill hit is `section-skill-group`. - The entries themselves have no ids (`overview.html:71`, the `<li>` of an entry carries none), so the section is the closest target the page offers today. - Nothing notices: the search test asserts the first career hit's title and not its address (`tests/test_search.py:107`), and the browser test of the career sections follows the sidebar's anchors, not a search hit (`tests/e2e/test_career_sections.py:18-33`). - Not the same defect as #506 (the "All N in …" links of the search page drop the query). Checked against `main` at `0df9c342e`. ## Proposal 1. **Write the id the page has.** experience → `section-experience`, education → `section-education`, project → `section-project`, certification → `section-certification`, skill → `section-skill-group`. 2. **Take the id from the registry** instead of typing a second copy of the names: look up the section whose `model` is the hit's model in `resume.registry.SECTIONS` and build `f"section-{spec.slug}"` from it (a skill goes to its group's section). A section renamed in the registry then cannot leave the search pointing at nothing, which is how this one came about: the search was written with its own names beside the page's. 3. **A test that follows the link.** For one row of each of the five models, find it by search, read the fragment of the hit's address and assert that the career page's response holds an element with that id. ## Decisions - **The section or the entry.** The page gives its sections ids and its entries none. **Recommended:** the section now, which is what the page has and is one commit. Landing on the entry itself (an id per entry, with a `scroll-margin-top` that clears the sticky header, `--header-height`) is a better result and a separate step if the maintainer wants it. - **The other career sections.** The search covers five models and not links, skill groups or languages (`search.py:467-490`). **Recommended:** leave that out of this issue; it is a question about what the search should find, not about where it links. ## Done when - A new test in `tests/test_search.py`: a row of each of the five models is found, the fragment of its hit's address is an id on the career page (`/career/`), and the test fails today on all five. - `tests/e2e/test_career_sections.py` gains one visit from a search hit: the target section is on screen below the sticky header, as it is for the sidebar's anchors. - No new strings and no migration; the CHANGELOG line ends `(#N)`. ## Related #506 (search: the "All N in …" links), #676 and #677 (the account menu and the sub-menu of the same page), #692 (the additional career sections, which add sections to the page and its anchors).
tiagoagueda added this to the 0.5.0 milestone 2026-10-03 02:11:26 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#707
No description provided.