New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

lander

Package Overview
Dependencies
Maintainers
2
Versions
53
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

lander - pypi Package Compare versions

Comparing version
1.0.9
to
1.1.0
+52
CLAUDE.md
# Claude Configuration for Lander
This is an HTML landing page generator for LSST PDF documentation. It's a Python project with Node.js/Webpack for frontend asset building.
## Commands
Build frontend assets:
```bash
npm run build
```
Run tests:
```bash
tox run -e py
```
Run linting/formatting:
```bash
tox run -e lint
```
Run type checking:
```bash
tox run -e typing
```
Run end-to-end integration tests:
```bash
make test
```
## Project Structure
- `src/lander/` - Python source code
- `tests/` - Python unit tests
- `integration-tests/` - End-to-end integration tests
- `js/`, `scss/` - Frontend source files
- `gulpfile.js`, `webpack.config.js` - Build configuration
## Development Notes
- Python package managed with setuptools
- Frontend assets built with Webpack and Gulp
- Uses pytest for unit testing
- Integration tests in bash scripts
- Code formatting with black (79 char line length)
- Import sorting with isort
"""Metadata formatting modules for HTML output."""
__all__ = ["HighwireMetadata", "OpenGraphMetadata"]
from .highwire import HighwireMetadata
from .opengraph import OpenGraphMetadata
"""Base class for metadata formatters."""
from abc import ABC, abstractmethod
from typing import List, Optional, Union
class MetaTagFormatterBase(ABC):
"""A base class for generating HTML meta tags."""
def __str__(self) -> str:
"""Create the metadata tags."""
return self.as_html()
@property
@abstractmethod
def tag_attributes(self) -> List[str]:
"""The names of class properties that create tags."""
raise NotImplementedError
def as_html(self) -> str:
"""Create the metadata HTML tags."""
tags: List[str] = []
for prop in self.tag_attributes:
self.extend_not_none(tags, getattr(self, prop))
return "\n".join(tags) + "\n"
@staticmethod
def extend_not_none(
entries: List[str], new_item: Optional[Union[str, List[str]]]
) -> None:
"""Extend a list with new items if they are not None."""
if new_item is None:
return
if isinstance(new_item, list):
entries.extend(new_item)
else:
entries.append(new_item)
"""Highwire Press metadata formatter for Google Scholar."""
from typing import TYPE_CHECKING, List, Optional
from .base import MetaTagFormatterBase
if TYPE_CHECKING:
from lander.config import Configuration
class HighwireMetadata(MetaTagFormatterBase):
"""Format Lander metadata as Highwire Press tags for Google Scholar.
Highwire Press metadata tags are used by Google Scholar to index
academic and technical documents.
"""
def __init__(self, config: "Configuration") -> None:
self.config = config
@property
def tag_attributes(self) -> List[str]:
"""The names of class properties that create tags."""
return [
"title",
"authors",
"date",
"technical_report_number",
"pdf_url",
"fulltext_html_url",
]
@property
def title(self) -> str:
"""The document title tag."""
title = self.config.title.plain or self.config.title.html or ""
return f'<meta name="citation_title" content="{self._escape(title)}">'
@property
def authors(self) -> List[str]:
"""Author metadata tags.
Each author generates multiple tags:
- citation_author
- citation_author_institution (if available)
- citation_author_email (if available)
- citation_author_orcid (if available)
"""
if not self.config.authors:
return []
tags = []
for author in self.config.authors:
name = author.plain or author.html or ""
tags.append(
f'<meta name="citation_author" content="{self._escape(name)}">'
)
# Note: Institution, email, ORCID not currently in Lander config
# but included here for future extension
return tags
@property
def date(self) -> Optional[str]:
"""The publication date tag.
Format: YYYY/MM/DD (Highwire requires slash separators)
"""
if self.config.build_datetime:
date_str = self.config.build_datetime.strftime("%Y/%m/%d")
return f'<meta name="citation_date" content="{date_str}">'
return None
@property
def technical_report_number(self) -> Optional[str]:
"""The technical report number (document handle)."""
if self.config.handle:
return (
f'<meta name="citation_technical_report_number" '
f'content="{self._escape(self.config.handle)}">'
)
return None
@property
def pdf_url(self) -> Optional[str]:
"""The PDF download URL."""
if self.config.canonical_url and self.config.pdf_path:
pdf_url = (
f"{self.config.canonical_url}/"
f"{self.config.relative_pdf_path}"
)
return f'<meta name="citation_pdf_url" content="{pdf_url}">'
return None
@property
def fulltext_html_url(self) -> Optional[str]:
"""The canonical HTML URL of the document."""
if self.config.canonical_url:
return (
f'<meta name="citation_fulltext_html_url" '
f'content="{self.config.canonical_url}">'
)
return None
@staticmethod
def _escape(value: str) -> str:
"""Escape HTML attribute content."""
return value.replace('"', "&quot;").replace("'", "&#39;")
"""OpenGraph metadata formatter for social media."""
from datetime import timezone
from typing import TYPE_CHECKING, List, Optional
from .base import MetaTagFormatterBase
if TYPE_CHECKING:
from lander.config import Configuration
class OpenGraphMetadata(MetaTagFormatterBase):
"""Format Lander metadata as OpenGraph tags for social media.
OpenGraph metadata enables rich link previews in social media
platforms and messaging applications.
"""
def __init__(self, config: "Configuration") -> None:
self.config = config
@property
def tag_attributes(self) -> List[str]:
"""The names of class properties that create tags."""
return [
"title",
"description",
"url",
"og_type",
"authors",
"dates",
]
@property
def title(self) -> str:
"""The og:title tag."""
title = self.config.title.plain or self.config.title.html or ""
return (
f'<meta property="og:title" ' # noqa: E231
f'content="{self._escape(title)}">'
)
@property
def description(self) -> Optional[str]:
"""The og:description tag from the abstract."""
if self.config.abstract:
abstract = (
self.config.abstract.plain or self.config.abstract.html or ""
)
return (
f'<meta property="og:description" ' # noqa: E231
f'content="{self._escape(abstract)}">'
)
return None
@property
def url(self) -> Optional[str]:
"""The og:url canonical URL tag."""
if self.config.canonical_url:
return ( # noqa: E231
f'<meta property="og:url" ' # noqa: E231
f'content="{self.config.canonical_url}">'
)
return None
@property
def og_type(self) -> str:
"""The og:type tag (always 'article' for documents)."""
return '<meta property="og:type" content="article">'
@property
def authors(self) -> List[str]:
"""The og:article:author tags."""
if not self.config.authors:
return []
tags = []
for author in self.config.authors:
name = author.plain or author.html or ""
tags.append(
f'<meta property="og:article:author" ' # noqa: E231
f'content="{self._escape(name)}">'
)
return tags
@property
def dates(self) -> List[str]:
"""Publication and modification time tags.
Format: ISO 8601 with timezone (YYYY-MM-DDTHH:MM:SSZ)
"""
tags = []
if self.config.build_datetime:
# Ensure datetime has timezone info
dt = self.config.build_datetime
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt_str = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
tags.append(
f'<meta property="og:article:published_time" ' # noqa: E231
f'content="{dt_str}">'
)
# Also use as modified time since we don't track separate
# modification dates
tags.append(
f'<meta property="og:article:modified_time" ' # noqa: E231
f'content="{dt_str}">'
)
return tags
@staticmethod
def _escape(value: str) -> str:
"""Escape HTML attribute content."""
return value.replace('"', "&quot;").replace("'", "&#39;")
+2
-0

