RobotsTxtHandler project – How it works (part 2)

You need to have Episerver CMS project (CMS >= 10.0.2). Use Episerver NuGet (if you want check here) and search for Zanganeh.RobotsTxtHandler or directly running code below in “Package Manager Console”:

PM> Install-Package Zanganeh.RobotsTxtHandler

Then re-build  and run project. Then goto {siteurl}/EPiServer/CMS/Admin/Default.aspx and on left hand menu click on “tools -> Robots.txt Handler”:

capture

You can select current site from “Dropdown”and enter text you want to show as “robots.txt” file for that site.

When you save you can see list:

capture

 

And if you got to http://{siteurl}/robots.txt you can see what you entered in the textbox:

capture

 

You can delete or edit robots.txt value using same list:

capture

The  idea behind the scene is quite simple. For each site we store data of robots.txt into Dynamic Data Store. And having http handler that based on current site extract associated robots.txt and show the content. In next step I will describe Episerver add-on Gui.

IContentRepository.GetItems – how it works under hood!

I’ve been told IContentRepository.GetItems is heaps better. And when I asked  people why? They told me it is built DB load. Just want to clarify this. You pass a list of ContentReference (IEnumerable<ContentReference>) and it loads all IContent for each item. This seems to build DB load but when you take a look into code it does build load from MEMORY CACHE otherwise, it does load individually:

class EPiServer.Core.ContentProvider:

    protected virtual IEnumerable<IContent> LoadContents(IList<ContentReference> contentReferences, ILanguageSelector selector)
    {
      IList<IContent> contentList = (IList<IContent>) new List<IContent>();
      foreach (ContentReference contentReference in (IEnumerable<ContentReference>) contentReferences)
      {
        IContent content = this.Load(contentReference, selector);
        if (content != null)
          contentList.Add(content);
      }
      return (IEnumerable<IContent>) contentList;
    }

So please consider this as  performance consideration.

RobotsTxtHandler project (part 1)

As I wanted to get some  experience with Episerver plugin I started this project. This package allow site admin to add/edit/delete “robots.txt” file for Episerver site(s). Requirements  are as below:

  1. Provide “/robots.txt” handler
  2. Support multi-site
  3. Support multilingual
  4. Support multi-channel (e.g. for mobile channel provides different robots.txt)
  5. Easy admin area
  6. Support “default” robots.txt
  7. For UAT site disallow robots to  crawl the site (using web.config to override all  existing configs)
  8. Give some basic analytics data
  9. Plugin is based on MVC (for my own learning purpose only!)

Based  on above I broke it down to into three phases

Phase 1:

  1. Provide “/robots.txt” handler
  2. Support multi-site
  3. Plugin is based on MVC (for my own learning purpose only!)

Phase 2:

  1. Easy admin area
  2. Support “default” robots.txt
  3. For UAT site disallow robots to  crawl the site (using web.config to override all  existing configs)

Phase 3:

  1. Give some basic analytics data
  2. Support multilingual
  3. Support multi-channel (e.g. for mobile channel provides different robots.txt)

I already release the first RC for Phase 1 and in this tutorial, I will try to explain the challenges and what I learned. You  can access the  repo via:

https://github.com/zanganeh/RobotsTxtHandler

More than happy to get feedbacks on githib. Next step I will describe the architecture and base of plugin, nuget  package and MyGet integration!

How to debug Dojo in Episerver

Recently I struggled  with debugging a custom property code. I usually  check Console in chrome  developer tool to see if  I missed something but there was nothing in  there at all! I tried to debug Dojo and Episerver JS code to find out what a problem and all JS is minified! To  resolve this you can add:

<clientResources debug="true" />

to “web.config” -> “episerver.framework” section! With that all “warnings” from  dojo and episerver will be shown to you and JS is not minified! Hope it can save someone else time! Just remember to remove this item on your live site!

 

EPiServer Custom Property in Simple Steps by Step example!

One our fellow in world.episerver ask question about the custom property in EpiServer. This has been discussed many many time in forum and blogs but I couldn’t be able to find simple version to reference. In this post I’m trying to describe this in simple way!

