abp icon indicating copy to clipboard operation
abp copied to clipboard

CmsKit: Meta information for SEO

Open nebula2 opened this issue 2 years ago • 6 comments
trafficstars

Is there an existing issue for this?

  • [X] I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

I think it would be a good addition if blog posts and pages get the ability of adding some meta information to be more SEO friendly.

For example:

Description

Take short description and put that into <meta name="description" content="YOURSHORTDESCRIPTION" />

I think a configuration would be good to control whether to apply the logic or not. Maybe the user has something else in place.

Keywords

Take tags and put that into <meta name="keywords" content="YOURTAG1,YOURTAG2" />

I think a configuration would be good to control whether to apply the logic or not. Maybe the user has something else in place.

Title

Take the Title and set ViewBag.Title to that

I think this would need something additional. Maybe you want to have something like "CompanyName - " + EntityTitle

Describe the solution you'd like

The maybe-overkill solution

There would be multiple ways of archiving this. Here's just an idea here to get discussion going.

Add a new page as a submenu item of CMS for meta information. The new page prodives CRUD functionality for setting meta stuff like described above. The entity could look something like this:

  • string input -> EntityType (set if you want to apply logic for a specific entity type. leave empty to apply to all)
  • dropdown input -> MetaAttribute enum info (Title, Description, Keywords, CustomMetaProperty) where the first three are as described above
  • string input -> if CustomMetaProperty, you can set things like og:title if you want to support Open Graph stuff
  • dropdown input -> dropdown for supported properties to set + the option SetManually

If SetManually is set -> You need to manually define the attribute when creating/ editing the specified type.

Related components would need to respect what's been set here.

Additional context

No response

nebula2 avatar Apr 22 '23 13:04 nebula2

This is a very important functionality for professional blogposts. SEO is almost make or brake for sites so this is very important.

Is there a workaround @EngincanV?

sturlath avatar Mar 14 '24 07:03 sturlath

This is a very important functionality for professional blogposts. SEO is almost make or brake for sites so this is very important.

Is there a workaround @EngincanV?

I agree that SEO is absolutely important, especially in such content (blogposts). There are some workaround that you might use but they are not effective and are cumbersome, to be honest, at least the ways I thought.

I think we can prioritize this issue. I'll talk with the team, and we'll evaluate this.

EngincanV avatar Mar 14 '24 07:03 EngincanV

OK we'll plan to implement this feature

ebicoglu avatar Mar 14 '24 10:03 ebicoglu

Any Update on this

freebsensetips avatar Jul 01 '24 11:07 freebsensetips

Any Update on this

We haven't prioritized it yet.

EngincanV avatar Jul 01 '24 12:07 EngincanV

To further formulate this issue so that a developer has more info on this:

some things that would make sense to define every time:

  1. If the page/ article should be indexed by search engines or not
  2. Meta title aka. ViewBag.Title
  3. Meta description aka ViewBag.MetaDescription

optional but strongly emphasized:

Apart from that, something like this could make sense: New CRUD entity with

  • string MetaName
  • bool IsRequired
  • string content

which is then just displayed in a new tab when creating/ updating a page/ blog post

packages that may be of interest here: https://github.com/PureKrome/SimpleSitemap

some pretty retarted example but here you go (having a controller in out Web.Public)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ConiDerp.Cms;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using SimpleSiteMap;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Timing;
using Volo.CmsKit.Public.Blogs;

namespace ConiDerp.Web.Public.Controllers;

public class SitemapController : Controller
{
    private const string _defaultBlogSlug = "default";

    private readonly string _siteUrl;

    private readonly IConfiguration _configuration;
    private readonly IBlogPostPublicAppService _blogPostPublicAppService;
    private readonly IConiCmsHelperAppService _coniCmsHelperAppService;
    private readonly IClock _clock;

    public SitemapController(
        IBlogPostPublicAppService blogPostPublicAppService,
        IConfiguration configuration,
        IClock clock,
        IConiCmsHelperAppService coniCmsHelperAppService)
    {
        _blogPostPublicAppService = blogPostPublicAppService;
        _configuration = configuration;
        _siteUrl = _configuration["App:SelfUrl"].EnsureEndsWith('/');
        _clock = clock;
        _coniCmsHelperAppService = coniCmsHelperAppService;
    }

