Add a 5-word Description column to the Posts table

LinkedIn's exports have no title/text field for a post anywhere (neither
Aggregate Analytics nor SinglePostAnalytics) - only the URL, whose slug is a
URL-safe truncation of the post's own opening words. post_title_from_url()
decodes it (handles accented names like Krutošíková), strips the trailing
-share-/-ugcPost-<id>-<code> suffix, and takes the first 5 words as a readable
stand-in for a title. Added to both the live dashboard's posts table and the
downloadable report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
beantownbabeandClaude Sonnet 5 committed 2026-10-30 13:08:33 +01:00
1 parent 3de9143e46
commit 0335e17e89
2 files changed
+24 -3

No files matched your search

+3 -2
View File
@@ -2876,6 +2876,7 @@ def _team_top_posts_table(top_posts_df, post_details):
details = post_details.get(r["Post URL"]) or {}
table_rows.append(html.Tr([
html.Td(r["Post Publish Date"]),
html.Td(team_exports.post_title_from_url(r["Post URL"])),
html.Td(dbc.Badge(r["Type"], color="secondary" if r["Type"] == "Repost" else "primary")),
html.Td(int(r["Impressions"]) if pd.notna(r["Impressions"]) else "—"),
html.Td(int(r["Engagements"]) if pd.notna(r["Engagements"]) else "—"),
@@ -2888,13 +2889,13 @@ def _team_top_posts_table(top_posts_df, post_details):
]))
return dbc.Table([
html.Thead(html.Tr([html.Th(c) for c in [
"Date", "Type", "Impressions", "Engagements (export)", "Profile Viewers*",
"Date", "Description", "Type", "Impressions", "Engagements (export)", "Profile Viewers*",
"Followers Gained*", "Reactions*", "Comments*", "Reposts*", "Link",
]])),
html.Tbody(table_rows),
html.Tfoot(html.Tr(html.Td(
"* from each post's own \"Export\" button (SinglePostAnalytics file) — not in the Aggregate Analytics export",
colSpan=10, style={"fontSize": "0.7rem", "color": "#6b7a8d", "fontStyle": "italic"}))),
colSpan=11, style={"fontSize": "0.7rem", "color": "#6b7a8d", "fontStyle": "italic"}))),
], bordered=True, size="sm", style={"fontSize": "0.85rem"})
+21 -1
View File
@@ -20,6 +20,7 @@ hasn't, and app.py renders a placeholder for that case.
import io
import os
import re
from urllib.parse import unquote
import pandas as pd
from docx import Document
@@ -106,6 +107,24 @@ def _post_is_repost(url):
return m.group(1).lower() == "share" if m else None
_POST_SLUG_RE = re.compile(r"_(.+?)-(?:share|ugcpost)-\d+-\w+$", re.I)
def post_title_from_url(url, word_count=5):
"""A short, readable stand-in for an actual post title - LinkedIn's export has
no title/text field anywhere (Aggregate Analytics or SinglePostAnalytics), only
the URL, whose slug is a URL-safe truncation of the post's own opening words.
Decodes %-escapes first (needed for names with accents, e.g. Krutošíková)."""
if not url:
return ""
m = _POST_SLUG_RE.search(unquote(url))
if not m:
return ""
words = m.group(1).replace("-", " ").split()
title = " ".join(words[:word_count])
return title[0].upper() + title[1:] if title else ""
def _parse_top_posts(fp):
raw = pd.read_excel(fp, sheet_name="TOP POSTS", header=None)
body = raw.iloc[3:]
@@ -221,13 +240,14 @@ def build_person_report_docx(name, data, data_dir, upload_dir):
if posts.empty:
doc.add_paragraph("No posts in this window.")
else:
rows = [["Date", "Type", "Impressions", "Engagements (export)", "Profile Viewers*",
rows = [["Date", "Description", "Type", "Impressions", "Engagements (export)", "Profile Viewers*",
"Followers Gained*", "Reactions*", "Comments*", "Reposts*", "Link"]]
for _, r in posts.iterrows():
post_type = "Repost" if r["Is Repost"] else ("Original" if r["Is Repost"] is False else "Unknown")
d = post_details.get(r["Post URL"]) or {}
rows.append([
r["Post Publish Date"].strftime("%b %d, %Y"),
post_title_from_url(r["Post URL"]),
post_type,
int(r["Impressions"]) if pd.notna(r["Impressions"]) else "—",
int(r["Engagements"]) if pd.notna(r["Engagements"]) else "—",