Skip to content

fix: add lastmod entries to sitemaps - #2875

Open
Skazitron wants to merge 3 commits into
mainfrom
feat-lastmod
Open

fix: add lastmod entries to sitemaps#2875
Skazitron wants to merge 3 commits into
mainfrom
feat-lastmod

Conversation

@Skazitron

@Skazitron Skazitron commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Done

  • Script to auto-generate lastmod field generation.

QA

Issue / Card

https://warthogs.atlassian.net/browse/WD-38046

@webteam-app

Copy link
Copy Markdown

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.64865% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.48%. Comparing base (2f30a84) to head (03315f6).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
webapp/lastmod_manifest.py 0.00% 36 Missing ⚠️
webapp/views.py 92.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2875      +/-   ##
==========================================
- Coverage   74.64%   74.48%   -0.16%     
==========================================
  Files          19       20       +1     
  Lines        2374     2450      +76     
==========================================
+ Hits         1772     1825      +53     
- Misses        602      625      +23     
Flag Coverage Δ
python 74.48% <48.64%> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@immortalcodes immortalcodes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have left some comments for the current approach.
I would also like to suggest an alternative approach where we save the manifest file for path-date date from git and instead of serving the sitemap files at the runtime, we generate them and pack them as static files inside the OCI-image.

Comment on lines +64 to +67
if not path.startswith(TEMPLATES_PATHSPEC + "/"):
# --name-only lists every file touched by the commit, not
# just ones under our pathspec; skip the rest.
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not already covered when in line 48 above when you mention the TEMPLATE_PATHSPEC in git process

Comment on lines +35 to +74
def build_manifest():
"""
Walk git history once and record, for every file ever committed
under templates/, the commit date of the most recent commit that
touched it.
"""
result = subprocess.run(
[
"git",
"log",
f"--format={RECORD_MARKER}%cs",
"--name-only",
"--",
TEMPLATES_PATHSPEC,
],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=True,
)

manifest = {}
date = None
for line in result.stdout.splitlines():
if line.startswith(RECORD_MARKER):
date = line[len(RECORD_MARKER) :]
continue

path = line.strip()
if not path.startswith(TEMPLATES_PATHSPEC + "/"):
# --name-only lists every file touched by the commit, not
# just ones under our pathspec; skip the rest.
continue

rel_path = path[len(TEMPLATES_PATHSPEC) + 1 :]
# git log is newest-first, so the first commit we see touching
# a path is its most recent change.
manifest.setdefault(rel_path, date)

return manifest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are literally traversing the whole git history here for the repository which will become time-consuming as the commits grow.
What I would do is instead of building this whole file again at the build-time, I would keep the file in the repository, tag it with the date it was last built and then run the workflow to update it with commits that have taken place after that date-time

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think webapp/ is the best location for this file, we might want to move it to say scripts/ for example, because this is essentially a build step and not part of flask app.

Comment thread webapp/views.py
Comment on lines +374 to +381
Load templates/lastmod-manifest.json: a map of template file path
(relative to templates/) to the date it was last changed in git.

The production image doesn't ship .git (or the git history needed to
read it), so this manifest -- generated from git log by
scripts/generate-lastmod-manifest.py before the app is packaged -- is
how sitemap lastmod dates survive into the running app. Cached for
the life of the process: it's a static build artefact.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I understand the detailed comments are useful, we can trim these comments to just couple of lines or even format our code in a way that comments are not needed at all, bigger comments increase the file size and also consume more time to read.
The code should itself act a understanding of itself.

Comment thread webapp/app.py
("https://canonical.com/company", "company/index.html"),
("https://canonical.com/knowledge", "knowledge/index.html"),
]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would not be a good idea to populate these dicts in the app.py, may be we abstract the dicts into separate yaml?

Comment thread webapp/app.py
Comment on lines +431 to +464
CAREERS_STATIC_SITEMAP_PAGES = [
("https://canonical.com/careers", "careers/index.html", "monthly"),
(
"https://canonical.com/careers/career-explorer",
"careers/career-explorer.html",
"monthly",
),
("https://canonical.com/careers/all", "careers/all.html", "weekly"),
(
"https://canonical.com/careers/hiring-process",
"careers/hiring-process/index.html",
"weekly",
),
(
"https://canonical.com/careers/company-culture/remote-work",
"careers/company-culture/remote-work.html",
"monthly",
),
(
"https://canonical.com/careers/company-culture/progression",
"careers/company-culture/progression.html",
"monthly",
),
(
"https://canonical.com/careers/company-culture/diversity",
"careers/company-culture/diversity.html",
"monthly",
),
(
"https://canonical.com/careers/company-culture/sustainability",
"careers/company-culture/sustainability.html",
"monthly",
),
]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also do we need to manually add these paths? As far as I remember, were we not using sitemap generator to dynamically generate paths?
cc: @britneywwc

Comment thread webapp/app.py
Comment on lines +947 to 956
pages = [
{
"url": url,
"last_modified": get_file_last_modified(TEMPLATES_DIR / path),
}
for url, path in PARTNERS_SITEMAP_PAGES
]

xml_sitemap = flask.render_template("partners/sitemap.xml", pages=pages)
response = flask.make_response(xml_sitemap)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we should also abstract the sitemap generation functions in a separate file and then import and call them here, this makes the entire code more modular and clean.

Comment thread webapp/views.py
Comment on lines +582 to +603
def get_knowledge_last_modified(sections):
"""
Get the last-modified date for the /knowledge index page: the most
recent of its own template and every section it links to.

Args:
sections: list of section dicts, as returned by
get_knowledge_sections()

Returns:
An ISO 8601 date string (YYYY-MM-DD)
"""
templates_dir = Path(flask.current_app.root_path).parent / "templates"
index_file = templates_dir / "knowledge" / "index.html"

last_modified = get_file_last_modified(index_file)
if sections:
last_modified = max(
last_modified, *(s["last_modified"] for s in sections)
)

return last_modified

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can actually generalize it by passing in the path and then it can basically do the last modified for any parent path.

Comment on lines +85 to +86
- name: Pre-generate sitemap lastmod manifest
run: python3 webapp/lastmod_manifest.py generate-lastmod

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you able to test it on staging? I think you would have to make changes to rockcraft.yaml so that it actually copies the generated manifest inside the OCI-image.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants