PHPackages                             justinholtweb/craft-freelink - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. justinholtweb/craft-freelink

ActiveCraft-plugin[Utility &amp; Helpers](/categories/utility)

justinholtweb/craft-freelink
============================

A powerful, lightweight link field for Craft CMS 5 with hybrid storage, proper element relations, and migration paths from popular link plugins.

5.1.0(2w ago)00proprietaryPHPPHP ^8.2

Since Jun 11Pushed 1w agoCompare

[ Source](https://github.com/justinholtweb/craft-freelink)[ Packagist](https://packagist.org/packages/justinholtweb/craft-freelink)[ RSS](/packages/justinholtweb-craft-freelink/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (5)Dependencies (4)Versions (5)Used By (0)

FreeLink for Craft CMS 5
========================

[](#freelink-for-craft-cms-5)

A powerful, lightweight link field plugin for Craft CMS 5. Hybrid storage with proper element relations, 12 built-in link types, and migration paths from Hyper, Linkit, Typed Link Field, and native Craft Link fields.

Why FreeLink?
-------------

[](#why-freelink)

FreeLink takes a different approach to link fields:

- **Hybrid storage** — Simple links (URL, email, phone) are stored as JSON in the content column. Element links (entries, assets, etc.) get proper rows in a relations table with foreign keys. This gives you referential integrity and reverse lookups without a separate cache table.
- **Lightweight models** — Links extend `yii\base\Model`, not Craft's `Element` class. Less overhead, simpler API.
- **Native CP UI** — Field UI uses Craft's Garnish library and vanilla JS. No Vue.js, no Alpine.js.

Requirements
------------

[](#requirements)

- Craft CMS 5.0.0+
- PHP 8.2+

Installation
------------

[](#installation)

```
composer require justinholtweb/craft-freelink
php craft plugin/install freelink
```

Configuration
-------------

[](#configuration)

Add a FreeLink field in **Settings &gt; Fields**. The settings UI lets you:

- Enable/disable individual link types and set custom labels
- Toggle between single-link and multi-link modes (with min/max constraints)
- Show/hide the link text field, new window toggle, and advanced attributes
- Set default link type and default new window behavior

### Available Link Types

[](#available-link-types)

TypeHandleDescriptionURL`url`Any URL with validationEmail`email`Email address (outputs `mailto:` links)Phone`phone`Phone number (outputs `tel:` links)SMS`sms`Phone number (outputs `sms:` links)Custom`custom`Arbitrary value, no validationSite`site`Relative path resolved against a site's base URLEntry`entry`Craft entry elementAsset`asset`Craft asset elementCategory`category`Craft category elementUser`user`Craft user elementProduct`product`Commerce product (requires Commerce)Variant`variant`Commerce variant (requires Commerce)Twig Usage
----------

[](#twig-usage)

### Single-Link Mode

[](#single-link-mode)

The field value is a `LinkCollection` that transparently proxies to the first link:

```
{# Output the URL #}
{{ entry.myLink }}
{{ entry.myLink.url }}

{# Display text (custom label or element title or URL) #}
{{ entry.myLink.text }}

{# Full  tag #}
{{ entry.myLink.link }}

{# With extra HTML attributes #}
{{ entry.myLink.link({ class: 'btn btn-primary', 'data-track': 'cta' }) }}

{# Access the linked element (entry, asset, etc.) #}
{{ entry.myLink.element }}
{{ entry.myLink.element.title }}

{# Check link properties #}
{{ entry.myLink.type }}          {# 'url', 'entry', etc. #}
{{ entry.myLink.isEmpty }}       {# true/false #}
{{ entry.myLink.isElement }}     {# true/false #}
{{ entry.myLink.newWindow }}     {# true/false #}
{{ entry.myLink.target }}        {# '_blank' or null #}

{# Advanced attributes #}
{{ entry.myLink.ariaLabel }}
{{ entry.myLink.title }}
{{ entry.myLink.classes }}
{{ entry.myLink.urlSuffix }}
```

### Multi-Link Mode

[](#multi-link-mode)

```
{# Iterate all links #}
{% for link in entry.myLinks.all %}
    {{ link.link }}
{% endfor %}

{# Count and first link #}
{{ entry.myLinks.count }}
{{ entry.myLinks.first.url }}

{# Filter by type #}
{% for link in entry.myLinks.filter(l => l.type == 'entry') %}
    {{ link.link }}
{% endfor %}
```

### Element Resolution

[](#element-resolution)

Element links (entry, asset, category, user, product, variant) resolve their target element automatically when you access it — no `.with()` needed:

```
{% for entry in craft.entries.section('news').all() %}
    {{ entry.myLink.link }}              {# resolves the linked element on access #}
    {{ entry.myLink.element.title }}
{% endfor %}
```

> **Note:** Because the field value is a `LinkCollection` value object (not a raw element list), it does **not** participate in Craft's `.with([...])` eager loading. Don't pass a FreeLink field handle to `.with()` — element links resolve lazily on access instead.

### Reverse Lookups

[](#reverse-lookups)

Find elements that link to a given element:

```
{% set linkedFrom = craft.freelink.getRelatedElements(entry) %}
{% set linkedFrom = craft.freelink.getRelatedElements(entry, 'myLink') %}
```

### Conditionals

[](#conditionals)

```
{% if not entry.myLink.isEmpty %}
    {{ entry.myLink.link }}
{% endif %}

{% if entry.myLink.isElement %}
    {# It's an element link, safe to access .element #}

{% endif %}
```

GraphQL
-------

[](#graphql)

FreeLink fields return `[FreeLinkInterface]` (always an array; single-link mode returns an array of one).

```
{
  entries {
    myLink {
      type
      url
      text
      label
      newWindow
      target
      ariaLabel
      title
      urlSuffix
      classes
      htmlId
      rel
      isEmpty
      isElement
    }
  }
}
```

Element API
-----------

[](#element-api)

FreeLink works out of the box with [`craftcms/element-api`](https://github.com/craftcms/element-api). `Link` and `LinkCollection` implement `JsonSerializable`, so field values serialize to clean JSON objects with resolved URLs, display text, and element metadata.

```
// config/element-api.php
use craft\elementapi\Plugin as ElementApi;
use craft\elements\Entry;

return [
    'endpoints' => [
        'news.json' => function() {
            return [
                'elementType' => Entry::class,
                'criteria' => ['section' => 'news'],
                'transformer' => function(Entry $entry) {
                    return [
                        'title' => $entry->title,
                        'link' => $entry->myLink,  // Single link → object
                        'links' => $entry->myLinks, // Multi link → array
                    ];
                },
            ];
        },
    ],
];
```

Single-link fields produce a JSON object:

```
{
  "link": {
    "type": "url",
    "url": "https://example.com#pricing",
    "text": "Visit Example",
    "target": "_blank",
    "newWindow": true,
    "isEmpty": false,
    "isElement": false
  }
}
```

Element link fields include additional metadata:

```
{
  "link": {
    "type": "entry",
    "url": "https://example.com/blog/my-post",
    "text": "My Blog Post",
    "target": null,
    "newWindow": false,
    "isEmpty": false,
    "isElement": true,
    "elementId": 42,
    "elementSiteId": 1,
    "elementType": "craft\\elements\\Entry",
    "elementTitle": "My Blog Post",
    "elementUrl": "https://example.com/blog/my-post",
    "elementStatus": "live"
  }
}
```

Multi-link fields produce an array of objects.

You can also call `toApiArray()` directly on any link for programmatic use:

```
$link = $entry->myLink->one();
$data = $link->toApiArray();
```

Custom Link Types
-----------------

[](#custom-link-types)

Register custom link types via the `EVENT_REGISTER_LINK_TYPES` event:

```
use justinholtweb\freelink\events\RegisterLinkTypesEvent;
use justinholtweb\freelink\services\Links;
use yii\base\Event;

Event::on(
    Links::class,
    Links::EVENT_REGISTER_LINK_TYPES,
    function(RegisterLinkTypesEvent $event) {
        $event->types[] = MyCustomLinkType::class;
    },
);
```

Your custom type should extend `justinholtweb\freelink\base\Link` (for simple links) or `justinholtweb\freelink\base\ElementLink` (for element links), and implement the `displayName()` and `handle()` static methods.

Migrating from Other Plugins
----------------------------

[](#migrating-from-other-plugins)

FreeLink includes console commands to migrate data from four popular link plugins. Each command converts the field type, transforms content data, and creates relations table rows for element links.

```
# Migrate from Verbb Hyper
php craft freelink/migrate/from-hyper

# Migrate from Linkit
php craft freelink/migrate/from-linkit

# Migrate from Typed Link Field
php craft freelink/migrate/from-typed-link

# Migrate from Craft's native Link field
php craft freelink/migrate/from-craft-link

# Check migration status
php craft freelink/migrate/status
```

### Options

[](#options)

FlagDescription`--field=`Migrate a specific field only`--backup`Create a full database backup before migrating`--dry-run`Preview changes without applying them### Safety

[](#safety)

- All migrators log each step to the `freelink_migrations` table
- `--dry-run` previews the entire migration without writing anything
- `--backup` uses Craft's built-in backup system before starting
- Migrate one field at a time with `--field` to reduce risk

Storage Architecture
--------------------

[](#storage-architecture)

### Content Column (JSON)

[](#content-column-json)

Every link stores its metadata as JSON in the field's content column:

```
{
  "type": "url",
  "value": "https://example.com",
  "label": "Visit Example",
  "newWindow": true,
  "ariaLabel": "Visit the Example website",
  "title": "Example Site",
  "urlSuffix": "#pricing",
  "classes": "btn btn-primary",
  "id": "",
  "rel": "noopener",
  "customAttributes": [
    {"attribute": "data-track", "value": "cta"}
  ]
}
```

For element links, `value` is `null` in the JSON. The actual element reference lives in the relations table.

### Relations Table (`freelink_links`)

[](#relations-table-freelink_links)

Element link references are stored with proper foreign keys:

ColumnFK TargetOn Delete`fieldId``fields.id`CASCADE`ownerId``elements.id`CASCADE`ownerSiteId``sites.id`CASCADE`targetId``elements.id`SET NULL`targetSiteId``sites.id`SET NULLThis means:

- Deleting a field or owner element automatically cleans up relations
- Deleting a target element sets `targetId` to null (the link gracefully becomes empty)
- Queries against the relations table use proper indexes and foreign keys

License
-------

[](#license)

This plugin requires a commercial license purchasable through the [Craft Plugin Store](https://plugins.craftcms.com).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance97

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

4

Last Release

20d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/035cb655c55af0e9e5b96754b80fd9703e195c32dbdfc49ae9a43ab9cf8db560?d=identicon)[justinholtweb](/maintainers/justinholtweb)

---

Top Contributors

[![justinholtweb](https://avatars.githubusercontent.com/u/295903?v=4)](https://github.com/justinholtweb "justinholtweb (12 commits)")

---

Tags

urllinkfieldsCraftcraftcmscraft-pluginlink field

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/justinholtweb-craft-freelink/health.svg)

```
[![Health](https://phpackages.com/badges/justinholtweb-craft-freelink/health.svg)](https://phpackages.com/packages/justinholtweb-craft-freelink)
```

###  Alternatives

[verbb/formie

The most user-friendly forms plugin for Craft.

101393.6k74](/packages/verbb-formie)[verbb/comments

Add comments to your site.

13753.9k1](/packages/verbb-comments)[verbb/vizy

A flexible visual editor field for Craft.

4250.4k](/packages/verbb-vizy)[verbb/hyper

A user-friendly links field for Craft.

24147.8k12](/packages/verbb-hyper)[verbb/workflow

Enforce multi-step review processes for creating entries.

138124.4k1](/packages/verbb-workflow)[verbb/social-poster

Automatically post entries to social media.

918.5k](/packages/verbb-social-poster)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
