-
Notifications
You must be signed in to change notification settings - Fork 26
Working with a Headless approach
Puck is pretty flexible, you don't need to use razor views if you don't wan't to. You could instead return JSON of the current page or other queried content.
By default, there is a catch-all endpoint mapped to the HomeController
Index
action. The code for this mapping is in Startup.cs
:
endpoints.MapControllerRoute(
name: "default",
pattern: "{**path}"
,defaults: new { controller = "Home", action = "Index"}
);
HomeController
inherits from puck.core.Controllers.BaseController
and if you look at the Index
action:
public IActionResult Index()
{
return base.Puck();
}
all it does is call the inherited Puck action which will get the current page and render a view. if you wanted to go headless, you could just return the current page as JSON:
public IActionResult Index()
{
var currentPage = QueryHelper<BaseModel>.Current();
return Json(currentPage);
}
this is a simple example but in a real world scenario you'll likey want to use the QueryHelper to get multiple pieces of content and compose them into a wrapper class that contains your ViewModel and any other required data to return to the client. you'll also likely be calling Puck from another app, something like a single page app which contains your views or a mobile app and in this scenario you can set up your Puck project to allow cross origin requests. then your single page app hosted on a different domain can make requests to Puck and get the JSON responses.
the previous example showed you how to get Puck to return the requested page as JSON. what if you wanted to get back multiple results by search query? to set up a query endpoint, you need a controller which inherits from puck.core.Controllers.BaseController
- you could use HomeController
, for example. then you can create a Query
action:
[Route("Query")]
[HttpPost]
public IActionResult Query([FromBody]List<puck.core.Models.QueryModel> queries,string cacheKey=null,int cacheMinutes=0) {
return Json(base.Query(queries,cacheKey:cacheKey,cacheMinutes:cacheMinutes));
}
you can then post JSON requests to this endpoint (you will need to set the Content-Type
header on your request to application/json
). here's an example JSON request body:
[
{
"Type": "School",
"Query": "NodeName:\"Moordale School\"",
"Skip": 0,
"Take": 100,
"Sorts":"NodeName:desc,SortOrder:asc",
"Include":[
["People.Students","Friends"],
["GalleryImages"]
],
"Fields":["People","GalleryImages","NodeName"]
},
{
"Type": "Page",
"Query": "+Type:Page PuckGeoM:Location.LongLat,-0.1277582,51.5073509,10,asc",
"Skip": 0,
"Take": 100,
"Sorts":"",
"Include": []
}
]
the above request body specifies two searches. each search consists of a Type
- which is the type parameter that you would normally pass to QueryHelper. the Query
parameter is a Lucene query, read this for reference. use Skip
and Take
for pagination and Sorts
accepts a comma separated string of Field:asc
or Field:desc
, e.g. "SortOrder:desc,NodeName:asc". the "Fields" property allows you to specify which fields are returned, if not present, all fields will be returned.
the Include
argument allows you to include any referenced ViewModels from Content Picker or Image Picker fields. these are fields of type List<PuckReference>
. in the first query, you are specifying that for each School
ViewModel in the result, include the ViewModels referenced in the People.Students
property, which means that property will go from being a List<PuckReference>
to List<TViewModel>
- a list of referenced ViewModels.
since include is recursive, the query above also specifies a recursive include of Friends
, so for each Student
in the People.Students
list, their friends will be loaded too.
the next include is GalleryImages
and this will apply for each School
in the result, loading in any referenced images.
it's also worth noting that Include
also supports Lists, so if you have a list of a complex type like Person
with a Content Picker
property Friends
, you can Include like so; Include:[["People.Friends"]]
and it will actually loop through each person in the People
list property and load their friends in.
this type of querying is very powerful but beware that it can be inefficient to pre-emptively include many-levels-deep worth of ViewModels. you may prefer to lazily load in referenced ViewModels by making separate requests to the Query
action when those ViewModels are required. you may also want to specify CacheKey
and CacheMinutes
arguments when calling the Query
action so that results can be cached.
the equivalent of QueryHelper's ExplicitType
is specifying the type, like so; "+Type:Page".
the syntax for Geo/Spatial
queries is PuckGeoM:Location.LongLat,-0.1277582,51.5073509,10,asc
when the distance is specified in Miles and PuckGeoK:Location.LongLat,-0.1277582,51.5073509,10,asc
when specified in kilometres. the parameters are passed in comma separated with no spaces, the parameters being the Field Name
,Longitude
,Latitude
,Distance
and finally the Sort
. specify asc
,desc
or null
for Sort
values.
the results for these two queries will be returned in the format List<QueryResult>
where each QueryResult
contains a Results
property which is a list of ViewModels and a Total
property, which is the total hits - this is useful for pagination.
one of the handy features of the QueryHelper is that you can search for ViewModels that implement a particular interface or multiple interfaces. you can do this kind of search with your query endpoint by specifying the Implements
property:
[
{
"Type": "BaseModel",
"Query": "+Title:News",
"Skip": 0,
"Take": 100,
"Implements":"IMainContent,IGalleryImages",
"Sorts":"SortOrder:asc",
"Include":[]
}
]
above, you are specifying that you want ViewModels which implement the IMainContent
and IGalleryImages
interfaces. for Puck to be able to work with your interfaces, you need your interfaces to inherit from the base interface puck.core.Abstract.I_BaseModel
.
[
{
"Type": "ImageVM",
"Query": "",
"Skip": 0,
"Take": 100,
"Implements":"",
"Sorts":"",
"Include":[],
"Similar":"84f9d72e-63a1-43cb-b7c0-7b53cdf13e59,en-gb"
}
]
as mentioned earlier, it's simple enough to get Puck to return the current page ViewModel as JSON. what will likely happen though, is that you will want to return related/additional data with your ViewModel. for example, you may want to return all CarouselImages
with the Homepage
ViewModel to display a carousel on your homepage. with the graphql style Query Endpoint
detailed in the previous section, it would be simple enough to Include
the CarouselImages
property on the Homepage
ViewModel to make sure all carousel image ViewModels linked to your Homepage
are returned with it.
thing is, with the Query Endpoint
, you have to know the Type
you're querying and which properties to include upfront, this isn't always realistic. assuming we're working with the initial pattern shown on this page where Puck returns the current page as JSON, what would be ideal, is that if every ViewModel knew what additional data it needed and could return a Wrapped
version of itself, containing itself (the ViewModel, i.e the Homepage
) and any additional required data. fortunately, with a little polymorphism, this is easy to achieve.
the first thing you need to do, is in your ViewModels
folder, create a BasePage
which all of your ViewModels will inherit from.
public class BasePage:puck.core.Base.BaseModel
{
[Display(Name ="Meta Title",GroupName ="Meta")]
public string MetaTitle { get; set; }
[Display(Name = "Meta Description", GroupName = "Meta")]
[UIHint(EditorTemplates.TextArea)]
public string MetaDescription { get; set; }
[Display(Name = "Page Title", GroupName = "Content")]
public string PageTitle { get; set; }
public virtual BasePageWrapper GetWrappedViewModel() {
return new BasePageWrapper { ViewModel = this };
}
}
as you can see above, the BasePage
inherits from Puck's BaseModel
and has some Meta
properties that all web pages should have as well as the PageTitle
. you may have additional requirements which you can add. importantly, notice that it has a virtual
method GetWrappedViewModel
which returns a BasePageWrapper
(which has a single property, the current ViewModel). the idea is that, for every ViewModel you create which inherits from this BasePage
, you override
the GetWrappedViewModel
method and return a Wrapper
model based on that particular ViewModel. here's an example of Homepage
using this pattern:
public class Homepage:BasePage
{
[Display(GroupName = "Content")]
[UIHint(EditorTemplates.RichText)]
public string Bio { get; set; }
[Display(Name ="Profile Picture",GroupName ="Content")]
[UIHint(EditorTemplates.ImagePicker)]
public List<PuckReference> ProfilePicture { get; set; }
[Display(GroupName="Content")]
[UIHint(EditorTemplates.ListEditor)]
public List<SocialLink> Links { get; set; }
public override BasePageWrapper GetWrappedViewModel()
{
var model = new HomepageWrapper { ViewModel = this};
if (ProfilePicture != null && ProfilePicture.Any()) {
var picture = ProfilePicture.GetAll<ImageVM>()?.FirstOrDefault();
if (picture != null) {
model.ProfilePicture = picture;
}
}
return model;
}
}
above is the homepage ViewModel, notice how it inherits from BasePage
. the interesting part is where it overrides
GetWrappedViewModel
. notice that the model being returned by this method is not a BasePageWrapper
as stated in the method signature but actually a HomepageWrapper
- which, crucially, inherits from BasePageWrapper
so can be boxed to that type and has an additional property, ProfilePicture
which is not on the BasePageWrapper
. the idea is that you have a different wrapper type for each type of ViewModel which inherits from BasePage
but has properties related to that specific ViewModel. you can put your Wrapper
models in a Models
folder (not your ViewModels
folder - you may have to create this Models
folder).
now in the HomeController
, your index
action looks like this:
public IActionResult Index()
{
var viewModel = puck.core.Helpers.QueryHelper<BasePage>.Current();
var wrapped = viewModel?.GetWrappedViewModel();
return Json(wrapped);
}
notice above that you query for the current page, cast to your BasePage
type and then call GetWrappedViewModel
on it. this will return the specific wrapper for whatever type of ViewModel the current page is. along with using the graphql style query endpoint from the previous section, this is a flexible and powerful approach for tackling most situations in headless websites.
note, to get this working, you must use JSON.net as your JSON serializer as the default one doesn't handle polymorphism. you will need to install the required package - Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson
.
you will then need to modify your ConfigureServices
method in your Startup.cs
file.
the following:
services.AddControllersWithViews()
.AddApplicationPart(typeof(puck.core.Controllers.BaseController).Assembly)
.AddControllersAsServices()
.AddRazorRuntimeCompilation()
.AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null);
needs to be replaced with this:
services.AddControllersWithViews()
.AddApplicationPart(typeof(puck.core.Controllers.BaseController).Assembly)
.AddControllersAsServices()
.AddRazorRuntimeCompilation()
.AddNewtonsoftJson(x => {
x.UseMemberCasing();
})
.AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null);