Mulliman / xStatic-for-Umbraco

This is a static site generator built for Umbraco so that you can host simple Umbraco sites on fast and cheap hosting providers such as netlify.
42 stars 11 forks source link

Adding pagination to a page #28

Open royberris opened 9 months ago

royberris commented 9 months ago

Hi,

I have a question, maybe feature request but not sure how to tackle it with your plugin. My personal website uses xStatic and it's great. Thanks!

I have a blogs page and it's getting a bit long, so I want xStatic to render pages for as long as there are results.

Right now a page works with /blogs?page=1 and it's ok to make this /blogs/1 for example.

How would you tackle this?

Mulliman commented 9 months ago

Hey

This was something I was considering when trying to add support for the new content delivery API and also exporting the data from that statically. I struggled to come up with a solution that I liked, hence why on the back burner.

If I wanted to to add this to my site, I'd start by approaching this from the client side. The quickest way to do this would be to list all the pages but only show the first n, and add a button that shows the next n until all are shown. You could update the URL when the button is pressed to to keep some sort of history state.

This is fine until you really do start having a lot of articles and sending all the data over to the client in the initial request is bad.

If I wanted to handle this in xStatic, I'd probably look at overriding the Generator to also call the page with a query string, and if it notices a difference to store that with a different file name. You'd then need to create a transformer to replace the URLs with the querystring to the path of the file.

Ideally I'd need to find a nice way to do this built in to xStatic, but I don't have a plan on how to do it yet.

royberris commented 9 months ago

Hi,

Yeah it's a hard problem. But thinking out loud here:

Best way would probably be to add some sort of context to the document (from a content app) where we can verify by x. Where x might be a page number but it could also be a page theme, or any other use case. Or maybe as a setting for a document type.

I'm at cross roads if this problem should be solved on document type level, or on content level. Usually when I create a page with pagination it is there for all paged. But you'd need to context of the content itself to be able to determine of the variation is there.

Maybe problem is solved by allowing us to hook in to the generator process for a specific document type. See below pseudo code as an example of what I mean. Where we can push multiple versions of a page to the output. I'm not sure how xStatic works under the hood, so doing a guess here 😆

public class OverviewPageVariation : StaticVariationGenerator<OverviewPageModelsBuilderClass>
{
  public void Generate(OverviewPageModelsBuilderClass model, ...)
  {
     var pageSize = 10; // or model.PageSize;
     var childrenCount = model.Children().Count;
     var pages = childrenCount / pageSize;

     foreach (var page in pages)
     {
       var generatedResult = VariateOn(variation: $"?page={page}", outputSuffic: $"/{page}/index.html");
     }
  }
}

I guess VariateOn would just call the (default) controller for this document but with the provided variation? Which might also just be an anonymous object. new { page }. And public ActionResult OverviewPage(OverviewPageModelsBuilderClass model, int page = 1) understand this.

Hard one indeed.

Mulliman commented 9 months ago

I can't do this nicely without introducing breaking changes, but considering that this is a valid case (and because variants would also be good for multilingual sites) it may be something I put in the next major version.

However, I can get something along the lines of what you need by doing this

public class NodeInfo
{
    public int Id { get; set; }

    public int StaticSiteId { get; set; }

    public IFileNameGenerator FileNamer { get; set; }

    public IEnumerable<ITransformer> Transformers { get; set; }

    public string Url { get; set; }

    public string AbsoluteUrl { get; set; }

    public IStaticSiteStorer Storer { get; set; }

    public IPublishedContent Node { get; set; }
}

public abstract class VariantHtmlSiteGeneratorBase : StaticHtmlSiteGenerator
{
    protected VariantHtmlSiteGeneratorBase(IUmbracoContextFactory umbracoContextFactory,
        IPublishedUrlProvider publishedUrlProvider,
        IStaticSiteStorer storer,
        IImageCropNameGenerator imageCropNameGenerator,
        MediaFileManager mediaFileSystem,
        IWebHostEnvironment hostingEnvironment)
        : base(umbracoContextFactory, publishedUrlProvider, storer, imageCropNameGenerator, mediaFileSystem, hostingEnvironment)
    {
    }