Requirement: We want to get ahtur “First Name” and “Last Name” and store that into object with type named “Author” as below:

    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

 

So eventually in AlloySample having something like:

public class ArticlePage : StandardPage
{
	[UIHint(AuthorEditorDescriptor.UIHint)]
	[BackingType(typeof(PropertyAuthor))]
	public virtual Author Author { get; set; }
}

 

Solution:

  1. First create Alloy sample
  2. Definitely we need type so create “Author” under “Model” folder:
    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  3. PropertyDefinitionTypePlugIn: PropertyDefinitionTypePlugIni is used to introduce a “Type” as Episerver predefined “Type”!  The type needs to be based on  “PropertyString (long or short) or ProperttNumber.  So this will tells Episerver which  property “VALUE” (in our example complex type named  ‘Author’) need to be stored as ‘string’ or ‘int’. So what we do is serialize object as JSON string and store it as ‘string’ in DB. I will speak on separate post about performance considerations when you are building these kind of ‘Custom Properties’. So below is descriptor:
        [PropertyDefinitionTypePlugIn]
        public class PropertyAuthor : PropertyLongString
        {
            public override Type PropertyValueType
            {
                get { return typeof(Author); }
            }
    
            public override object Value
            {
                get
                {
                    var value = base.Value as string;
    
                    if (value == null)
                    {
                        return null;
                    }
    
                    return JsonConvert.DeserializeObject<Author>(value);
                }
    
                set
                {
                    if (value is Author)
                    {
                        base.Value = JsonConvert.SerializeObject(value);
                    }
                    else
                    {
                        base.Value = value;
                    }
                }
            }
    
            public override object SaveData(PropertyDataCollection properties)
            {
                return LongString;
            }
        }

     

  4. EditorDescriptor: With EditorDescriptor we instruct episerver UI  how to render the control in admin area. So we in our case we need two text boxes for “First Name” and “Last Name”  and admin can change the value. To achieve this we need to instruct Episerver to use what JS to render our property on admin area:
        [EditorDescriptorRegistration(TargetType = typeof(Author),
            UIHint = AuthorEditorDescriptor.UIHint)]
        public class AuthorEditorDescriptor : EditorDescriptor
        {
            public const string UIHint = "Author";
            private const string AuthorProperty = "alloy/editors/AuthorProperty";
    
            public AuthorEditorDescriptor()
            {
                ClientEditingClass = AuthorProperty;
            }
        }

     

  5. Dojo JS! This bit is most mysterious bit.  Episerver is using Dojo framework on admin area. So rendering will be happen  on client side! To achieve this  we need to create two file. One is HTML which is “template” file use by JS as template for rendering and actual JS file which will be called first to fire the  rendering process! So let’s create JS first.  Create asda in:captureAuthorProperty.js:
    define([
        "dojo/_base/declare",
        "dijit/_Widget",
        "dijit/_TemplatedMixin",
    
        "dojo/text!./templates/AuthorProperty.html",
        "dojo/dom",
        "dojo/domReady!"
    ],
    function (
        declare,
        _Widget,
        _TemplatedMixin,
        template,
        dom
    ) {
        return declare("alloy/editors/AuthorProperty", [
            _Widget,
            _TemplatedMixin], {
                templateString: template,
                _onFirstNameChange: function (event) {
                    if (!this.value)
                    {
                        this.value = { firstName: '', lastName: '' };
                    }
                    this.value.firstName = event.target.value
                    this._set('value', this.value);
                    this.onChange(this.value);
                },
                _onLastNameChange: function (event) {
                    if (!this.value) {
                        this.value = { firstName: '', lastName: '' };
                    }
                    this.value.lastName = event.target.value
                    this._set('value', this.value);
                    this.onChange(this.value);
                },
                _setValueAttr: function (val) {
                    if (val) {
                        this.firstName.value = val.firstName;
                        this.lastName.value = val.lastName;
                        this._set('value', val);
                    }
                },
                isValid: function () {
                    return true;
                }
            }
        );
    });

     

    AuthorProperty.html:

    <div>
        <label for="firstname">First name</label>
        <input type="text" data-dojo-attach-point="firstName" 
               name="firstname"
               data-dojo-attach-event="onchange:_onFirstNameChange" />
    
        <label for="lastName">Last name</label>
        <input type="text" data-dojo-attach-point="lastName" 
               name="lastName" 
               data-dojo-attach-event="onchange:_onLastNameChange" />
    </div>

    Hope it help.