    /// <summary>
    /// simplistic sitemap just to have something to start with
    /// <para/>
    /// for more info see <see href="https://programmingcsharp.com/tips-asp-net-seo/#Headers_Meta_tags_and_other_HTML_tags">here</see> or <see href="https://github.com/PureKrome/SimpleSitemap/wiki/sitemap-index-example">here</see>
    /// </summary>
    /// <returns></returns>
    [Route("/sitemap.xml")]
    public async Task<IActionResult> Sitemap()
    {
        List<SitemapNode> nodes =
        [
            new SitemapNode(GetStaticSiteUrl(string.Empty), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("contact-us"), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("imprint"), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("privacy-policy"), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("privacy-policy-de"), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("software-consulting"), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("software-development"), DateTime.Now),
            new SitemapNode(GetStaticSiteUrl("software-extension"), DateTime.Now),
            .. await GetBlogPostsAsync(),
            .. await GetPagesAsync(),
        ];

        var sitemapService = new SitemapService();

        var xml = sitemapService.ConvertToXmlUrlset(nodes);

        return Content(xml, "application/xml");
    }

    private Uri GetStaticSiteUrl(string site)
    {
        return new Uri($"{_siteUrl}{site}");
    }

    private Uri GetBlogPostUrl(string blogPostSlug)
    {
        // sample: https://mysite.com/blogs/default/hello-world
        return new Uri($"{_siteUrl}blogs/{_defaultBlogSlug}/{blogPostSlug}");
    }

    private Uri GetPageUrl(string pageSlug)
    {
        // sample: https://mysite.com/hello-world
        return new Uri($"{_siteUrl}{pageSlug}");
    }

    private async Task<List<SitemapNode>> GetBlogPostsAsync()
    {
        try
        {
            BlogPostGetListInput input = new()
            {
                MaxResultCount = LimitedResultRequestDto.MaxMaxResultCount,
                SkipCount = 0,
                AuthorId = null,
                TagId = null,
                Sorting = "LastModificationTime DESC",
            };

            var blogPosts = await _blogPostPublicAppService.GetListAsync(_defaultBlogSlug, input);

            List<SitemapNode> nodes = blogPosts.Items.Select(p => new SitemapNode(
                url: GetBlogPostUrl(p.Slug),
                lastModified: p.LastModificationTime ?? _clock.Now)
            ).ToList();

            return nodes;
        }
        catch
        {
            return [];
        }
    }

    private async Task<List<SitemapNode>> GetPagesAsync()
    {
        try
        {
            PagedAndSortedResultRequestDto input = new()
            {
                MaxResultCount = LimitedResultRequestDto.MaxMaxResultCount,
                SkipCount = 0,
                Sorting = "LastModificationTime DESC",
            };

            var blogPosts = await _coniCmsHelperAppService.GetPagesAsync(input);

            List<SitemapNode> nodes = blogPosts.Items.Select(p => new SitemapNode(
                url: GetPageUrl(p.Slug),
                lastModified: p.LastModificationTime ?? _clock.Now)
            ).ToList();

            return nodes;
        }
        catch
        {
            return [];
        }
    }
}

https://github.com/karl-sjogren/robots-txt-middleware

sample usage in *WebPublicModule:

private static void ConfigureRobotsTxtCore(ServiceConfigurationContext context, IConfiguration configuration)
    {
        // https://programmingcsharp.com/configure-robots-txt-in-asp-net-core/
        context.Services.AddStaticRobotsTxt(b =>
        {
            b
            .AddSection(section => section
                .AddUserAgent("Googlebot")
                .Allow("/"))
            .AddSection(section => section
                .AddUserAgent("Bingbot")
                .Allow("/"))

            .AddSitemap($"{configuration["App:SelfUrl"].EnsureEndsWith('/')}sitemap.xml");

            return b;
        });
    }

The goal is to have something like this - but dynamically generated. image

sample of how something like this looks in piranha.core:

image

image

image

image

nebula2 avatar Sep 11 '24 15:09 nebula2