    public abstract List<Func<NodeInfo, Task<GenerateItemResult>>> VariantProcessors { get; }

    public async override Task<GenerateItemResult> GeneratePage(int id, int staticSiteId, IFileNameGenerator fileNamer, IEnumerable<ITransformer> transformers = null)
    {
        SslTruster.TrustSslIfAppSettingConfigured();

        var node = GetNode(id);

        if (node == null)
        {
            return null;
        }

        try
        {
            var url = node.Url(_publishedUrlProvider, mode: UrlMode.Relative);
            string absoluteUrl = node.Url(_publishedUrlProvider, mode: UrlMode.Absolute);

            var fileData = await GetFileDataFromWebClient(absoluteUrl);
            var transformedData = RunTransformers(fileData, transformers);
            var filePath = fileNamer.GetFilePartialPath(url);
            var generatedFileLocation = await Store(staticSiteId, filePath, transformedData);

            await ProcessVariants(id, node, staticSiteId, fileNamer, transformers, url, absoluteUrl);

            return GenerateItemResult.Success("Page", node.UrlSegment, generatedFileLocation);
        }
        catch (Exception e)
        {
            return GenerateItemResult.Error("Page", node.UrlSegment, e.Message);
        }
    }

    protected async Task ProcessVariants(int id,
        IPublishedContent node,
        int staticSiteId,
        IFileNameGenerator fileNamer,
        IEnumerable<ITransformer> transformers,
        string url,
        string absoluteUrl)
    {
        if (VariantProcessors?.Any() == true)
        {
            var nodeInfo = new NodeInfo
            {
                Id = id,
                Node = node,
                StaticSiteId = staticSiteId,
                FileNamer = fileNamer,
                Transformers = transformers,
                Url = url,
                AbsoluteUrl = absoluteUrl,
                Storer = _storer
            };

            foreach (var variant in VariantProcessors)
            {
                await variant.Invoke(nodeInfo);
            }
        }
    }
}

And then creating an implementation like the following

public class CustomSiteGenerator : VariantHtmlSiteGeneratorBase
{
    public CustomSiteGenerator(IUmbracoContextFactory umbracoContextFactory,
                   IPublishedUrlProvider publishedUrlProvider,
                              IStaticSiteStorer storer,
                                         IImageCropNameGenerator imageCropNameGenerator,
                                                    MediaFileManager mediaFileSystem,
                                                               IWebHostEnvironment hostingEnvironment)
        : base(umbracoContextFactory, publishedUrlProvider, storer, imageCropNameGenerator, mediaFileSystem, hostingEnvironment)
    {
    }

    public Func<NodeInfo, Task<GenerateItemResult>> PaginationVariantProcessor => async (nodeInfo) =>
    {
        var pageSize = 2;
        var childrenCount = nodeInfo.Node.Children.ToList().Count;
        var pages = childrenCount / pageSize;

        for (int page = 0; page < pages; page++)
        {
            var urlToFetch = $"{nodeInfo.AbsoluteUrl}?page={page}";
            var fileData = await GetFileDataFromWebClient(urlToFetch);

            var transformedData = RunTransformers(fileData, nodeInfo.Transformers);

            var filePath = nodeInfo.FileNamer.GetFilePartialPath(nodeInfo.Url);
            filePath = filePath.Replace("index.html", $"/{page}/index.html");

            var generatedFileLocation = await Store(nodeInfo.StaticSiteId, filePath, transformedData);
        }

        return GenerateItemResult.Success("PageVariant", nodeInfo.Node.UrlSegment, pages + " Variant Pages");
    };

    public override List<Func<NodeInfo, Task<GenerateItemResult>>> VariantProcessors => new List<Func<NodeInfo, Task<GenerateItemResult>>> {
        PaginationVariantProcessor
    };
}

You then need to register this

_services.AddSingleton<CustomSiteGenerator>();

and then create a new export type in the xstatic section of Umbraco and choose that as your generator.

With this in place I am getting multiple files created (I just don't have a paginated example to test with).

You'd need to create a matching transformer to handle rewriting links to use the new URL format, but other than that this should be workable until I can refactor this from the ground up.