Mobile Visitor Group criteria for EPiServer

We have heaps of requests from our client. There is a good reference here and we little a bit extended this using below approach, to define if the current user is on mobile or not and show, hide content based on this concept. First step is to define model and this will be stored in Dynamic  Data Store (DDS). This will be stored so CMS can use it to apply the criteria:

    public class BrowserModel : CriterionModelBase
    {
        [DojoWidget(
            SelectionFactoryType = typeof (EnumSelectionFactory),
            LabelTranslationKey = "/shell/cms/visitorgroups/criteria/browser/browsertype",
            AdditionalOptions = "{ selectOnClick: true }"),
         Required]
        public BrowserType Browser { get; set; }

        public override ICriterionModel Copy()
        {
            return ShallowCopy();
        }
    }

    public enum BrowserType
    {
        Desktop,
        Mobile
    }

Second step is to define a criteria and the logic which make decision about if the current user is on mobile or not! We are using  current user request UserAgent:

[VisitorGroupCriterion(
        Category = "User Criteria",
        DisplayName = "Browser",
        Description = "Criterion that matches type of the user's browser",
        LanguagePath = "/shell/cms/visitorgroups/criteria/browser")]
    public class BrowserCriterion : CriterionBase<BrowserModel>
    {
        public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext)
        {
            return MatchBrowserType(httpContext.Request.UserAgent);
        }

        protected virtual bool MatchBrowserType(string userAgent)
        {
            var os =
                new Regex(
                    @"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino",
                    RegexOptions.IgnoreCase | RegexOptions.Multiline);
            var device =
                new Regex(
                    @"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-",
                    RegexOptions.IgnoreCase | RegexOptions.Multiline);
            var deviceInfo = string.Empty;

            if (os.IsMatch(userAgent))
            {
                deviceInfo = os.Match(userAgent).Groups[0].Value;
            }

            if (device.IsMatch(userAgent.Substring(0, 4)))
            {
                deviceInfo += device.Match(userAgent).Groups[0].Value;
            }

            if (!string.IsNullOrEmpty(deviceInfo))
            {
                return Model.Browser == BrowserType.Mobile;
            }

            return Model.Browser == BrowserType.Desktop;
        }

Hope it can save you some time on writing custom code!

To exclude uploaded EPiServer Form file uploaded (FileUploadElementBlock) from EPiServer Find Index

There is a flues in EPiServer Form which all uploaded file can be indexed. To exclude uploaded file using EPiServer Form -> FileUploadElementBlock from EPiServer Find indexer you can go:

    [ModuleDependency(typeof(InitializationModule))]
    public class EPiServerFindInitialization : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            ContentIndexer.Instance.Conventions.ForInstancesOf().ShouldIndex(ShouldIndexDocument);
        }

        public void Uninitialize(InitializationEngine context)
        {
        }

        bool ShouldIndexDocument(DocumentFileBase documentFileBase)
        {
            if (contentAssetHelper.Service.GetAssetOwner(documentFileBase.ContentLink) is FileUploadElementBlock)
            {
                IEnumerable result;
                ContentIndexer.Instance.TryDelete(documentFileBase, out result);

                return false;
            }

            return true;
        }

        readonly Injected contentAssetHelper;
    }

Solution Architect vs Agile

