Relatude Help

23 Every feature in one Program.cs

Every option this manual describes, in one host, set the way a real site would set it. Each group of lines names the chapter that explains it. No site needs all of it: start from the smallest setup in chapter 2.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http.HttpResults;      // RazorComponentResult
using Microsoft.AspNetCore.Mvc;                   // [FromForm]
using MySite;                                     // the site's own classes, listed below
using MySite.Models;
using Relatude.CMS.Models;
using Relatude.CMS.Models.Donations;
using Relatude.CMS.Settings;
using Relatude.DB.CMS.Web;
using Relatude.DB.CMS.Web.Rendering;
using Relatude.DB.Datamodels;                     // AddNamespace, Add
using Relatude.DB.Nodes;                          // NodeStore
using Relatude.DB.NodeServer;                     // IStoreInitializer
using Relatude.DB.Web;                            // DefaultUrlManager

var builder = WebApplication.CreateBuilder(args);

// ---- the database (chapter 2) --------------------------------------------------------------
builder.AddRelatudeDB(o => {
    o.DefaultDataFolderPath = "data";                   // relatude.data/ and relatude.settings/ in ./data
    o.OnDatamodelInit = (dm, _) => {
        dm.AddCmsContentModel();                        // IItem, IPage, RTree, ITemplate and its kinds
        dm.AddNamespace<IArticle>();                    // the site's own types, IJsonTemplate included
        dm.Add<ICMSUser>();                             // members who sign in (chapter 2)
        dm.Add<ICMSUserGroup>();
        dm.AddNamespace<IDonation>();                   // donations (chapter 16)
    };
    o.OnStoreInit = db => db.RegisterTransactionPlugin(new PageAddressPlugin());   // addresses from names (chapter 6)
    o.OnStoreOpen = SiteContent.SeedIfEmpty;            // first content, before the modules' seeds (chapter 18)

    // ---- URLs (chapter 6) ------------------------------------------------------------------
    o.CreateUrlManager = _ => new DefaultUrlManager(new DefaultUrlManagerOptions {
        Parents = [new UrlParentRelation { ParentRelationName = nameof(RTree) }],   // the page tree: /news/hello-world
        Domains = [
            new UrlDomain { Host = "www.mysite.com", RootId = SiteContent.EnglishHome },   // a site per host,
            new UrlDomain { Host = "www.mysite.no", RootId = SiteContent.NorwegianHome },  // its home page at "/"
        ],
        FallbackRootId = SiteContent.EnglishHome,       // localhost and any other host
        UrlFormat = NodeUrlFormat.OnlyAddress,          // the default: no id in the URL
        IncludeTrailingSlash = false,                   // the default: /news, not /news/
        BaseAddressAssets = "https://cdn.mysite.com",   // file URLs through a CDN
        AssetUrlSignatureKey = builder.Configuration.GetValue<Guid>("Site:AssetUrlKey"),   // edited file URLs fail
    });
});

// a plugin registered the way a library registers one (chapter 2)
builder.Services.AddSingleton<IStoreInitializer, AuditInitializer>();

// ---- the CMS (chapters 2, 8 to 11, 17 and 18) ----------------------------------------------
builder.Services.AddRazorComponents();                  // for the Blazor campaign pages below
builder.AddRelatudeCMS(o => {                           // how a request is read (chapter 2)
        o.CultureCode = cms => cms.Http.Request.Host.Host.EndsWith(".no") ? "nb-NO" : "en-GB";
        o.LoginRedirect = true;                         // the default: content the visitor may not read asks for a login
    })
    .AddMvcRendering().AddRazorPagesRendering()         // MVC and Razor Pages
    .AddApi(o => o.VerifyLogin = (login, cms) =>        // the CMS API and interface (chapter 17)
        Task.FromResult(CheckPassword(cms.Admin, login.UserName, login.Password)?.Id))
    .AddModule<VenuesModule>()                          // modules (chapter 18)
    .AddModule(new EventsModule { ParentPageId = SiteContent.EnglishHome })
    .AddRendering(o => o
        // selectors decide first, for every page (chapter 8)
        .Select((http, m) => http.Request.Query["format"] == "json"
            ? new ResultTarget((_, c) => Results.Json(new { c.Content.Id, c.Content.Name, culture = c.CultureCode }))
            : null)
        .Select((http, m) => http.Request.Query["view"] == "print" && m.Content is IArticle ? new RazorViewTarget("ArticlePrint") : null)

        // a selector that checks a template's name overrides that template (chapter 8)
        .Select((http, m) => m.Template?.Name == "Campaign"
            ? new ResultTarget((_, c) => new RazorComponentResult<CampaignPage>(new { Content = c.Content }))
            : null)
        .Select((http, m) => m.Template?.Name == "Article" && m.Content is IArticle { Body.Length: > 20_000 } ? new RazorViewTarget("ArticleLong") : null)

        // a template kind of the site's own, which editors create in the admin UI (chapter 8)
        .TemplateKind<IJsonTemplate>(t => new ResultTarget((http, m) => Results.Json(new { m.Content.Name, fields = t.Fields })))

        // code defaults, for content without a template: the closest type wins (chapter 8)
        .RenderRazorView<IHomePage>("Home")                 // Views/Shared/Home.cshtml
        .RenderRazorView<IArticle>()                        // the type's name: Views/Shared/Article.cshtml
        .RenderMvcController<ISectionPage>("Section")       // SectionController.Index
        .RenderRazorView<IProduct>(p => p.InStock ? "Product" : "SoldOut")   // chosen per page
        .RenderRazorPage<ILandingPage>("Landing")           // Pages/Landing.cshtml
        .RenderResult<IFeedPage>((http, feed) => Results.Text(FeedWriter.Rss(feed), "application/rss+xml"))
        .RenderRazorView<IPage>("Default"));                // any other page; or .ViewPerType()

