Showing posts with label Orchard. Show all posts
Showing posts with label Orchard. Show all posts

Saturday, March 29, 2014

Startup code in Orchard module

If you work with some CMS it will be better if you will play by its rules. E.g. in Sharepoint basic way to add customizations is feature. It allows you to add custom functionality to the site. In Orchard the basic way to apply customization is module. There may be a lot of custom modules in your project and the more each module will be independent from others the easier maintenance will be.

In the module you may also implement custom functionality like controllers, views and models. Some of this functionality may require adding of initialization code (like adding custom model binder), which in regular ASP.Net MVC application goes to global.asax.cs Application_Start() method. Orchard has own global.asax, but it is located in the common Orchard.Web project which is not related with your module. Is there a way to add initialization code which will be executed on startup for the module? Yes, and it is quite simple. In order to do it we need to add a class which inherits Autofac.Module class to the module’s project (Autofac is IoC library used in Orchard) and override its Load method:

   1: public class ContactFormModule : Module
   2: {
   3:     private bool isInitialized = false;
   4:     protected override void Load(ContainerBuilder builder)
   5:     {
   6:         if (!this.isInitialized)
   7:         {
   8:             ModelBinders.Binders.Add(typeof (FooModel), new FooModelBinder());
   9:             this.isInitialized = true;
  10:         }
  11:     }
  12: }

And that’s basically all. Orchard will automatically wire it up and call during application initialization.

Saturday, March 8, 2014

Multilingual site on Orchard CMS with single codebase and content database depending on domain name

When we need to create multilingual site on Orchard we have several options:

  1. Create site on first basic language, then copy content database and translate all content. In this case we will have 2 separate databases and will need to maintain them separately, although codebase can be the same (however you will still need to create separate sites in IIS in order to specify different connection strings in \App_Data\Sites\Default\Settings.txt). Also you will need new content database for each new language;
  2. After creating the site on basic language go to Admin panel > Settings and add necessary languages to the list of available languages (via “Add or remove supported cultures for the site” link). After that in Modules install and enable Localization module which allows to create languages variations for each content item, like pages. There will be “New translation” under each page:

image

Using other module called “Culture layer” you may create new layers in Widgets for separate languages. E.g. this is how you may create layer for front page for Russian and English languages:

Name Rule
TheHomepage ru-RU url("~/") and lang("ru-RU")
TheHomepage en-US url("~/") and lang("en-US")

(lang rule is added with “Culture layer” module). After that you may add different widgets and define localized content in different layers.

Now when you will change language in Settings > Default site culture, you will see localized content. But in this case it still will be needed to have different site in IIS and different copies of content databases for each site which will have only one difference in Default site culture, which isn’t good. The better way would be to make it so that site language will be selected automatically based on the domain name of the site? I.e. when we visit site by http://example.ru it will use Russian, but when by http://example.com – English, while both host headers will point to the single IIS site. The answer is yes, it is possible with Orchard.

In order to do it we will need to create new Module for our site. Into Content folder our module we will put text file with list of domains of 1st level and appropriate locale names:

ru:ru-RU;com:en-US

