OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.36k stars 2.37k forks source link

Implementation notes for decoupled CMS #3553

Closed dodyg closed 5 years ago

dodyg commented 5 years ago

I am going to write a running commentary and notes for a decoupled CMS website implementation so it can be a reference for official documentation/guide.

I am using

  <PackageReference Include="OrchardCore.Application.Cms.Targets" Version="1.0.0-beta3-71077" />

The website I am building contains:

I am not using any themes in this decoupled CMS implementation because it's not really obvious how to use it or whether it is beneficial at all.

I prefer the decoupled CMS approach at this moment because as a developer, using the traditional approach requires higher learning curve than using decoupled approach.

There are things like Zone, Placement or whatnot that are simply not obvious and I assume necessary when using traditional approach. Using decoupled CMS, I just need to worry about getting the content and I can put them exactly where I want.

dodyg commented 5 years ago

It's important to inherit @inherits OrchardCore.DisplayManagement.Razor.RazorPage<TModel> in all your cshtml shape templates.

shape

Note: I am not sure whether the CMS will pick up the shape templates anywhere or it has to be under /Views.

dodyg commented 5 years ago

The concept of Shape and Shape Templates needs better discussion in the current documentation. All the shape discussion is based on Orchard Core 1.0 documentation.

dodyg commented 5 years ago

Do not precompile your Razor otherwise your Shape templates won't work

This is how to prevent Razor precompilation

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
    <RazorCompileOnPublish>false</RazorCompileOnPublish>
  </PropertyGroup>

shapes-folder

The Views folder contains the Shapes templates. If you precompile your Razor views, this 'Views' folder won't be copied over and your Shapes rendering customization won't work.

Example When Shape templates are available shapes-output-1

When Shape templates are missing because the Views folder isn't at the published folder shapes-output-2

dodyg commented 5 years ago

SQL Queries module is kinda useless in comparison to Lucene Queries module because the thing that you can sort and filter in SQL Queries is very limited. You can't for example filter/sort by your Content Types field in SQL Queries. That just makes it a deal breaker. Lucene Queries doesn't have this limitation.

dodyg commented 5 years ago

All you need is love and

private readonly OrchardCore.IOrchardHelper _helper;
private readonly OrchardCore.Queries.IQueryManager _queryManager;

or @inject them at your views.

These two are pretty much all you need to implement decoupled CMS AFAIK.

dodyg commented 5 years ago

Get by Alias

await _helper.GetContentItemByAliasAsync("alias:blog");

Get by Slug

await _helper.GetContentItemByAliasAsync("slug:blog/my-awesome-content");
dodyg commented 5 years ago

Taxonomy module sounds cool but I have no idea how to use it from the Admin or how it relates to content.

OK this article is really useful: https://www.davidhayden.me/blog/taxonomies-in-orchard-core-cms

dodyg commented 5 years ago

I wish there's an option to modify the /admin path for OrchardCore admin UI.

dodyg commented 5 years ago

Admin Menu should be enabled by default in an empty installation. It's super useful and it's not obvious for people implementing OrchardCore for the first time.

dodyg commented 5 years ago

Content Types and Content Parts need discussions at the documentation. Right now people have to rely on Orchard 1 documentation.

This whole Orchard Core basic concepts from v1 need to be migrated and updated.

dodyg commented 5 years ago

I still can't find a way how to obtain the site name and display it in Razor page.

agriffard commented 5 years ago

@inject OrchardCore.Settings.ISiteService SiteService

string siteName = (await SiteService.GetSiteSettingsAsync()).SiteName;

dodyg commented 5 years ago

It would be super nice if there's a version of _helper.GetContentItemByAliasAsync; that takes multiple aliases.

dodyg commented 5 years ago

Who would have thought that the humble TextField has awesome list of editors!

predefined-list

dodyg commented 5 years ago

@agriffard how does one load Widget in decoupled CMS? Just this _helper.GetContentItemByIdAsync?

dodyg commented 5 years ago

Once some keys concepts are understood, developing decoupled CMS with OrchardCore is a very pleasant experience.

dodyg commented 5 years ago

display-json-model

This JSON rendering of a Content Item is just so invaluable. In this case this is a taxonomy content item. @Model.Topics.Content.

sebastienros commented 5 years ago

It's important to inherit @inherits OrchardCore.DisplayManagement.Razor.RazorPage in all your cshtml shape templates.

