New Feature·Pushed May 1, 2026·L
Gitpulse site SEO infrastructure added
The static site now speaks the language of search engines and social platforms—canonical URLs, OG images, Twitter cards, and JSON-LD structured data ship on every page.
The Gitpulse site previously lacked any search engine optimization. Links shared on Twitter or LinkedIn rendered as bare URLs with no preview image. Search engines had no sitemap to crawl, and no structured data to understand the content's meaning.
Every public route now ships a complete SEO package. The homepage and each story page include canonical URLs, OpenGraph metadata, Twitter card tags, and dynamically generated 1200×630 PNG images. A robots.txt tells crawlers they have full access, while a sitemap.xml—populated from actual story data—gives them a map of every page and its last modification date.
For search engines that understand structured data, pages now include JSON-LD schemas. The homepage carries a WebSite schema with the publication as publisher. Each story article includes a NewsArticle schema with headline, author, publication date, word count, and section category.
In the [[code]]@gitpulse/site[[/code]] app, the base URL resolves from a GITPULSE_SITE_URL environment variable or falls back to deriving https://{owner}.github.io/{repo} from GITHUB_REPOSITORY—ensuring canonical URLs always match the deployed path structure.
Technical description
The SEO implementation spans five new files and three modified ones, centered on two new library modules: [[code ref=1]]getBaseUrl[[/code]] from [[code]]lib/seo.ts[[/code]] and [[code ref=2]]JsonLd[[/code]] from [[code]]lib/json-ld.tsx[[/code]].
**Base URL Resolution**
[[code ref=1]]getBaseUrl[[/code]] resolves the site's absolute URL through a priority chain: explicit GITPULSE_SITE_URL override, then a derived GitHub Pages URL from GITHUB_REPOSITORY, then an empty string for development previews. This mirrors Next.js basePath behavior so canonical URLs stay consistent with served routes. The [[code]]canonicalUrl()[[/code]] helper appends paths to this base, and [[code]]truncateDescription()[[/code]] enforces the 160-character limit for search result snippets.
**Metadata Builders**
[[code]]buildHomeMetadata()[[/code]] and [[code]]buildStoryMetadata()[[/code]] construct full Next.js [[code]]Metadata[[/code]] objects. Home pages get website-type OG tags; stories get article-type tags with publishedTime and authors. Story titles are truncated to 70 characters with the reference number (PR #N or commit hash) preserved. Both builders include the canonical alternate URL.
**JSON-LD Structured Data**
[[code ref=2]]JsonLd[[/code]] injects a script tag with application/ld+json. It escapes the < character as \u003c to prevent XSS while keeping the JSON valid. [[code ref=3]]buildWebSiteJsonLd[[/code]] outputs a WebSite schema with an Organization publisher—mounted in [[code ref=11]]layout.tsx[[/code]] for every page. [[code ref=4]]buildStoryJsonLd[[/code]] outputs NewsArticle schema with headline, description, datePublished, author, articleSection, and wordCount—embedded in each story's [[code ref=10]]generateMetadata[[/code]] and rendered directly in the article body.
**Static Route Generation**
[[code ref=5]]robots[[/code]] exports a MetadataRoute.Robots object allowing all crawlers and optionally pointing to the sitemap. [[code ref=6]]sitemap[[/code]] generates entries for the root path and every story, using mergedAt ?? committedAt as lastModified. All routes declare [[code]]dynamic = 'force-static'[[/code]] as required by Next.js static export.
**OpenGraph Images**
[[code ref=7]]opengraph-image[[/code]] (homepage) renders the publication name, subtitle, and a gold accent bar on a dark background at 1200×630. [[code ref=8]]opengraph-image (story)[[/code]] renders the headline, standfirst, category label, reference identifier, and size label. Both use Next.js [[code]]ImageResponse[[/code]] and generate static params via [[code]]generateStaticParams()[[/code]] to prerender at build time.
````mermaid
graph LR
A[layout.tsx] -->|WebSite JSON-LD| B[JsonLd in head]
C[page.tsx] -->|Home metadata| D[OG image + twitter card]
E[stories/[id]/page.tsx] -->|Story metadata| F[Story OG + JSON-LD in body]
G[robots.ts] -->|robots.txt| H[Search crawlers]
I[sitemap.ts] -->|sitemap.xml| H
````
**Files at a Glance**
- [[code]]lib/seo.ts[[/code]] — URL helpers and metadata builders
- [[code]]lib/json-ld.tsx[[/code]] — JSON-LD component and schema builders
- [[code]]app/robots.ts[[/code]] — robots.txt route
- [[code]]app/sitemap.ts[[/code]] — sitemap.xml route
- [[code]]app/opengraph-image.tsx[[/code]] — homepage OG image
- [[code]]app/stories/[id]/opengraph-image.tsx[[/code]] — story OG images
- [[code]]app/layout.tsx[[/code]] — injects WebSite schema
- [[code]]app/page.tsx[[/code]] — uses buildHomeMetadata
- [[code]]app/stories/[id]/page.tsx[[/code]] — uses buildStoryMetadata + story schema
Categories
- New Feature (85%) — The PR is primarily adding a comprehensive SEO layer: robots.txt, sitemap.xml, OG images, JSON-LD structured data, and per-page metadata. These are all net-new capabilities for the site.
- Maintenance (15%) — The commit message explicitly states this is a 'port' from gitsky, adapted for the static-export + single-repo shape. Some refactoring of existing metadata logic occurred.