Then we create new class which inherits ICultureSelector interface from Orchard framework. It will parse text file with locales and depending on current host name will choose the correct one:

   1: public class CultureSelector : ICultureSelector
   2: {
   3:     private const string DEFAULT_CULTURE = "ru-RU";
   4:     private const int CULTURE_PRIORITY = 1000;
   5:  
   6:     public CultureSelectorResult GetCulture(HttpContextBase context)
   7:     {
   8:         var defaultCulture = new CultureSelectorResult {Priority = CULTURE_PRIORITY,
   9: CultureName = DEFAULT_CULTURE};
  10:         try
  11:         {
  12:             var locales = this.getLocales();
  13:             if (!locales.Any())
  14:             {
  15:                 return defaultCulture;
  16:             }
  17:  
  18:             string host = HttpContext.Current.Request.Url.Host;
  19:             int idx = host.LastIndexOf(".");
  20:             if (idx < 0)
  21:             {
  22:                 return defaultCulture;
  23:             }
  24:             var currentDomain = host.Substring(idx + 1).ToLower();
  25:             var currentLocale = locales.FirstOrDefault(l =>
  26: l.Domain == currentDomain);
  27:             if (currentLocale == null)
  28:             {
  29:                 return defaultCulture;
  30:             }
  31:             return new CultureSelectorResult {Priority = CULTURE_PRIORITY,
  32: CultureName = currentLocale.Locale};
  33:         }
  34:         catch
  35:         {
  36:             return defaultCulture;
  37:         }
  38:     }
  39:  
  40:     private List<dynamic> getLocales()
  41:     {
  42:         string path = HttpContext.Current.Server.
  43: MapPath("~/Modules/Example.Localization/Content/locales.txt");
  44:         string locales = File.ReadAllText(path);
  45:         if (string.IsNullOrEmpty(locales))
  46:         {
  47:             return new List<dynamic>();
  48:         }
  49:  
  50:         string[] pairs = locales.Split(new[] { ';' },
  51: StringSplitOptions.RemoveEmptyEntries);
  52:         if (!pairs.Any())
  53:         {
  54:             return new List<dynamic>();
  55:         }
  56:  
  57:         var result = new List<dynamic>();
  58:         foreach (string pair in pairs)
  59:         {
  60:             int idx = pair.IndexOf(":");
  61:             if (idx < 0)
  62:             {
  63:                 continue;
  64:             }
  65:  
  66:             result.Add(new {Domain = pair.Substring(0, idx),
  67: Locale = pair.Substring(idx + 1)});
  68:         }
  69:         return result;
  70:     }
  71: }

Now if we will open our site in ru domain, we will see Russian content, while in com – English.

Saturday, December 8, 2012

Problem with robots.txt module for Orchard CMS and HTTP 404 Not found

Many modern CMS allow you to specify content of robots.txt manually, which then will be available for search engines by the regular URL: http://example.com/robots.txt. In Orchard you can use Robots module from online gallery for that. This module adds dynamic route for robots.txt file:

   1: public IEnumerable<RouteDescriptor> GetRoutes() {
   2:     return new[] {
   3:         new RouteDescriptor {   Priority = 5,
   4:                                 Route = new Route(
   5:                                     "robots.txt",
   6:                                     new RouteValueDictionary {
   7:                                         {"area", "SH.Robots"},
   8:                                         {"controller", "Robots"},
   9:                                         {"action", "Index"}
  10:                                     },
  11:                                     new RouteValueDictionary(),
  12:                                     new RouteValueDictionary {
  13:                                         {"area", "SH.Robots"}
  14:                                     },
  15:                                     new MvcRouteHandler())
  16:         },
  17:     };
  18: }

Content of robots.txt is stored in the CMS content database. You can change it from admin panel in Robots.txt section.

In some cases when you will install this module and enable robots.txt feature, you will get HTTP 404 Not found error when will try to access http://example.com/robots.txt. In Orchard by default all static files from the virtual folder will give 404 result. David Hayden described it in his post: Modifying Web.config to Serve Site.xml and Static Files with Orchard CMS.

However in our case robots.txt is not static file. It is dynamic ASP.Net MVC route. So why we can get 404 error? It can be caused if someone put real static robots.txt file to the virtual folder. In this case request will be handled by IIS without ASP.Net MVC (real files have priority over dynamic routes), and you will get 404 for robots.txt because as I said above in Orchard all static files give 404 by default. In order to fix the issue, remove static robots.txt from the route.

Several useful SEO rules via IIS URL rewrite module

In this post I would like to share with you several useful rules for IIS URL rewrite module, which can increase rating of your site for search engines. Before to add these rules to the web.config we need to install URL rewrite extension on the web server, otherwise site won’t work.

