Nnews guide
Claude AI Skills repository
npx -y skills add landim32/awesome-ai-skills --skill nnews-guideAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guides how to integrate the NNews NuGet package for consuming the NNews CMS API in a .NET 8 project. Use when the user wants to consume articles, categories, tags, images, or AI-powered content generation from the NNews API.
SKILL.md
22.2 KB, as published. Nobody here has run it
NNews NuGet Package Integration Guide
You are an expert assistant that helps developers integrate the NNews NuGet package for consuming the NNews CMS API in .NET 8 projects.
Input
The user may provide a specific question or context as argument: $ARGUMENTS
If no argument is provided, present a complete overview of the NNews integration.
When the user asks about NNews, use this knowledge base to provide accurate, contextual guidance.
NNews — Data Transfer Objects
Install: dotnet add package NNews
NNewsSetting
public class NNewsSetting
{
public string ApiUrl { get; set; } = string.Empty; // NNews API base URL
}
ArticleInfo
public class ArticleInfo
{
public long ArticleId { get; set; }
public long CategoryId { get; set; }
public long? AuthorId { get; set; }
public string? ImageName { get; set; } // Max 560 chars
public string Title { get; set; } // Required, max 255 chars
public string Content { get; set; } // Required
public int Status { get; set; } // 0=Draft, 1=Published, 2=Archived, 3=Scheduled
public int ContentType { get; set; } = 2; // 1=PlainText, 2=Html, 3=MarkDown
public DateTime DateAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public CategoryInfo? Category { get; set; }
public List<TagInfo> Tags { get; set; } = new();
public List<RoleInfo> Roles { get; set; } = new();
}
ArticleInsertedInfo
public class ArticleInsertedInfo
{
public long CategoryId { get; set; } // Required
public long? AuthorId { get; set; }
public string? ImageName { get; set; } // Max 560 chars
public string Title { get; set; } // Required, max 255 chars
public string Content { get; set; } // Required
public int Status { get; set; } // 0=Draft, 1=Published, 2=Archived, 3=Scheduled
public int ContentType { get; set; } = 2; // Default: Html
public DateTime DateAt { get; set; } // Required
public string TagList { get; set; } = string.Empty; // Comma-separated tags
public List<string> Roles { get; set; } = new(); // Role slugs
}
ArticleUpdatedInfo
public class ArticleUpdatedInfo
{
public long ArticleId { get; set; }
public long CategoryId { get; set; }
public long? AuthorId { get; set; }
public string? ImageName { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int Status { get; set; }
public int ContentType { get; set; } = 2;
public DateTime DateAt { get; set; }
public string TagList { get; set; } = string.Empty;
public List<string> Roles { get; set; } = new();
}
CategoryInfo
public class CategoryInfo
{
public long CategoryId { get; set; }
public long? ParentId { get; set; } // For hierarchical categories
public string Title { get; set; } // Required, max 240 chars
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public int ArticleCount { get; set; } // Read-only counter
}
TagInfo
public class TagInfo
{
public long TagId { get; set; }
public string Title { get; set; } // Required, max 120 chars
public string? Slug { get; set; } // Max 120 chars
public int ArticleCount { get; set; } // Read-only counter
}
RoleInfo
public class RoleInfo
{
public string Slug { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
PagedResult<T>
public class PagedResult<T>
{
public IList<T> Items { get; set; } = new List<T>();
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public bool HasPrevious => Page > 1;
public bool HasNext => Page < TotalPages;
}
AI DTOs
// Request for AI content generation
public class AIArticleRequest
{
public long? ArticleId { get; set; }
public string Prompt { get; set; } // Required, 10-2000 chars
public bool GenerateImage { get; set; } = false;
}
// Response from AI generation
public class AIArticleResponse
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public long CategoryId { get; set; }
public string TagList { get; set; } = string.Empty;
public string? ImagePrompt { get; set; }
}
// Response from AI article update
public class AIArticleUpdateResponse
{
public long ArticleId { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public long CategoryId { get; set; }
public string TagList { get; set; } = string.Empty;
public string? ImagePrompt { get; set; }
}
// Category summary for AI context
public class AICategorySummary
{
public long CategoryId { get; set; }
public string Title { get; set; } = string.Empty;
public long? ParentId { get; set; }
}
NNews — Anti-Corruption Layer (ACL)
IArticleClient
public interface IArticleClient
{
Task<PagedResult<ArticleInfo>> GetAllAsync(long? categoryId = null, int? status = null, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);
Task<PagedResult<ArticleInfo>> ListByCategoryAsync(long categoryId, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);
Task<PagedResult<ArticleInfo>> ListByRolesAsync(int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);
Task<PagedResult<ArticleInfo>> ListByTagAsync(string tagSlug, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);
Task<PagedResult<ArticleInfo>> SearchAsync(string keyword, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default);
Task<ArticleInfo> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<ArticleInfo> CreateAsync(ArticleInsertedInfo article, CancellationToken cancellationToken = default);
Task<ArticleInfo> UpdateAsync(ArticleUpdatedInfo article, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
}
IArticleAIClient
public interface IArticleAIClient
{
Task<ArticleInfo> CreateWithAIAsync(string prompt, bool generateImage = false, CancellationToken cancellationToken = default);
Task<ArticleInfo> UpdateWithAIAsync(int articleId, string prompt, bool generateImage = false, CancellationToken cancellationToken = default);
}
ICategoryClient
public interface ICategoryClient
{
Task<IList<CategoryInfo>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IList<CategoryInfo>> ListByParentAsync(long? parentId = null, CancellationToken cancellationToken = default);
Task<CategoryInfo> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<CategoryInfo> CreateAsync(CategoryInfo category, CancellationToken cancellationToken = default);
Task<CategoryInfo> UpdateAsync(CategoryInfo category, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
}
ITagClient
public interface ITagClient
{
Task<IList<TagInfo>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IList<TagInfo>> ListByRolesAsync(CancellationToken cancellationToken = default);
Task<TagInfo> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<TagInfo> CreateAsync(TagInfo tag, CancellationToken cancellationToken = default);
Task<TagInfo> UpdateAsync(TagInfo tag, CancellationToken cancellationToken = default);
Task DeleteAsync(int id, CancellationToken cancellationToken = default);
Task MergeTagsAsync(long sourceTagId, long targetTagId, CancellationToken cancellationToken = default);
}
IImageClient
public interface IImageClient
{
Task<string> UploadImageAsync(IFormFile file, CancellationToken cancellationToken = default);
}
ITenantResolver
public interface ITenantResolver
{
string TenantId { get; }
string ConnectionString { get; }
string JwtSecret { get; }
}
NNews REST API Endpoints
All endpoints are prefixed by the controller name (e.g., /Article, /Category). Authenticated endpoints require a Bearer JWT token. Tenant is resolved via X-Tenant-Id header or JWT tenant_id claim.
Article (/Article)
| Method | Route | Query Params | Body | Auth | Response |
|---|---|---|---|---|---|
| GET | /Article | categoryId?, status?, page, pageSize | — | Yes | PagedResult<ArticleInfo> |
| GET | /Article/ListByCategory | categoryId, page, pageSize | — | No | PagedResult<ArticleInfo> |
| GET | /Article/ListByRoles | page, pageSize | — | No | PagedResult<ArticleInfo> |
| GET | /Article/ListByTag | tagSlug, page, pageSize | — | No | PagedResult<ArticleInfo> |
| GET | /Article/Search | keyword, page, pageSize | — | No | PagedResult<ArticleInfo> |
| GET | /Article/{id} | — | — | No | ArticleInfo |
| POST | /Article | — | ArticleInsertedInfo | Yes | ArticleInfo (201) |
| POST | /Article/insertWithAI | — | AIArticleRequest | Yes | ArticleInfo (201) |
| PUT | /Article | — | ArticleUpdatedInfo | Yes | ArticleInfo |
| PUT | /Article/updateWithAI | — | AIArticleRequest | Yes | ArticleInfo |
| DELETE | /Article/{id} | — | — | Yes | 204 No Content |
Status filter values: 0 = Draft, 1 = Published, 2 = Archived, 3 = Scheduled
Category (/Category)
| Method | Route | Query Params | Body | Auth | Response |
|---|---|---|---|---|---|
| GET | /Category | — | — | Yes | IList<CategoryInfo> |
| GET | /Category/listByParent | roles?, parentId? | — | No | IList<CategoryInfo> |
| GET | /Category/{id} | — | — | No | CategoryInfo |
| POST | /Category | — | CategoryInfo | Yes | CategoryInfo (201) |
| PUT | /Category | — | CategoryInfo | Yes | CategoryInfo |
| DELETE | /Category/{id} | — | — | Yes | 204 No Content |
Tag (/Tag)
| Method | Route | Query Params | Body | Auth | Response |
|---|---|---|---|---|---|
| GET | /Tag | — | — | Yes | IList<TagInfo> |
| GET | /Tag/ListByRoles | — | — | No | IList<TagInfo> |
| GET | /Tag/{id} | — | — | No | TagInfo |
| POST | /Tag | — | TagInfo | Yes | TagInfo (201) |
| PUT | /Tag | — | TagInfo | Yes | TagInfo |
| DELETE | /Tag/{id} | — | — | Yes | 204 No Content |
| POST | /Tag/merge/{sourceTagId}/{targetTagId} | — | — | Yes | 200 OK |
Image (/Image)
| Method | Route | Query Params | Body | Auth | Response |
|---|---|---|---|---|---|
| POST | /Image/uploadImage | — | IFormFile (multipart, max 100MB) | Yes | string (image URL) |
Multi-Tenancy
NNews supports multi-tenancy via the X-Tenant-Id HTTP header.
TenantHeaderHandler
A DelegatingHandler that automatically adds the X-Tenant-Id header to all outgoing HTTP requests. It reads the tenant ID from the configuration key Tenant:DefaultTenantId.
TenantResolver
Implements ITenantResolver. Reads tenant configuration from:
Tenant:DefaultTenantId— the default tenant IDTenants:{TenantId}:ConnectionString— database connection stringTenants:{TenantId}:JwtSecret— JWT secret for the tenant
Step-by-Step Integration
1. Install Package
dotnet add package NNews
2. Configure appsettings.json
{
"NNews": {
"ApiUrl": "http://localhost:5007"
},
"Tenant": {
"DefaultTenantId": "my-tenant"
}
}
Docker: use "ApiUrl": "http://nnews-api:80".
3. Register Services (DI)
using NNews.ACL;
using NNews.ACL.Interfaces;
using NNews.ACL.Handlers;
using NNews.ACL.Services;
using NNews.DTO.Settings;
// Settings
services.Configure<NNewsSetting>(configuration.GetSection("NNews"));
// Tenant resolver
services.AddScoped<ITenantResolver, TenantResolver>();
// Register TenantHeaderHandler for automatic X-Tenant-Id header
services.AddTransient<TenantHeaderHandler>();
// Register HttpClients with TenantHeaderHandler
services.AddHttpClient<IArticleClient, ArticleClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<IArticleAIClient, ArticleAIClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<ICategoryClient, CategoryClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<ITagClient, TagClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<IImageClient, ImageClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
4. Register Only What You Need
If you only need to read articles (e.g., a public blog frontend), register only the necessary clients:
services.Configure<NNewsSetting>(configuration.GetSection("NNews"));
services.AddTransient<TenantHeaderHandler>();
services.AddHttpClient<IArticleClient, ArticleClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<ICategoryClient, CategoryClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
services.AddHttpClient<ITagClient, TagClient>()
.AddHttpMessageHandler<TenantHeaderHandler>();
Usage Examples
List Articles (Paginated)
public class BlogService
{
private readonly IArticleClient _articleClient;
public BlogService(IArticleClient articleClient)
{
_articleClient = articleClient;
}
public async Task<PagedResult<ArticleInfo>> GetLatestArticles(int page = 1, int pageSize = 12)
{
return await _articleClient.GetAllAsync(page: page, pageSize: pageSize);
}
public async Task<PagedResult<ArticleInfo>> GetPublishedArticles(int page = 1, int pageSize = 12)
{
// Filter by status: 0=Draft, 1=Published, 2=Archived, 3=Scheduled
return await _articleClient.GetAllAsync(status: 1, page: page, pageSize: pageSize);
}
}
List Articles by Category
public async Task<PagedResult<ArticleInfo>> GetByCategory(long categoryId, int page = 1)
{
return await _articleClient.ListByCategoryAsync(categoryId, page: page, pageSize: 10);
}
List Articles by Tag
public async Task<PagedResult<ArticleInfo>> GetByTag(string tagSlug, int page = 1)
{
return await _articleClient.ListByTagAsync(tagSlug, page: page, pageSize: 10);
}
Search Articles
public async Task<PagedResult<ArticleInfo>> Search(string keyword, int page = 1)
{
return await _articleClient.SearchAsync(keyword, page: page, pageSize: 10);
}
Get Single Article
public async Task<ArticleInfo> GetArticle(int id)
{
return await _articleClient.GetByIdAsync(id);
}
Create Article
public async Task<ArticleInfo> CreateArticle()
{
var article = new ArticleInsertedInfo
{
Title = "My First Article",
Content = "<p>Hello World!</p>",
CategoryId = 1,
Status = 1, // Published
ContentType = 2, // Html
DateAt = DateTime.UtcNow,
TagList = "dotnet,csharp,webapi",
Roles = new List<string> { "admin", "editor" }
};
return await _articleClient.CreateAsync(article);
}
Update Article
public async Task<ArticleInfo> UpdateArticle(long articleId)
{
var article = new ArticleUpdatedInfo
{
ArticleId = articleId,
Title = "Updated Title",
Content = "<p>Updated content</p>",
CategoryId = 1,
Status = 1,
ContentType = 2,
DateAt = DateTime.UtcNow,
TagList = "dotnet,updated"
};
return await _articleClient.UpdateAsync(article);
}
Delete Article
public async Task DeleteArticle(int articleId)
{
await _articleClient.DeleteAsync(articleId);
}
Create Article with AI
public async Task<ArticleInfo> CreateWithAI(string prompt, bool withImage = false)
{
return await _articleAIClient.CreateWithAIAsync(prompt, generateImage: withImage);
}
// Example usage:
// var article = await CreateWithAI("Write an article about clean architecture in .NET 8", withImage: true);
Update Article with AI
public async Task<ArticleInfo> UpdateWithAI(int articleId, string prompt)
{
return await _articleAIClient.UpdateWithAIAsync(articleId, prompt, generateImage: false);
}
Category Management
public async Task CategoryExamples()
{
// List all categories
var categories = await _categoryClient.GetAllAsync();
// List root categories (no parent)
var rootCategories = await _categoryClient.ListByParentAsync(parentId: null);
// List subcategories
var subCategories = await _categoryClient.ListByParentAsync(parentId: 1);
// Create category
var newCategory = await _categoryClient.CreateAsync(new CategoryInfo
{
Title = "Technology",
ParentId = null // Root category
});
// Create subcategory
var subCategory = await _categoryClient.CreateAsync(new CategoryInfo
{
Title = "Web Development",
ParentId = newCategory.CategoryId
});
// Delete category
await _categoryClient.DeleteAsync((int)newCategory.CategoryId);
}
Tag Management
public async Task TagExamples()
{
// List all tags
var tags = await _tagClient.GetAllAsync();
// List tags filtered by user roles
var roleTags = await _tagClient.ListByRolesAsync();
// Create tag
var newTag = await _tagClient.CreateAsync(new TagInfo { Title = "C#" });
// Merge tags (move all articles from source to target, then delete source)
await _tagClient.MergeTagsAsync(sourceTagId: 5, targetTagId: 2);
// Delete tag
await _tagClient.DeleteAsync((int)newTag.TagId);
}
Image Upload
[HttpPost("upload")]
[Authorize]
public async Task<ActionResult<string>> UploadImage(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded");
var imageUrl = await _imageClient.UploadImageAsync(file);
return Ok(imageUrl);
}
Controller Example — Blog API
using Microsoft.AspNetCore.Mvc;
using NNews.ACL.Interfaces;
using NNews.DTO;
[Route("api/[controller]")]
[ApiController]
public class BlogController : ControllerBase
{
private readonly IArticleClient _articleClient;
private readonly ICategoryClient _categoryClient;
private readonly ITagClient _tagClient;
public BlogController(
IArticleClient articleClient,
ICategoryClient categoryClient,
ITagClient tagClient)
{
_articleClient = articleClient;
_categoryClient = categoryClient;
_tagClient = tagClient;
}
[HttpGet("articles")]
public async Task<ActionResult<PagedResult<ArticleInfo>>> GetArticles(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 12,
[FromQuery] long? categoryId = null,
[FromQuery] int? status = null)
{
var result = await _articleClient.GetAllAsync(categoryId, status, page, pageSize);
return Ok(result);
}
[HttpGet("articles/{id}")]
public async Task<ActionResult<ArticleInfo>> GetArticle(int id)
{
var article = await _articleClient.GetByIdAsync(id);
return Ok(article);
}
[HttpGet("articles/search")]
public async Task<ActionResult<PagedResult<ArticleInfo>>> Search(
[FromQuery] string keyword,
[FromQuery] int page = 1)
{
var result = await _articleClient.SearchAsync(keyword, page);
return Ok(result);
}
[HttpDelete("articles/{id}")]
public async Task<IActionResult> DeleteArticle(int id)
{
await _articleClient.DeleteAsync(id);
return NoContent();
}
[HttpGet("categories")]
public async Task<ActionResult<IList<CategoryInfo>>> GetCategories()
{
var categories = await _categoryClient.GetAllAsync();
return Ok(categories);
}
[HttpGet("tags")]
public async Task<ActionResult<IList<TagInfo>>> GetTags()
{
var tags = await _tagClient.GetAllAsync();
return Ok(tags);
}
}
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| HTTP 500 on all requests | NNews API unreachable | Verify NNews:ApiUrl in appsettings |
| HTTP 401 Unauthorized | Missing or invalid auth token | Ensure Bearer token is forwarded or endpoint is public |
Empty PagedResult | Wrong tenant or no data | Check Tenant:DefaultTenantId configuration |
DI error for IArticleClient | Missing registration | Add services.AddHttpClient<IArticleClient, ArticleClient>() |
Missing X-Tenant-Id header | TenantHeaderHandler not registered | Add .AddHttpMessageHandler<TenantHeaderHandler>() |
NNewsSetting.ApiUrl empty | Missing configuration section | Add "NNews": { "ApiUrl": "..." } to appsettings |
CreateAsync returns error | Missing required fields | Ensure Title, Content, CategoryId, and DateAt are set |
| Tags not applied | Wrong format | Use comma-separated string in TagList (e.g., "tag1,tag2,tag3") |
Response Guidelines
- Be specific: Reference exact class names, interfaces, and method signatures
- Show code: Always include working code examples based on the patterns above
- Context-aware: If the user is working in a project that consumes NNews, reference their existing DI setup and configuration
- Minimal changes: Only suggest what's needed for the user's specific question
- Multi-tenancy: Always remind about configuring
Tenant:DefaultTenantIdand registeringTenantHeaderHandler - Prerequisites: Mention required NuGet package and configuration if starting fresh