From 01d5d971c88b332602a6312b79004891f9e34cc6 Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Thu, 18 Jun 2026 08:27:34 -0700 Subject: [PATCH] feat(seo): smarter meta-description fallbacks; trim over-long defaults (#1142, #1324) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OG/social inspectors flagged the generic 187-char description on pages that fell back to the lab boilerplate (home + projects without a one-line summary). Give those pages distinct, length-bounded descriptions: - Home: a concise (~135 char) description mirroring the hero "About Us" blurb next to the animated logo, instead of the shared generic one. Reused for the Organization JSON-LD description too. - Projects without a `summary`: fall back to the project's `about` text (stripped + truncated to 160) rather than the generic boilerplate — gives each summary-less project a distinct description (directly helps #1142's duplicate-content signal). - Trim the last-resort generic default to ~135 chars. Per-page descriptions for listing/member pages were already fine (144-160). The image-headline / call-to-action warnings from opengraph.xyz are marketing heuristics (text baked into the share image) and intentionally not addressed — inappropriate for an academic lab and not a correctness/SEO issue. Bumps version to 2.12.1. Template/view-only — no schema change, so no migration. Adds DescriptionFallbackTests; full suite 270 green. Co-Authored-By: Claude Opus 4.8 (1M context) --- makeabilitylab/settings.py | 4 +-- website/templates/website/base.html | 2 +- website/tests/test_page_metadata.py | 39 +++++++++++++++++++++++++++++ website/views/index.py | 13 +++++++--- website/views/project.py | 5 +++- 5 files changed, 55 insertions(+), 8 deletions(-) diff --git a/makeabilitylab/settings.py b/makeabilitylab/settings.py index d83349fc..f338d005 100644 --- a/makeabilitylab/settings.py +++ b/makeabilitylab/settings.py @@ -72,8 +72,8 @@ ALLOWED_HOSTS = ['*'] # Makeability Lab Global Variables, including Makeability Lab version -ML_WEBSITE_VERSION = "2.12.0" # Keep this updated with each release and also change the short description below -ML_WEBSITE_VERSION_DESCRIPTION = "Minor: site-wide SEO / social-sharing metadata overhaul (#1236, #1142, #1324). Centralized per-page Open Graph + Twitter Card tags with https URLs behind the TLS proxy (fixes #1236 via a site_scheme context processor), self-referential canonical links, distinct per-page meta descriptions, and schema.org JSON-LD (Organization on home, Person w/ sameAs on member pages, NewsArticle on news). Adds ORCID + Google Scholar fields to Person. IT follow-up #1329 (SECURE_PROXY_SSL_HEADER) would supersede the site_scheme workaround." +ML_WEBSITE_VERSION = "2.12.1" # Keep this updated with each release and also change the short description below +ML_WEBSITE_VERSION_DESCRIPTION = "Patch: tighten meta descriptions (#1142/#1324). Home now uses a concise description mirroring the hero blurb; projects without a one-line summary fall back to a truncated About instead of the generic lab boilerplate; the last-resort default is trimmed to ~135 chars. Reduces duplicate/over-long descriptions flagged by social/OG inspectors. Template/view-only — no schema change." DATE_MAKEABILITYLAB_FORMED = datetime.date(2012, 1, 1) # Date Makeability Lab was formed MAX_BANNERS = 7 # Maximum number of banners on a page diff --git a/website/templates/website/base.html b/website/templates/website/base.html index e532c686..ba874025 100644 --- a/website/templates/website/base.html +++ b/website/templates/website/base.html @@ -79,7 +79,7 @@ ============================================================================ {% endcomment %} {% with meta_title=page_meta.title|default:"Makeability Lab" og_type=page_meta.og_type|default:"website" canonical_path=page_meta.canonical_path|default:request.path %} - {% firstof page_meta.description "The Makeability Lab is an advanced research lab in Human-Computer Interaction and AI directed by Professor Jon E. Froehlich at University of Washington's Allen School of Computer Science." as meta_description %} + {% firstof page_meta.description "The Makeability Lab is an advanced research lab in Human-AI directed by Prof. Jon E. Froehlich at UW's Allen School of Computer Science." as meta_description %} diff --git a/website/tests/test_page_metadata.py b/website/tests/test_page_metadata.py index 031e8b87..dd840bd8 100644 --- a/website/tests/test_page_metadata.py +++ b/website/tests/test_page_metadata.py @@ -205,6 +205,45 @@ def test_jsonld_escapes_script_breakout(self): self.assertEqual(data["headline"], "Pwn x") # round-trips +class DescriptionFallbackTests(DatabaseTestCase): + """Distinct, length-bounded descriptions instead of the generic boilerplate + (#1142/#1324): home mirrors the hero blurb; summary-less projects use About.""" + + def _meta_description(self, resp): + m = re.search(r'name="description" content="([^"]*)"', resp.content.decode()) + return m.group(1) if m else None + + def test_home_uses_distinct_hero_description(self): + resp = self.client.get(reverse("website:index")) + desc = self._meta_description(resp) + self.assertIn("advanced research lab in Human-AI, directed by", desc) + self.assertLessEqual(len(desc), 160) + # not the old generic boilerplate + self.assertNotIn("Human-Computer Interaction and AI directed by Professor", desc) + + def test_project_without_summary_falls_back_to_about(self): + p = self.make_project( + name="GlassEar", short_name="glassear", is_visible=True, + start_date=date(2021, 1, 1), summary="", + about="

GlassEar is a wearable sound-awareness display for d/Deaf users.

", + ) + resp = self.client.get(reverse("website:project", args=[p.short_name])) + desc = self._meta_description(resp) + self.assertIn("GlassEar is a wearable sound-awareness display", desc) + self.assertLessEqual(len(desc), 160) + + def test_project_without_summary_or_about_uses_trimmed_default(self): + p = self.make_project( + name="Empty Proj", short_name="emptyproj", is_visible=True, + start_date=date(2021, 1, 1), summary="", about="", + ) + resp = self.client.get(reverse("website:project", args=[p.short_name])) + desc = self._meta_description(resp) + # last-resort generic default — present but trimmed + self.assertIn("Makeability Lab", desc) + self.assertLessEqual(len(desc), 160) + + class PageMetadataSchemeTests(DatabaseTestCase): """site_scheme keys off DJANGO_ENV, not DEBUG. The test server runs DEBUG=True behind the same TLS proxy as prod, so a DEBUG-based check would emit http:// diff --git a/website/views/index.py b/website/views/index.py index c8807a19..cc8114b5 100644 --- a/website/views/index.py +++ b/website/views/index.py @@ -79,6 +79,14 @@ def index(request): 'sponsors': sponsors, 'debug': settings.DEBUG} + # Distinct home-page description, mirroring the hero "About Us" blurb next to + # the animated logo (index.html). Concise (~155 chars) so it isn't truncated + # in search/social previews, and not the generic fallback (#1142/#1324). + home_description = ("The Makeability Lab is an advanced research lab in Human-AI, " + "directed by Prof. Jon E. Froehlich in UW's Allen School of " + "Computer Science.") + context['page_meta'] = {'description': home_description} + # schema.org Organization JSON-LD (home page) — helps Google build a # knowledge panel for "Makeability Lab" (#1142/#1324). Rendered by the # jsonld block in base.html. @@ -89,10 +97,7 @@ def index(request): "url": absolute_url(request, "/"), "logo": absolute_url(request, static( "website/img/logos/makelab_logo_v3_white_with_colors_and_text_og_image_ratio_1200w.png")), - "description": ("The Makeability Lab is an advanced research lab in " - "Human-Computer Interaction and AI directed by Professor " - "Jon E. Froehlich at the University of Washington's Allen " - "School of Computer Science."), + "description": home_description, "parentOrganization": { "@type": "CollegeOrUniversity", "name": ("Paul G. Allen School of Computer Science & Engineering, " diff --git a/website/views/project.py b/website/views/project.py index 4a6d6682..444639b0 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -121,7 +121,10 @@ def project(request, project_name): # /projects/ alias to one indexable URL. context['page_meta'] = { 'title': project.name, - 'description': meta_description(project.summary), + # Prefer the one-line summary; fall back to the (HTML) About text, + # stripped + truncated. Gives summary-less projects a distinct + # description instead of the generic lab boilerplate (#1142/#1324). + 'description': meta_description(project.summary or project.about), 'canonical_path': reverse('website:project', args=[project.short_name]), }