No. It has lots of helpers but you don't need them if you are not using a theme usually. You just need to inject @inject OrchardCore.IOrchardHelper OrchardHelper and you have access to most of the things you need in a decoupled CMS.

Do not precompile your Razor otherwise your Shape templates won't work

You don't need shapes either. You should precompile your views.

SQL Queries module is kinda useless in comparison to Lucene Queries

There is a PR that provides it already, I will blame @Skrypt if it's not ready yet

I still can't find a way how to obtain the site name and display it in Razor page.

We definitely need to add an helper to IOrchardHelper for this

how does one load Widget in decoupled CMS

My advice is to create a "Section" content type that is based on FlowPart and you give it an alias. then you can load it and render it. You can also just use it as a data structure and render it by yourself.

json

Yes, we need helpers to render a json tree that can be navigated in html directly

something like this: https://www.jqueryscript.net/other/Beautiful-JSON-Viewer-Editor.html But we should do it in the admin first, simpler and would solve most issues

dodyg commented 5 years ago

Helper.QueryCategorizedContentItemsAsync usage isn't yet documented.

 await _helper.QueryCategorizedContentItemsAsync(query =>
            {
               return query.Where(x => x.TermContentItemId == contentItemId);
            });
dodyg commented 5 years ago

This sample from the documentation isn't clear.

@foreach(var termId in Model.TermContentItemIds)
{
    @await OrchardCore.GetTaxonomyTermAsync(Model.TaxonomyContentItemId, termId);
}
dodyg commented 5 years ago

It will be much user friendly if ContentItemId is visible in the UI.

It can be much smaller in contrast of the other UI element. Right now we have to fish it out of the url.

content-item-id

LatinaAtanasova commented 5 years ago

Hello, I'm working on similar website using decoupled CMS. I also came across with missing information on this part. There is a very nice video with Sebastien but it's just one :). Now I'm facing problems with contact form and sending e-mail. If i make it as widget, how will i insert it in my ready html. On the other side, when i start to write my logic in back-end, I should struggle with workflows and their usage. It will be nice if you share some experience :)

dodyg commented 5 years ago

@LatinaAtanasova I have not started to touch the form and sending email although that's coming. I will let you know when I figure it out. As you say, it's not very obvious.

dodyg commented 5 years ago

I tried precompilation but orchard core does not pick up the Shape templates that was located at Views folder. Now I have to copy the folder manually over.

sebastienros commented 5 years ago

If you are using the .Web sdk then it's automatic. All our module work like this.

LatinaAtanasova commented 5 years ago

@dodyg so far i managed to use

functionality but i had to implement theme. I couldn't get without it. I used "The Theme" where i modified html according to my html just for that part. I kept all other html in Pages in Web Project. As to sending e-mails from contact form. I did it but with work around - wrote my own service and take smtp and recaptcha credentials from administration . I'm afraid that this is not the idea and i don't like it that way but so far...

dodyg commented 5 years ago

@LatinaAtanasova yeah I don't know how to render the form yet on decoupled CMS.

dodyg commented 5 years ago

@sebastienros I already use .Web SDK.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Markdig" Version="0.16.0" />
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.3" />
    <PackageReference Include="OrchardCore.Application.Cms.Targets" Version="1.0.0-beta3-71077" />
  </ItemGroup>

  <ItemGroup>
    <Content Update="wwwroot\css\site.css">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

</Project>
LatinaAtanasova commented 5 years ago

me too :)

jtkech commented 5 years ago

Yes, at the app level that's ok for pure mvc views but shape views need to be outputed on publishing.

This is because the shape discovering relies on the existing file providers, a shape view needs to be discovered at runtime even the related precompiled view will be used.

It works for modules because we also embed view contents in their assemblies, so shape views are found through our embedded file provider.

Right now, you need to output app level shape views (razor and liquid) on publishing.

LatinaAtanasova commented 5 years ago

Hello again, I need help on my project :) I have a few pages:

Web

and in order to use menu functionality i included "The Theme" with restyling the menu html.

Theme

I try to understand and implement one simple widget. I created an alert message as follows:

Layer Messages Zones

My idea is to visualize that alert in zone name="Messages" which is in Contacts Page. (Next step will be to have it as success or warning, etc.) I try it with

<zone name="Messages">
          @await DisplayAsync(Model.Content);
</zone>

