Skip to main content

wagtail-link-field

A flexible link block and field for Wagtail CMS that supports internal pages, external URLs, documents, anchors, email, phone, and custom links.

Features

  • LinkBlock: A StreamField block for flexible link selection
  • LinkField: A model field that stores link data as JSON
  • LinkPanel: A panel for editing LinkField in Wagtail admin
  • Template tags: For rendering links in templates
  • Dynamic field visibility: JavaScript shows/hides relevant fields based on selected link type
  • Clean JSON storage: Only relevant fields are stored, irrelevant ones are stripped

Installation

pip install wagtail-link-field

Add to your INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    "wagtail_link_field",
    # ...
]

Usage

LinkBlock in StreamField

from wagtail.fields import StreamField
from wagtail import blocks
from wagtail_link_field import LinkBlock

class MyPage(Page):
    body = StreamField([
        ("link", LinkBlock()),
    ], blank=True, use_json_field=True)

    # Restrict to specific link types
    body_restricted = StreamField([
        ("link", LinkBlock(link_types=["internal-link", "external-link"])),
    ], blank=True, use_json_field=True)

LinkField with LinkPanel

from django.db import models
from wagtail_link_field import LinkField, LinkPanel

class MyPage(Page):
    cta = LinkField(blank=True, null=True)

    content_panels = Page.content_panels + [
        LinkPanel("cta"),
        # Or restrict link types:
        LinkPanel("cta_restricted", link_types=["internal-link", "external-link"]),
    ]

Both LinkBlock values and LinkField values provide the same methods:

# From a LinkField
page = MyPage.objects.first()
link = page.cta  # Returns LinkFieldValue instance

# From a LinkBlock in StreamField
for block in page.body:
    if block.block_type == 'link':
        link = block.value  # Returns LinkValue instance

# Both provide the same methods:
url = link.url()              # Computed URL (e.g., "/about/", "https://example.com", "mailto:test@example.com")
is_external = link.is_external()  # True for external, email, phone, document, custom links
title = link.title()          # Page/document title if available, otherwise None
safe_title = link.safe_title()    # Formatted title (page title or cleaned URL/email/phone)
action = link.action          # Link type: "internal-link", "external-link", etc.

Using in templates

Load the template tags:

{% load wagtail_link_field_tags %}

Automatically renders a complete <a> tag with proper attributes:

{# Basic usage - uses safe_title automatically #}
{% render_link page.cta %}
{# Output: <a href="/about/">About Us</a> #}

{# With CSS class #}
{% render_link page.cta css_class="btn btn-primary" %}

{# With extra attributes #}
{% render_link page.cta extra_attrs='data-action="click"' %}

{# Works with StreamField blocks too #}
{% for block in page.body %}
    {% if block.block_type == 'link' %}
        {% render_link block.value css_class="content-link" %}
    {% endif %}
{% endfor %}

The render_link tag automatically:

  • Adds target="_blank" and rel="noopener noreferrer" for external links
  • Uses safe_title() for the link text (page title or formatted URL/email/phone)

Option 2: Use filters for custom markup

{# Get the URL #}
<a href="{{ page.cta|link_url }}">Click here</a>

{# Get page/document title (returns None if not available) #}
<a href="{{ page.cta|link_url }}">
    {{ page.cta|link_title|default:"Read more" }}
</a>

{# Get formatted title (always returns something user-friendly) #}
<a href="{{ page.cta|link_url }}">
    {{ page.cta|link_safe_title }}
</a>

{# Build custom markup #}
{% if page.cta %}
    <a href="{{ page.cta|link_url }}" 
       class="{% if page.cta.is_external %}external-link{% endif %}">
        {{ page.cta|link_safe_title }}
    </a>
{% endif %}

Option 3: Access methods directly

{# Access methods on the value object #}
<a href="{{ page.cta.url }}" 
   {% if page.cta.is_external %}target="_blank" rel="noopener noreferrer"{% endif %}>
    {{ page.cta.safe_title }}
</a>

{# Check link type #}
{% if page.cta.action == "internal-link" %}
    <a href="{{ page.cta.url }}" class="internal">{{ page.cta.title|default:page.cta.safe_title }}</a>
{% elif page.cta.action == "email-link" %}
    <a href="{{ page.cta.url }}" class="email">{{ page.cta.safe_title }}</a>
{% endif %}

When to use each approach:

  • {% render_link %}: Quick, automatic rendering with sensible defaults
  • Filters (|link_url, |link_safe_title): Custom HTML structure but simpler syntax
  • Direct methods (.url, .safe_title): Maximum control and conditional logic
Type Description Fields
internal-link Link to a Wagtail page internal_link, page_query_string
document-link Link to a Wagtail document document_link
external-link External URL external_link
anchor-link Same-page anchor anchor_link
email-link Email address url_link (auto-prefixed with mailto:)
phone-link Phone number url_link (auto-prefixed with tel:)
custom-link Custom URL scheme url_link

Pass link_types to LinkBlock or LinkPanel:

# Only internal and external links
LinkBlock(link_types=["internal-link", "external-link"])

# Only documents
LinkPanel("download", link_types=["document-link"])

Set required=False for optional links:

# LinkBlock (default is required=True)
LinkBlock(required=False)

# LinkField (use blank=True, null=True)
cta = LinkField(blank=True, null=True)

Development

Setup

# Clone the repo
git clone https://github.com/infofactory/wagtail-link-field.git
cd wagtail-link-field

# Install with uv
uv sync --extra testing

Running tests

# Run tests
python testmanage.py test

# Run with coverage
python -m coverage run testmanage.py test
python -m coverage report -m

Running linting

ruff check .
ruff format --check .

Running tox (matrix testing)

tox

License

BSD-3-Clause. See LICENSE for details.

Metadata

Release files for wagtail-link-field 0.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wagtail-link-field 0.1.6
File Size Uploaded Provenance Actions
wagtail_link_field-0.1.6.tar.gz 17.4 kB With provenance Details for wagtail_link_field-0.1.6.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for wagtail-link-field 0.1.6
File Interpreter ABI Platform Actions
wagtail_link_field-0.1.6-py3-none-any.whl Python 3 none any Details for wagtail_link_field-0.1.6-py3-none-any.whl

Total release size: 38.2 kB

Security for wagtail-link-field 0.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page