The first rule is redirect with 301 (Permanent redirect) http status code from the url without www prefix to the url with www. Very often sites are available by both urls, e.g. http://www.example.com and http://example.com. Search engines may treat these sites as separate (most of search engines may handle this case, but I would not suggest experimenting) and as content on them will be the same, search rank may be decreased. In order to fix this problem we need to setup permanent redirect from one address to another. It is important to use permanent redirect (301), because in this case search engine will know that url from which redirect was made is not used anymore and can be deleted from search index. Another way to perform redirect is to use 302 (Moved temporarily), but in this case page won’t be deleted from index. In theory it also can impact search rating, but I didn’t find exact evidences so if you know that please share it in comments.

In order to add the redirection rule to the site running on IIS we will use IIS URL rewrite module. You can add rules by several ways:

  • via UI – in IIS manager select site under the question and then select URL rewrite in the right panel (it will be added after installation of URL rewrite extension. Note that you need to restart IIS manager after installation. No need to make iisreset)
  • directly to the web.config using any text editor, e.g. notepad.

UI is just convenient interface for adding rules to the web.config. Result in both cases will be the same: rules will be added to the web.config of your site. In this post I will show how to add rules to web.config directly.

Rules are added to the <system.webServer> section under <rewrite>/<rules>. Redirection rule from no www to www will look like this:

   1: <rule name="redirect_from_nowwww_to_www" enabled="true" stopProcessing="true">
   2:   <match url=".*" />
   3:     <conditions>
   4:       <add input="{HTTP_HOST}" pattern="^example\.com$" />
   5:     </conditions>
   6:   <action type="Redirect" url="http://www.example.com/{R:0}"
   7: appendQueryString="true" redirectType="Permanent" />
   8: </rule>

Note that above I separated <action> to 2 lines (lines 6 and 7) in order to fit the article width, but in real example it should be single line. This rules tells to rewrite module that it should be applied to all urls (line 2) which have host “example.com” (line 4) and response should be redirected with 301 status code to “http://www.example.com” (lines 6-7). Here for matching urls we used regular expressions. {R:0} means back reference in term of regexp, which in this example has url part after the host. Plus we need to add possible query string in order to keep existing functionality working: see appendQueryString="true" attribute.

After that you can run fiddler and check that when you enter http://example.com in the browser, you will be redirected to http://www.example.com with 301 status. The rule will be also applied to all pages on the sites, including images and css.

Another useful rule is redirect from addresses without trailing slash to the addresses with slash: http://example.com/about –> http://example.com/about/. The reason here is the same: search engines may treat these url as different urls and if they will have the same content (in most cases they will), search rank can suffer. In order to add these redirection you can use the following url:

   1: <rule name="add_trailing_slash" stopProcessing="true">
   2:   <match url="(.*[^/])$" />
   3:   <conditions>
   4:     <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
   5:     <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
   6:   </conditions>
   7:   <action type="Redirect" redirectType="Permanent" url="{R:1}/" />
   8: </rule>

Here we used several conditions which mean that if physical file or folder are requested (matchType="IsFile" and matchType="IsDirectory"), the rule should not be applied (negate="true"). With this url you may have problems, e.g. if you use some CMS which allows to edit content of commonly used in SEO files robots.txt and sitemap.xml dynamically (i.e. if content is stored in the content database and routings are configured dynamically: http://example.com/robots.txt or http://example.com/sitemap.xml). In this case IIS won’t treat them as files and will add trailing slash: http://example.com/robots.txt/. Also it may cause problems on login view or administrative views if you use ASP.Net MVC (some actions won’t work). Solution is do disable this rule for these views. In order to avoid mentioned problems we need to add several additional conditions:

   1: <add input="{REQUEST_FILENAME}" matchType="Pattern" negate="true"
   2: pattern="robots\.txt" />
   3: <add input="{REQUEST_FILENAME}" matchType="Pattern" negate="true"
   4: pattern="sitemap\.xml" />
   5: <add input="{REQUEST_URI}" matchType="Pattern" negate="true" pattern="^/admin.*" />
   6: <add input="{REQUEST_URI}" matchType="Pattern" negate="true" pattern="^/users/.*" />
   7: <add input="{REQUEST_URI}" matchType="Pattern" negate="true"
   8: pattern="^/packaging/.*" />

This example contains conditions for administrative views for Orchard CMS. Here I excluded internal urls “/admin”, “/users”, “/packaging” from the rule (these urls are used in Orchard).

Note that also I used lowercase rule names with underscores, you may use more user friendly names for your rules. I used to this syntax :) That’s all what I wanted to write about in this article. Hope that it will be useful for you.

