承接 justinholtweb/craft-freelink 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

justinholtweb/craft-freelink

最新稳定版本:5.0.0

Composer 安装命令:

composer require 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.

README 文档

README

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?

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

  • Craft CMS 5.0.0+
  • PHP 8.2+

Installation

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

Configuration

Add a FreeLink field in Settings > 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

Type Handle Description
URL url Any URL with validation
Email 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 validation
Site site Relative path resolved against a site's base URL
Entry entry Craft entry element
Asset asset Craft asset element
Category category Craft category element
User user Craft user element
Product product Commerce product (requires Commerce)
Variant variant Commerce variant (requires Commerce)

Twig Usage

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 <a> 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

{# 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 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

Find elements that link to a given element:

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

Conditionals

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

{% if entry.myLink.isElement %}
    {# It's an element link, safe to access .element #}
    <img src="{{ entry.myLink.element.url }}" alt="{{ entry.myLink.element.title }}">
{% endif %}

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

FreeLink works out of the box with 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

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

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

Flag Description
--field=<handle> Migrate a specific field only
--backup Create a full database backup before migrating
--dry-run Preview changes without applying them

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

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)

Element link references are stored with proper foreign keys:

Column FK Target On Delete
fieldId fields.id CASCADE
ownerId elements.id CASCADE
ownerSiteId sites.id CASCADE
targetId elements.id SET NULL
targetSiteId sites.id SET NULL

This 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

This plugin requires a commercial license purchasable through the Craft Plugin Store.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: proprietary
  • 更新时间: 2026-06-11

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固