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
cmsfeature. Its key is the server'sApiKeysetting, set in the admin UI or asRelatudeDB:ApiKeyin 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).