// ---- the site's own services (chapters 2 and 7) --------------------------------------------
builder.Services.AddSingleton<ContentResolver, PreviewContentResolver>();   // ?preview=<id> for editors
builder.Services.AddScoped<LegacyRouting>();                                // URLs another system answers
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(o => o.LoginPath = "/login");                                // a page with the sign-in form
builder.Services.AddAuthorization();

var app = builder.Build();

// ---- the pipeline (chapters 2 and 7) -------------------------------------------------------
app.UseRelatudeCMS();                                   // the database, its admin UI, analytics
app.UseStaticFiles();                                   // wwwroot, before routing
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();                                   // forms in Razor components, and the sign-in form

app.MapGet("/api/status", () => "ok");                  // explicit routes win over content
app.MapGet("/api/analytics", (RelatudeCmsContext cms) =>   // the CMS settings (chapter 13)
    cms.DB.Settings.Get<CmsSettings>().Analytics.Enabled ? "on" : "off");
app.MapPost("/login", async (RelatudeCmsContext cms, [FromForm] string email, [FromForm] string password) => {
    if (CheckPassword(cms.Admin, email, password) is not { } user) return Results.Unauthorized();
    await cms.Http.SignInAsync(CmsPrincipal.For(user, CookieAuthenticationDefaults.AuthenticationScheme));
    return Results.Redirect("/");                       // members sign in (chapter 2)
});
app.MapRelatudeCmsApi();                                // the CMS API at /relatude.cms/api (chapter 17)
app.MapRelatudeCmsUI();                                 // the CMS interface at /relatude.cms/
app.MapRelatudeCMS();                                   // pages, stored files and the modules' routes
app.MapDynamicControllerRoute<LegacyRouting>("{**url}");   // what the database does not address (chapter 7)
app.MapFallback(() => Results.NotFound());              // what nothing answers

app.Run();

// the site's password check: the member with this email, if the password is right (chapter 4)
static ICMSUser? CheckPassword(NodeStore admin, string? email, string? password) {
    if (string.IsNullOrWhiteSpace(email) || string.IsNullOrEmpty(password)) return null;
    var key = email.Trim().ToLowerInvariant();          // emails are stored in lower case
    var user = admin.Query<ICMSUser>().Where(u => u.Email == key).Execute().FirstOrDefault();
    return user != null && user.Password.Verify(password) ? user : null;
}

The site's own classes it names:

Class What it is Chapter
IHomePage, ISectionPage, IArticle, IProduct, ILandingPage, IFeedPage the page types, in MySite.Models 3
IJsonTemplate a template kind of the site's own: an ITemplate with a Fields property 8
SiteContent the fixed ids of the two home pages, and the content made on the first start 6
PageAddressPlugin gives every new page an address from its name 6
AuditInitializer an IStoreInitializer that registers a plugin of its own 2
VenuesModule, EventsModule two modules: venues, and events at them, under a page the host names 18
PreviewContentResolver a ContentResolver that shows unpublished pages to editors 7
LegacyRouting a route transformer for the URLs another system answers 7
CampaignPage, FeedWriter a Razor component, and a class that writes RSS 11
SectionController, the views in Views/Shared/, Pages/Landing.cshtml what the code defaults and selectors name 9, 10

What is set outside Program.cs:

  • Analytics is turned on in the admin UI, on the database's CMS tab (chapter 14).
  • Templates are made by editors in the admin UI, and a page's template is a field on the page (chapter 5).
  • Read access is set in the admin UI, by group, on pages, types or the whole store. The store applies it to every read as the visitor (chapter 2).
  • The license. The CMS interface needs a license with the cms feature. Its key is the server's ApiKey setting, set in the admin UI or as RelatudeDB:ApiKey in appsettings (chapter 17).
  • File URLs come from GetUrl(file) in the views (chapter 7).
  • The startup check logs problems with templates and code defaults when the store opens (chapter 8).