but DisplayAsync is not reachable. I know that it can be used if page inherits OrchardCore.DisplayManagement.Razor.RazorPage but than i get the following error "The name 'PageContext' does not exist in the current context" when trying to reach that page. If i remove "@page" at the start of "Contacts"page "await DisplayAsync" can be used, but page is than not reachable and i get "The page cannot be displayed". Obviously I am missing something. I am really new to Orchard but I want to understand and use it. My main question is what is the best approach to build a web side with ready html pages? I tried with replacing existing theme, but it was kind of mess :) I need to have contact page, user registrations, some items and mapping them to users. I'll appreciate every advice :)

dodyg commented 5 years ago

I need to have contact page, user registrations, some items and mapping them to users. I'll appreciate every advice :)

Right now my approach is to build the contact page form in razor as normal then submit the value to a workflow end point. I haven't completed the process yet. I will update when I am done.

Note: I just found out zone tag helper exists. I will research this further today and see if I can find a solution to the problem you encounter.

LatinaAtanasova commented 5 years ago

Thanks :) It will be great if you can share your experience with workflow. I also made one, but couldn't submit the value to it. I'm missing something with the transition between admin and back-end.

dodyg commented 5 years ago

Make sure you enable HTTP Workflows Activities module

dodyg commented 5 years ago

I screwed up a workflow and now my Content Item listing is broken. https://github.com/OrchardCMS/OrchardCore/issues/3593

dodyg commented 5 years ago

@LatinaAtanasova,

This is a good guide for workflow except that it's a bit out of date in the liquid part.

This contains the latest up to date reference on what is available for Liquid.

My workflow is this.

workflow-1

You need to create:

workflow-2

In my ContactUs form, I just serialize a json payload and post it to the URL set by the HTTP Request event.

contact-us

So now when I submit my contact us form, a new content will be added in OrchardCore.

LatinaAtanasova commented 5 years ago

Thank you. I'll try it. By the way the following solution is nice and useful. I don't know if you have come across it. https://github.com/psijkof/ModernBusiness.OC.RazorPages

dodyg commented 5 years ago

Good find. Let me review that.

dodyg commented 5 years ago

This is an important information regarding media resize https://github.com/OrchardCMS/OrchardCore/issues/3474#issuecomment-483791638.

You cannot resize image outside these values.

To override the default values, do this in your startup.cs

    MediaFileProvider.DefaultSizes = new[] { 16, 32, 50, 100, 160, 240, 480, 576, 600, 1024, 2048 };

The default values are very limiting because it doesn't allow you to do the ratio of still photography https://en.wikipedia.org/wiki/Aspect_ratio_%28image%29#Still_photography

dodyg commented 5 years ago

Lucene Queries seems to have problem with sorting https://github.com/OrchardCMS/OrchardCore/issues/3278. Bummer.

dodyg commented 5 years ago

Or use SupportedSizes configuration to provide custom supported resizing for the media module https://github.com/OrchardCMS/OrchardCore/commit/8da9513d011d62c272adeb917488c56efed9f950

sebastienros commented 5 years ago

We have an issue already and a design to support "any" size also.

juniordev19 commented 5 years ago

All you need is love and

private readonly OrchardCore.IOrchardHelper _helper;
private readonly OrchardCore.Queries.IQueryManager _queryManager;

or @inject them at your views.

These two are pretty much all you need to implement decoupled CMS AFAIK.

Could you give an example of how to use these objects to execute a query in the controller and send the result of the query to the view?

dodyg commented 5 years ago

This will get you going (Razor Page)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using OrchardCore.ContentManagement;

namespace Silverkey.Website.Pages
{
    public class GeneralPageModel : PageModel
    {
        private readonly OrchardCore.IOrchardHelper _helper;
        private readonly OrchardCore.Queries.IQueryManager _queryManager;

        public ContentItem PageContent { get; set; }

        public GeneralPageModel(OrchardCore.IOrchardHelper helper, OrchardCore.Queries.IQueryManager QueryManager)
        {
            _helper = helper;
            _queryManager = QueryManager;
        }

        public async Task<ActionResult> OnGetAsync(string alias)
        {
            if (string.IsNullOrEmpty(alias))
                return NotFound();

            var content = await _helper.GetContentItemByAliasAsync($"alias:{alias}");

            if (content is null)
                return NotFound();

            PageContent = content;

            return Page();
        }
    }
}