@@ -103,1 +103,3 @@ # Byte-compiled / optimized / DLL files

.mypy_cache
.claude/settings.local.json

@@ -5,2 +5,13 @@ ##########

1.1.0 (2025-09-22)
==================
- Generate meta tags and microdata consistent with the `Technote project <https://technote.lsst.io/user-guide/metadata.html>`__:
- Highwire Press metadata supported by Google Scholar
- OpenGraph metadata for additional social media support
- microformats2 metadata on HTML elements
These changes improve compatibility for Ook to ingest Lander landing pages.
1.0.9 (2025-09-09)

@@ -7,0 +18,0 @@ ==================

+1
-1

@@ -13,3 +13,3 @@ .PHONY: help test pytest ldm151 dmtn070

pytest:
pytest --flake8 --doctest-modules lander tests
tox

@@ -16,0 +16,0 @@ ldm151:

Metadata-Version: 2.4
Name: lander
Version: 1.0.9
Version: 1.1.0
Summary: HTML landing page generator for LSST PDF documentation deployed from Git to LSST the Docs.

@@ -259,2 +259,13 @@ Home-page: https://github.com/lsst-sqre/lander

1.1.0 (2025-09-22)
==================
- Generate meta tags and microdata consistent with the `Technote project <https://technote.lsst.io/user-guide/metadata.html>`__:
- Highwire Press metadata supported by Google Scholar
- OpenGraph metadata for additional social media support
- microformats2 metadata on HTML elements
These changes improve compatibility for Ook to ingest Lander landing pages.
1.0.9 (2025-09-09)

@@ -261,0 +272,0 @@ ==================

Metadata-Version: 2.4
Name: lander
Version: 1.0.9
Version: 1.1.0
Summary: HTML landing page generator for LSST PDF documentation deployed from Git to LSST the Docs.

@@ -259,2 +259,13 @@ Home-page: https://github.com/lsst-sqre/lander

1.1.0 (2025-09-22)
==================
- Generate meta tags and microdata consistent with the `Technote project <https://technote.lsst.io/user-guide/metadata.html>`__:
- Highwire Press metadata supported by Google Scholar
- OpenGraph metadata for additional social media support
- microformats2 metadata on HTML elements
These changes improve compatibility for Ook to ingest Lander landing pages.
1.0.9 (2025-09-09)

@@ -261,0 +272,0 @@ ==================

@@ -6,2 +6,3 @@ .gitattributes

CHANGELOG.rst
CLAUDE.md
LICENSE