What is the definition of Architecture? There many definition but I like this one “defining a structured solution that meets all of the technical and operational requirements, while optimizing common quality attributes such as performance, security, and manageability”. It is very broad but what most of the people think of the architecture is a definition of how software structure should looks like but how much deep you should go? Is the job of solution architect is design the architecture and go away? In my opinion this is not going to work. Especially with Agile methodology it is hard to define all requirements upfront! So I think it is iterative process and solution architect needs to iteratively groom and adapt his solution. The architecture can’t be change too much because it would be expensive but the design could change. But is software architect needs to manage the team or based on Agile sole the team needs to be self-managed. I think solution architect needs to help team and there is no management. Solution architect needs to make sure his solution and design understood and it has been adapted properly and if there is any change needs to be done should be considered in next iteration or if it is blockage need to consider to change immediately. Solution architect can use code review as one of best tools to audit the design and making sure it adapted and communicated properly. I think solution architect need to read and write code, maybe more reading but should write some code to be hand on.

How to pass configuration to your custom editor in EPiServer

Recently I’m working on a project which has a requirement. We need focal point selector for an image. So the idea is you pick a focal point and application based on requested image size crop the image based on selected focal point. So I store the focal point (x,y) as a comma separated in string.

public class ImageFile : ImageData
{
	[UIHint("FocalPoint")]
	public virtual string FocalPoint { get; set; }
}

Now we want make the EPiServer custom editor named “FocalPoint”. There are many post about this so I ignore this part but what I need to pass current image URL to be passed to my editor. So for doing this we need EditorDescriptor as below:


	[EditorDescriptorRegistration(TargetType = typeof(string), UIHint = "FocalPoint")]
	public class FocalPointEditorDescriptor : EditorDescriptor
	{
		public FocalPointEditorDescriptor()
		{
			this.ClientEditingClass = "alloy.editors.FocalPointSelector";
		}
		public override void ModifyMetadata(ExtendedMetadata metadata, IEnumerable attributes)
		{
			base.ModifyMetadata(metadata, attributes);
		}
	}

Now we want to change the code to pass current image URL to editor, so editor can load the image and user can select the focal point. Code below is a magic which need to be injected into ModifyMetadata method.


			var contentDataMetadata = metadata as ContentDataMetadata;
			if (contentDataMetadata != null)
			{
				var imageFile = contentDataMetadata.OwnerContent as IContentMedia;
				if (imageFile != null)
				{
					metadata.EditorConfiguration["imageURL"] = UrlResolver.Current.GetUrl(imageFile.ContentLink);
				}
			}

And now we can access the value in our edit in JS:


define([
	"dojo/_base/connect",
	"dojo/_base/declare",
	"dijit/_CssStateMixin",
	"dijit/_Widget",
	"dijit/_TemplatedMixin",
	"dijit/_WidgetsInTemplateMixin",
	"epi/shell/widget/_ValueRequiredMixin",
	"dojo/dom",
	"dojo/on",
	"dojo/text!./templates/FocalPointSelector.html"],

	function (
		connect,
		declare,
		_CssStateMixin,
		_Widget,
		_TemplatedMixin,
		_WidgetsInTemplateMixin,
		_ValueRequiredMixin,
		dom,
		on,
		template) {

		return declare(
			"ras.editors.FocalPointSelector", [
				_Widget,
				_TemplatedMixin,
				_WidgetsInTemplateMixin,
				_CssStateMixin,
				_ValueRequiredMixin],
			{
				templateString: template,
				selector: null,
				intermediateChanges: false,
				image: null,
				value: null,
				onChange: function (value) { },
				postCreate: function () {
					this.image = dojo.create("img", { "style": "width:100%;height:100%", "src": this.imageURL }, this.domNode);
				}
			});
	});

And you can see “imageURL” is being passed to our javascript code and we can use it.

Do we really need accessible webiste

I’m currently working on website which should be accessible! What that means? I thought it could be waste of time to spend huge amount of time to make a website accessible because no one will use it. BUT just did some research and saw this video . I’m really surprised because in North America about %64 of blind people are advanced user of internet (link) so I think it is very important and I think most of websites need to be compatible with WCAG and first step to make this happen is to make famous web related frameworks (e.g. Angular) to be compatible with this. I really like to have comprehensive post about how to make this happen and a checklist for webmaster and developers to keep in mind. Unfortunately there is a good tool that help you to achieve. There are some and we used them but when we ask real user to use website they can’t because of dynamic structure and thing people designed these tools are not blind! So I really look to a post (perhaps series) about this.