View

@page "/p/{*alias}"
@model Silverkey.Website.Pages.GeneralPageModel
@{
    ViewData[Fmt.Title] = Model.PageContent.DisplayText;
    bool sideSectionExists = Model.PageContent.ContentItem.Content.SideSections.ContentItems.Count > 0;

    var mainSectionSize = sideSectionExists ? "is-two-thirds" : Model.PageContent.ContentItem.Content.SinglePage.ColumnCssClass.Text;
}

@foreach (var pitch in Model.PageContent.ContentItem.Content.Pitch.ContentItems)
{
    <section class="hero is-fullheight-with-navbar">
        <div class="hero-body has-background-grey-lighter">
            <div class="container">
                <div class="columns is-centered">
                    <div class="column is-half" style="text-align:center;">
                        <h1 class="title is-1">@pitch.TitlePart.Title</h1>
                        <div class="content">
                            @Fmt.Markdown((string)pitch.MarkdownBodyPart.Markdown)
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
}

<main class="section">
    <div class="container">
        <div class="columns">
            <div class="column @mainSectionSize">
                <main>
                    <h1 class="title is-1">@Model.PageContent.ContentItem.Content.TitlePart.Title</h1>
                    <div class="content">
                        @Fmt.Markdown((string)Model.PageContent.ContentItem.Content.MarkdownBodyPart.Markdown)
                    </div>

                    <div class="content">
                        @foreach (var part in Model.PageContent.ContentItem.Content.MainSections.ContentItems)
                        {
                            <h2>@part.DisplayText</h2>
                            if (part.ContentType == "Columns")
                            {
                                <div class="columns">
                                    @foreach (var column in part.BagPart.ContentItems)
                                    {
                                        <div class="column">
                                            <h3>@column.DisplayText</h3>
                                            @Fmt.Markdown((string)column.MarkdownBodyPart.Markdown)
                                        </div>
                                    }
                                </div>
                            }
                        }
                    </div>
                </main>
            </div>
            @if (sideSectionExists)
            {
                <div class="column">
                    @foreach (var part in Model.PageContent.ContentItem.Content.SideSections.ContentItems)
                    {
                        <div class="card">
                            <div class="card-header">
                                <p class="card-header-title is-centered">
                                    @part.TitlePart.Title
                                </p>
                            </div>
                            <div class="card-content">
                                <div class="content">
                                    @Fmt.Markdown((string)part.MarkdownBodyPart.Markdown)
                                </div>
                            </div>
                        </div>
                    }
                </div>
            }
        </div>
    </div>
</main>

@foreach (var part in Model.PageContent.ContentItem.Content.BottomSections.ContentItems)
{
    <section class="section">
        <div class="container">
            <div class="columns">
                <div class="column">
                    <div class="content">
                        @part.TitlePart.Title
                        @Fmt.Markdown((string)part.MarkdownBodyPart.Markdown)
                    </div>
                </div>
                <div class="column">
                </div>
            </div>
        </div>
    </section>
}

Note @Fmt.Markdown is my own method.

If you want to see what properties are available for you to access, do @Model.PageContent.Content and it will show you all the structure in Json that you can access dynamically.

juniordev19 commented 5 years ago

Thanks for the detailed example. Do you know how to create a table managed from the admin panel?

dodyg commented 5 years ago

Use a text field with HTML Editor

dodyg commented 5 years ago

When you are implementing a module, at="Foot" is your friend. It puts the scripts at the bottom of the page.

<script at="Foot" src="https://unpkg.com/gijgo@1.9.11/js/gijgo.min.js" type="text/javascript"></script>
<script at="Foot">
    document.write('<!-- some script -->');
</script>
dodyg commented 5 years ago

Don't forget to put the Admin attribute for controller that handles your module admin pages

admin

dodyg commented 5 years ago

Encountered a deployment problem https://github.com/OrchardCMS/OrchardCore/issues/3755. The cause because the environment was set to Development.

dodyg commented 5 years ago

This is important https://github.com/OrchardCMS/OrchardCore/issues/3752

Check that each module using the razor sdk also references directly (not transitively) the Microsoft.AspNetCore.Mvc package, that modules using our custom script tag reference OrchardCore.ResourceManagement, and that their _ViewImports.cshtml import the related tag helpers @addTagHelper *, OrchardCore.ResourceManagement.