@@ -63,2 +64,6 @@ MANIFEST.in

src/lander/lsstprojectmeta/tex/scraper.py
src/lander/metadata/__init__.py
src/lander/metadata/base.py
src/lander/metadata/highwire.py
src/lander/metadata/opengraph.py
src/lander/templates/_footer.jinja

@@ -65,0 +70,0 @@ src/lander/templates/_info-panel.jinja

@@ -341,2 +341,19 @@ """Lander's configuration."""

@property
def canonical_url(self) -> Optional[str]:
"""The canonical URL of the document.
This is the primary URL where the document can be accessed.
Prefers LSST the Docs URL, falls back to GitHub repository.
"""
if self.ltd_product:
# Construct LSST the Docs URL from product slug
# Assumes we're building the main/default version
return f"https://{self.ltd_product}.lsst.io" # noqa: E231
elif self.github_slug:
# Fall back to GitHub repository URL
return f"https://github.com/{self.github_slug}" # noqa: E231
else:
return None
@validator("pdf_path", always=True)

@@ -343,0 +360,0 @@ def check_pdf_path(cls, v: str) -> str:

@@ -18,2 +18,5 @@ """HTML renderering functions."""

from lander import __version__
from lander.metadata import HighwireMetadata, OpenGraphMetadata
if TYPE_CHECKING:

@@ -43,5 +46,14 @@ from lander.config import Configuration

def render_homepage(config: "Configuration", env: jinja2.Environment) -> str:
"""Render the homepage.jinja template."""
"""Render the homepage.jinja template with metadata."""
# Generate metadata
highwire = HighwireMetadata(config)
opengraph = OpenGraphMetadata(config)
template = env.get_template("homepage.jinja")
rendered_page = template.render(config=config)
rendered_page = template.render(
config=config,
lander_version=__version__,
highwire_metadata=highwire.as_html(),
opengraph_metadata=opengraph.as_html(),
)
return rendered_page

@@ -48,0 +60,0 @@

@@ -10,3 +10,3 @@ <header class="lander-info-header">

{% endif %}
<h1 class="lander-info-header__product-name">{{ config.title.html|safe }}</h1>
<h1 class="lander-info-header__product-name p-name">{{ config.title.html|safe }}</h1>

@@ -18,3 +18,5 @@ <ul class="o-list-bare">

</span>
{{ config.build_datetime|simple_date }}
<time class="dt-published" datetime="{{ config.build_datetime.isoformat() }}">
{{ config.build_datetime|simple_date }}
</time>
</li>

@@ -42,3 +44,3 @@

<li class="c-lander-btn-row__item">
<a class="c-btn c-btn--small c-btn--ghost c-btn--ghost-faint" href="https://{{ config.ltd_product }}.lsst.io/v">Change version</a>
<a class="c-btn c-btn--small c-btn--ghost c-btn--ghost-faint u-url" href="https://{{ config.ltd_product }}.lsst.io/v">Change version</a>
</li>

@@ -63,3 +65,3 @@ {% endif %}

{% for author_name in config.authors %}
<li>{{ author_name.html|safe }}</li>
<li class="p-author">{{ author_name.html|safe }}</li>
{% endfor %}

@@ -71,3 +73,3 @@ </ul>

{% if config.abstract %}
<section class="lander-info-abstract">
<section class="lander-info-abstract p-summary">
<h2 class="lander-subsection-header">Abstract</h2>

@@ -111,3 +113,3 @@ {{ config.abstract.html|safe }}

<li class="o-list-bare__item">
<a class="hidden-link" href="https://github.com/{{ config.github_slug }}">
<a class="hidden-link" href="https://github.com/{{ config.github_slug }}" data-lander-source-url="https://github.com/{{ config.github_slug }}">
<span class="svg-icon svg-icon--left svg-baseline">

@@ -114,0 +116,0 @@ <svg><use xlink:href="#octicon-mark-github" /></svg>

@@ -12,2 +12,17 @@ <!doctype html>

<meta name="viewport" content="width=device-width, initial-scale=1">
{# Generator meta tag #}
<meta name="generator" content="Lander {{ lander_version }}">
{# Canonical URL #}
{% if config.canonical_url %}
<link rel="canonical" href="{{ config.canonical_url }}">
{% endif %}
{# OpenGraph metadata for social media #}
{{ opengraph_metadata }}
{# Highwire metadata for Google Scholar #}
{{ highwire_metadata }}
{% include "_typekit_loader.jinja" %}

@@ -14,0 +29,0 @@ <link rel="stylesheet" href="assets/app.css">

@@ -5,3 +5,3 @@ {% extends "base.jinja" %}

<div class="lander-container lander-container--content">
<div class="lander-container lander-container--content h-entry">

@@ -12,3 +12,3 @@ <div class="lander-info-item">

<div class="lander-pdf-item">
<div class="lander-pdf-item e-content">
<div id="pdf-container" data-pdf-path="{{ config.relative_pdf_path }}">

@@ -15,0 +15,0 @@ </div>