New links module?

Overview

Linking to things tends to be important in a CMS, and has high reuse value.
This can be observed in the large download numbers for GitHub - sheadawson/silverstripe-linkable: Easily add external or internal links to a dataobject with a dialog form. I’m keen to start a conversation with module authors on how we can take this forward.

Shea’s module is now deprecated. A common replacement is GitHub - elliot-sawyer/silverstripe-linkfield: Adds a Linkfield for gorriecoe/silverstripe-link
(plus the underlying data model of GitHub - elliot-sawyer/silverstripe-link: Adds a Link Object that can be link to a URL, Email, Phone number, an internal Page or File.).

The has_one UI is pretty cruddy (it’s using GridField’s defaults)

We’ve also created a similar (more limited) implementation of this as part of GitHub - silverstripe/silverstripe-elemental-bannerblock: A banner block for the dnadesign/silverstripe-elemental module.
It only supports internal links (see Should support external links · Issue #23 · silverstripe/silverstripe-elemental-bannerblock · GitHub), but has a nicer way to manage them (in a modal).
And it is tied to the concept of a “banner”, but should really be useable outside of that (see Should BlockLinkField be pulled out into its own module? · Issue #10 · silverstripe/silverstripe-elemental-bannerblock · GitHub).


image

In my opinion, all of these modules have a shortcoming in terms of UX: They force users
to treat links as an object that’s managed on it’s own screen, rather than something that’s easily inlined.
This becomes particularly apparent in blocks implementations where the inline editing flow becomes more important due to granular content creation (e.g. a “call to action” button)

Proposed Requirements

  • Supports linking to internal pages and external URLs
  • Supports linking to emails
  • Supports grouping links in has_many or many_many relationships (incl. sorting)
  • Can choose to open links in new window
  • Validates formats (e.g. valid email addresses)
  • Optinally supports linking to phone numbers (with good template defaults to make them behave like phone numbers)
  • Can be extended (e.g. to support custom formatted links to VOIP apps based on staff name lookup)
  • Same data model can be managed in different ways (= separate out form field into own module)
  • Supports drafting changes in links before publishing them
  • Can be folded into other form contexts without requiring it’s own detail view to reduce editing friction
  • Only shows options releva
  • Supports more than one link editing UI in the same form

Implementation notes

  • Regarding folding this into other form contexts (e.g. GridField), I see two options:
  • Option A: CompositeField which can be either applied to a “Link” DataObject, or any other DataObject directly. CompositeField would create database columns like “PageID”, “ExternalURL”, “OpenInNewWindow” etc
  • Option B: Create a form field which can manage a has_one relationship inline
  • Techncailly, we also have SilverStripe’s built in “link” modal dialog for HTML content, which only supports writing to HTML embedded shortcodes, not data objects. I don’t think those use cases need to overlap, because it’ll introduce quite a lot of complexity.
2 Likes

This is needed so much. It’s a constant source of pain for me.

3 Likes

It’s a constant source of pain for me too.

There’s further designs available in one of those GitHub issues you mentioned for the link field:

Designs: Design Systems Manager
Issue: Should support external links · Issue #23 · silverstripe/silverstripe-elemental-bannerblock · GitHub

Is the suggestion to ditch both of the existing options and make a new module? Or is it a question of which we should “adopt”? This discussion has come up often at SilverStripe too - quite a few developers already use and are familiar with the gorriecoe library, but the field will need to be “Reactified” at some point. That’s one advantage of the bannerblock implementation - it’s already “Reactified”.

There are lots of options for links, but generally only the URL is critical. Sometimes the link text may be critical too, but this depends on context as a button label etc. may be predefined.

Maybe a compromise could be to have inline editing for the critical component (whatever defines the URL) then have an options button to open a modal for non-critical settings.

Here’s a quick mockup of what I mean:

inline-link

Maybe a compromise could be to have inline editing for the critical component (whatever defines the URL) then have an options button to open a modal for non-critical settings.

Yeah, I think for blocks, the inline editing flow on the most important field(s) is becoming pretty crucial. We’re up against Squarespace and Wordpress Gutenberg in this regard, and those are butter smooth when it comes to quickly inserting content. That being said, we’ve already got designs for a modal, which is much better than a full CMS reload to a GridField detail form.

Is the suggestion to ditch both of the existing options and make a new module?

Assuming we can create editing UIs for anything, it comes down to data model choices:

  • Data Model Option A: Link DataObject. Implemented by sheadawson and gorriecoe. Easiest to extend via DataExtension. Can be shaped to has_one, has_many, many_many. More complex relationship versioning and handling (although we have that solved through cascading publish etc.). More performance impact on query time. Can use lots of built-in editing UIs directly (e.g. GridField). Works with other core features like diff views, reports, etc. But also adds more database tables and schema complexity. Most consistent, SilverStripe devs know how to deal with this.
  • Data Model Option B: DBLink DBField with serialised JSON. Implemented by bannerblock. Harder to extend. Serialised JSON makes it hard to query individual fields (without using something like GitHub - phptek/silverstripe-jsontext: JSON storage and querying and JSON database support). Harder to enforce consistent schema. Creates new dependency if done properly (e.g. through phptek’s module). Less native form handling (e.g. validation errors?)
  • Data Model Option C: DBLink CompositeField with individual database columns. Harder to extend. Easy to query. Strong schema. Can be wrapped in arbitrary DataObject structures or relationships. Can “inline” columns into a DataObject to avoid relationship in cases where only one link is required. Can use lots of built-in editing UIs indirectly (e.g. GridField through DataObject). Less native form handling (e.g. validation errors?)
  • Data Model Option D: Shortcodes. Just mentioning it for completeness here. Technically we could store dedicated links in the same format as links inlined in our HTML.

The easy path is Option A: DataObject. Given that we’re already dealing with deeply nested hierarchies in the CMS (Blocks, Images, etc), I don’t think doing this for links as well will make or break the system. And you get a lot of tooling from the wider SilverStripe ecosystem.

My least favourite is Option B, unless we take this all the way and create some schema validation and deep querying abilities (by adding jsontext). Which would increase our implicit maintenance surface on a supported module. jsontext wouldn’t need to become supported, but we’d be on the hook for any bugs that permeate through our own module surface :slight_smile: I’m sure @theruss has done a great job here, just generally wary of adding more dependencies on a team of eight devs maintaining 90+ modules. I think there’s a case to be made for moving much of the SilverStripe data model into JSON blob columns, but as a one-off I don’t feel like it adds a whole lot of benefit?

Any update on this becoming a thing? The UI of the elemental one is rather pleasant.

Not planned at the moment. One of our two project teams is busy with implementing MFA for the next two months, and the other has a pretty full backlog of GraphQL and a broader “recipe” for SilverStripe incl. more modules and default functionality. Help appreciated :slight_smile:

having an easy UI for managing has_many/many_many links WITH sort order is super, super nice and i wholeheartedly support this.

I’ve been thinking about this a little recently again. The ux team has come up with some good ideas and we’ve made a start on those in the bannerblock module as chillu mentioned above. Given we might implement the designs, as I see it there are these options:

  • A: Do everything ourselves from scratch. This not only includes the field but a lot of extra DB work
  • B: Fork and update Shea’s module which does a lot of the groundwork - we can update the field
  • C: Contribute a redesign to the gorriecoe/linkfield module - this provides a field in a module that relies on a different module (gorricoe/link) to handle all the extra DB stuff.
  • D: Create an alternative to gorriecoe/linkfield that still ties into the gorriecoe/link module. This could be done as a fork or as a brand new module

Option D and C might mean we add the gorriecoe modules to our “supported” list.

I think option D probably makes the most sense. Keeping those concerns separate is nice. Contributions can always be made back to the base module if required too :slight_smile:

We went with “Data Model Option B” and I regret that now. It has allowed us to easily add the additional fields for the different link types we have, but it’s a hassle to search.

But inline editing is definitely something we wouldn’t give up.
image

We turn different fields on and off depending on how we’re going to use the link. Some links won’t have a ‘Link text’ (eg an image or tile link). Some links allow adding onClick JavaScript, sometimes we restrict the link types based on the situation too.

No, our code isn’t in a state to be released, I’m here hoping there was an alternative I could migrate to :slight_smile:

We went with “Data Model Option B” and I regret that now.

Did you hit limits with using JSON through SilverStripe Framework ?

Do you mean authors searching for CMS content (e.g. link titles)? Or for feeding into a website search engine?

Sorry just checked, we used PHP serialization, not JSON. But no major issues with that.

Search is mainly when people come to me with questions like “What pages link to X external site?” or “What links open in a new window?” - at that point I wish all my links were in a separate table.

Other ‘challenge’ is writing unit tests which end up with:

AirNZ\Page\TileInfo:
  Test1:
    SubsiteID: =>SilverStripe\Subsites\Model\Subsite.Test
    ParentID: =>AirNZ\Page\ContentPage.Test
    Sort: 0
    Title: "Info Tile Title 1"
    Description: "Info Tile Description 1"
    TileLink: 'a:6:{s:11:"AddNoFollow";i:0;s:5:"Title";s:16:"Info tile link 1";s:4:"Type";s:3:"URL";s:3:"URL";s:24:"https://www.google.co.nz";s:5:"Email";s:0:"";s:15:"OpenInNewWindow";i:1;}'

It’s be a lot nicer to not have a serialized link in there.

I’m thinking of having a go at this over the next couple hack days.

My thinking is Option A (link as DataObjects). Each link type would have a dedicated DataObject. This would allow users who need a custom link type to create a new DataOject to handle it. Either we would have:

  • a parent Link DataObject that all other Link type DataObject extend,
  • an interface that you can be applied to any DataObject.

The front end would be based on Sacha’s design.

Registering link type for default linkField

# This defines the default link type when creating a generic LinkField
# Each Link type has a shorthand that can be used to disabled specific link
# types on individual LinkFields
Silverstripe\LinkEverything\LinkField:
  types:
    CMS: Silverstripe\LinkEverything\CMSLink
    External: Silverstripe\LinkEverything\ExternalLink
    Asset: Silverstripe\LinkEverything\AssetLink
    Mailto: Silverstripe\LinkEverything\MailtoLink
    Anchor: Silverstripe\LinkEverything\AnchorLink

Adding a Link relation on a DataObject

<?php

use Silverstripe\LinkEverything\Link;
use ACME\App\MagicDataObjectLink;

class PageWithALink extends Page {

    private static $has_one = [
        'MyLink' => Link::class
    ];

    public function getCMSFields()
    {
        $fields = parent::getCMSFields();

        $linkField = $fields->fieldByName('Root.Main.MyLink');

        $linkField
            # We're registering a custom link type specific to this field
            ->registerLinkType('Magic', MagicDataObjectLink::class)
            # This link field will be limited to 3 acceptable link types
            ->setValidLinkType(['CMS', 'External', 'Magic']);

        return $fields;
    }
}

Define a custom link type

<?php
namespace ACME\App;

use Silverstripe\LinkEverything\Link;

class MagicDataObjectLink extends Link {

    private static $has_one = [
        'MagicDataObject' => MagicDataObject::class
    ];

    public function getCMSFields()
    {
        $fields = parent::getCMSFields();
        # Basically you add which ever field you needs here
        # By default this will be rendered in a modal with a form schema
        return $fields;
    }

    /**
     * Some Link type can't be handled with a plain form schema in a modal
     * (e.g.: link to files). This will allow link types that need special logic
     * to define a custom component or handler to call instead.
     */
    public function getJsHandlerName()
    {
      return parent::getJsHandlerName();
    }

}
1 Like

Thanks for pushing this Max :slight_smile:

So given that these need to be versioned, those 5 built-in types plus base class will add 18 database tables (incl. versioned and draft). Any chance we can model this as a single object with a “Type” column, separate from classname? It’d mean a bit more conditional logic in the code.

I’m starting to wonder if this is a good foray into Option B and JSON data structures. It’s not the shortest path to building the module of course, but I’m starting to think this will be unavoidable due to performance concerns soon anyway on a wider scale. We already perform hundreds of read/write queries on every publish of a blocks page, due to ownerships, versioned link tracking, etc. And GitHub - silverstripe/silverstripe-versioned-snapshots: Tracks version history and modification state in DataObject ownership structures will make that even worse. And then we run all those queries and joins again on page renders, forcing complex caching implementations even for seemingly straightforward website content structures. Anyway, a bit of a wider discussion, but ideally build the frontend code in a way that makes it easy to switch data persistence down the line.

Do we know for a fact that having more table slows queries down significantly? Won’t we have the exact same problem with elemental block types?

More tables doesn’t automatically slow down database operation per se (up to a point…). It probably slows down database dump/restore operations, but haven’t benchmarked that. It’ll slow down dev/build. It’ll slow down history viewing.

But my main point is that assembling your content graph from more tables definitely causes more queries on render, and saving to more tables definitely causes more writes. A 3.x site with good old page types probably causes a dozen (read/write) queries on publish, a 4.x site with a bunch of content blocks quickly causes hundreds.

And now if a few blocks or items within those blocks have links, they’ll cause even more. It’s just a continuation of what we’ve already started in 4.x with versioning everything, and with pushing the adoption of blocks in their current form. This will make the problem a few percentage points worse than it already is. I think it’d be an interesting test case for a wider adoption of JSONText as a data format, but it’s scope creep. I won’t block a DataObject based approach here, just flagging the opportunity. I’ll have a look into the current state of JSONText in MySQL/MariaDB/RDS/Aurora, and will do a little write up on the steps we’d need to take to embed this into SS modules without breaking half of everyone’s shit :smiley:

Just a bit of an update from my end. I have been applying as many new features to link and updating its docs Silverstripe link | silverstripe-link.

I also was diagnosed with cancer last year so depending on my treatment, I may not be available for immediate fixes but when I’m available I try to smash out as much as possible. So in saying that here is a list of projects and their status.

gorriecoe/silverstripe-linkfield: Upgrade this so it is no longer dependant on gridfield and hasonefield. My acceptance criteria is:

  • Be able to distingish between has_one and many (currently doing this).
  • Drag and drop to rearrange
  • Toggle between nested and flat drag and drop so that modules such as gorriecoe/silverstripe-menu have a better UI
  • Inline editing (Jonoms mockup seems most appropriate)

gorriecoe/silverstripe-modallinkfield: To start development. Port Shea’s linkfield to a separate module. (Temporary solution to improve the UI)
gorriecoe/silverstripe-securitylinks: Done
gorriecoe/silverstripe-directionslink: Fix google maps field as it is not consistently showing.
gorriecoe/silverstripe-advancedemaillinks. Done.
gorriecoe/silverstripe-advancedphonelinks. To start development. Improve cms options for phone numbers. This will need include the option to define country dial from and to.
gorriecoe/silverstripe-linkicon. Add additional options to display icons such as svg support.
gorriecoe/silverstripe-ymlpresetlinks. Done
gorriecoe/silverstripe-shorturl. Done.
gorriecoe/silverstripe-anchors. To start development of a module that will register anchors on a Page. This will need to gather anchors from wysiwygs and related objects such as elemental blocks. Simply out a list of anchors with a function called “who knows” maybe getAnchors(). This can then be accessed by silverstripe-link or any other module to return a list of anchors on that page.
gorriecoe/silverstripe-link: Update documentation to provide as much working examples (On-going)

Also on a side note message me if you would like to become a maintainer.

1 Like

Hello @gorriecoe, sorry to hear about your health problems! I hope your treatment is going well. Thanks for keeping your code so modular, and for document it well on Silverstripe link | silverstripe-link. Max has been working on a PoC for a link module that’s based on “Data Model Option B” (DBLink DBField with serialised JSON), with a form field that can either save into a DataObject containing this DBField, or into the CompositeField directly. Maybe I’ll convince him to comment with his progress on here, and we can see where there’s overlap :slight_smile:

Hey just a follow up on where I got to. I’ve got a PoC module. It always you to save your link Data as a JSON string or as a DataObject. I’m not sold on this idea and we’re sill having some discussion internally about how we should approach this.

You can have a look at what it looks like in this YouTube vid.

1 Like