Saturday, October 20, 2012

Hide content type fields in Orchard CMS

In Orchard CMS there is a concept of content types. From high vision it is similar to the content types idea in Sharepoint. It has less features of course, but the general idea is the same: you may create your own content types, add new content parts or fields and then create content items based on created content type. Content parts can be found from Dashboard > Content > Content Parts:

image

You can add fields of the following types (available in 1.5.1 version):

  • Boolean
  • Content picker (allows you to select content items like pages and display them on page)
  • Date time
  • Enumeration (shows list of predefined values. You can choose how you want to display values: using listbox, dropdown list, radio button list, checkboxes list)
  • Input (allows to add input fields to your content type, add validation for phones and emails, define max length, watermark, custom css and other features)
  • Link
  • Media picker (allows to show items from Media folder, e.g. images)
  • Numeric
  • Text (can be plain text, multiline text, html)

In order to add new field you should go to the Dashboard > Content > Content types > Edit content type > Add field:

image

What is missing in the current implementation is the ability to hide added fields. E.g. if you need to add metadata for displaying in liftups (or projections in terms of Orchard), but not in Detail view. In Sharepoint you can define fields visibility for each form separately using the following properties of SPField class: ShowInDisplayForm, ShowInEditForm, ShowInNewForm, ShowInViewForms. So how to hide field in e.g. detail view (default view which is used when you open e.g. the page)? I found the following way.

First of all you need to install Designer Tools module and enable Shape Tracing feature in it:

image

At the moment of writing Designer Tools module also has Url Alternates and Widget Alternates features. Once you enabled Shape Tracing, you will see the following icon in the right bottom corner of the page:

image

If you will click on it you will see panel similar to the firebug or IE developer tools:

image

Now you can check all shapes on your page using convenient visual representation like you do in firebug. In order to hide the field at first find its shape on the page. Here you can create alternate template for displaying appropriate field. Click Create button near the template name which is more suitable for you. After that panel will look like that:

image

Active template shows the path to the View which is currently used for rendering the field:

Active Template: ~/Themes/Terra/Views/Fields.Common.Text-Logo.cshtml

Now go to that folder and check the code of the view. For text fields it looks like this:

   1: @using Orchard.Utility.Extensions;
   2: @{
   3:     string name = Model.ContentField.DisplayName;
   4: }
   5:  
   6: if (HasText(name) && HasText(Model.Value)) {
   7:     <p class="text-field"><span class="name">@name:</span>
   8:     <span class="value">@(new MvcHtmlString(Html.Encode((HtmlString) Model.Value)
   9:         .ReplaceNewLinesWith("<br />$1")))</span></p>
  10: }

In order to hide the field we need to modify the view a bit:

   1: @using Orchard.Utility.Extensions;
   2: @{
   3:     string name = Model.ContentField.DisplayName;
   4: }
   5:  
   6: @if (Model.Metadata.DisplayType != "Detail") {
   7:     if (HasText(name) && HasText(Model.Value)) {
   8:         <p class="text-field"><span class="name">@name:</span>
   9:         <span class="value">@(new MvcHtmlString(Html.Encode((HtmlString) Model.Value)
  10:             .ReplaceNewLinesWith("<br />$1")))</span></p>
  11:     }
  12: }

Here on line 6 I added additional condition: @if (Model.Metadata.DisplayType != "Detail"). So view will be rendered only in other than Detail display types. After that you will have content items with metadata which you can use e.g. only in projections, but which won’t be shown in default view.

May be there is better and faster way to do that in Orchard, but I didn’t find it yet. If you know other ways to achieve the same result please share them in comments.