aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-10-06 16:22:40 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-10-06 16:22:40 -0400
commit0c2795f299ddd9398e107b136f1f2c4bd5da2037 (patch)
tree3a8d62e38b4b89ee5cde0359375c92622610852f
parentf7b739d08a50adc59583b02a8348b29675731cd2 (diff)
readme updates and atom support
-rw-r--r--README.md38
-rw-r--r--back-end/src/FeedGenerator.cs29
2 files changed, 41 insertions, 26 deletions
diff --git a/README.md b/README.md
index d69e3a2..a246c99 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
<h1 align="center">CMNext</h1>
<p align="center">
-A modern, dead simple, mulit-channel, content publishing platform (CMS) built with the VNLib.Plugins.Essentials framework, using S3 or FTP storage.
+A self-hosted, mulit-channel, content publishing platform (CMS) built with my [Essentials](https://github.com/VnUgE/VNLib.Core) framework, using your existing S3 or FTP storage.
</p>
<h4 align="center">
@@ -24,29 +24,33 @@ A modern, dead simple, mulit-channel, content publishing platform (CMS) built wi
<img src="https://www.vaughnnugent.com/public/resources/downloads/cms/c/hsknruyxlu6wpxq7q5hjbyv72y.png" width="100%">
+## What's the TLDR?
+CMNext is essentially a fancy admin web-ui, that turns your existing S3 or FTP static hosting service into a powerfull mutli-channel CMS, with Podcast 2.0 support. Your S3 or FTP becomes a headless/serverless CMS you can access your content from using web `fetch()` or the inlcuded JavaScript client library.
+
## Features
- - ✔Own your content and distribute it on your own equipment (or cloud)
- - ✔Uses your existing S3 or FTP static storage systems
- - ✔Publish arbitrary content and reference it in posts
- - ✔Publish podcasts (w/ Apple and Spotify support) w/ pocast 2.0 RSS feeds
- - ✔Reference the entire system through static JSON files
+ - ✔Own your content and distribute it on your own equipment (or cloud)
+ - ✔Publish podcasts w/ pocast 2.0 enabled RSS feeds
+ - ✔Turn it on only when you need to edit your content
+ - ✔Uses your existing S3 or FTP static storage systems
+ - ✔Publish arbitrary content and reference it in posts
+ - ✔Configurable RSS feeds for all channels
+ - ✔Built in WYSWYG and markdown conversion for easy editing
+ - ✔Reference the entire system through static JSON files
- ✔No databases (for this plugin anyway)
- - ✔Light or dark theme!
-
-TLDR, you can create your own file based, content publishing platform that has built in editing tools, blog post management, arbitrary content publishing to "unlimited" channels, with RSS and Apple/Spotify support for your podcast 2.0. Its essentially a fancy, self-hosted file based CMS with an admin UI.
+ - ✔Light or dark theme!
## What won't it do for you?
This tool does not (and will not)
-- ❌Serve your content
-- ❌Host your blog front-end
-- ❌Provide good SEO results by itself
-- ❌Configure/manage your public storage system (how the content is viewed)
+- ❌Serve your content
+- ❌Host your blog front-end
+- ❌Provide good SEO results by itself
+- ❌Configure/manage your public storage system (how the content is viewed)
## How does it do it?
-The user-interface interacts with the back-end plugin to store your entire blog's configuration in your storage system (S3 and FTP) in JSON files. These JSON files exist so you can access them from your blog front-end using web `fetch()`. This gives you so much freedom to develop without needing worry about hosting a new service/api since you are just accessing static files. This means you get all the performance and security benefits of static file hosting!
+When installed, you get self contained server application with a web-ui that you will use to create channels, publish posts, and add content. All of these actions are stored in json files within your S3 or FTP filesystem (or xml RSS feeds). These json files will be referenced by the client using fetch() or the included JavaScript client library to fetch your content and metadata. You can add custom xml elements to your feeds, per channel, and per post (as an item). There are also easy podcast integrations such as adding an enclosure file from existing content, and html content for the podcast description. [CKEdtior](https://github.com/ckeditor/ckeditor5) is the WYSWYG editor of choice for this project, and [showdownjs](https://github.com/showdownjs/showdown) for easy markdown-html conversions.
## Why would I use it?
-Simple - you get a CMS on your own equipment, without the headache of running another production application and trusting its security. That doesn't mean I don't take security seriously, because I do, but pre-release apps vs. production apps are very different levels of security. and tech folks are (and should be) concerned with the security of a production application!
+Simple - you get a CMS on your own equipment, without the headache of running another production application and trusting its security. That doesn't mean I don't take security seriously, because I do, but pre-release apps vs. production apps are very different levels of security. Tech folks are (and should be) concerned with the security of a production application!
## How do I get started?
Use this [documentation link](https://www.vaughnnugent.com/resources/software/articles?tags=_cmnext), or at the top of the readme, and follow the instructions for users, admins, and developers.
@@ -55,7 +59,7 @@ Use this [documentation link](https://www.vaughnnugent.com/resources/software/ar
For now, feel free to clone this repo and respect the license please. The front-end is just a VueJs multi page app, with vue-router. The back end is a .NET class library designed for dynamic loading by a VNLib.Plugins.Essentials compatible runtime host. The back end will be much more complicated to provide docs on, so they may come later. The front-end is rather straightforward for node/vuejs web-devs. Further instructions will be in the readme files.
### License
-The software in this repository is licensed under the GNU Affero General Public License (or any later version). See the LICENSE files for more information.
+The software in this repository is licensed under the GNU Affero General Public License (or any later version). See the LICENSE files for more information.
### Contact
-Head over to my [website](https://www.vaughnnugent.com) for my contact info/community. \ No newline at end of file
+Head over to my [website](https://www.vaughnnugent.com) for my contact info/community. \ No newline at end of file
diff --git a/back-end/src/FeedGenerator.cs b/back-end/src/FeedGenerator.cs
index ae8fd52..16915ff 100644
--- a/back-end/src/FeedGenerator.cs
+++ b/back-end/src/FeedGenerator.cs
@@ -37,9 +37,10 @@ namespace Content.Publishing.Blog.Admin
internal sealed class FeedGenerator : IRssFeedGenerator
{
const int defaultMaxItems = 20;
- const string ITUNES_XML_ATTR = "http://www.itunes.com/dtds/podcast-1.0.dtd";
- const string CONTENT_XML_ATTR = "http://purl.org/rss/1.0/modules/content/";
- const string PODCAST_INDEX_ATTR = "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md";
+ const string ITUNES_NAMESPACE_LINK = "http://www.itunes.com/dtds/podcast-1.0.dtd";
+ const string CONTENT_NAMESPACE_LINK = "http://purl.org/rss/1.0/modules/content/";
+ const string PODCAST_INDEX_LINK = "https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md";
+ const string ATOM_NAMESPACE_LINK = "https://www.w3.org/2005/Atom";
const string GENERATOR_NAME = "CMNext";
public FeedGenerator(PluginBase pbase)
@@ -71,22 +72,26 @@ namespace Content.Publishing.Blog.Admin
writer.WriteStartDocument();
writer.WriteStartElement("rss");
writer.WriteAttributeString("version", "2.0");
- writer.WriteAttributeString("xmlns", "itunes", null, ITUNES_XML_ATTR);
- writer.WriteAttributeString("xmlns", "content", null, CONTENT_XML_ATTR);
- writer.WriteAttributeString("xmlns", "podcast", null, PODCAST_INDEX_ATTR);
+ writer.WriteAttributeString("xmlns", "itunes", null, ITUNES_NAMESPACE_LINK);
+ writer.WriteAttributeString("xmlns", "content", null, CONTENT_NAMESPACE_LINK);
+ writer.WriteAttributeString("xmlns", "podcast", null, PODCAST_INDEX_LINK);
+ writer.WriteAttributeString("xmlns", "atom", null, ATOM_NAMESPACE_LINK);
//Channel element
writer.WriteStartElement("channel");
writer.WriteElementString("title", context.BlogName);
+ writer.WriteElementString("atom", "title", null, context.BlogName);
writer.WriteElementString("link", context.Feed.PublihUrl);
//Description/summary
writer.WriteElementString("description", context.Feed.Description);
writer.WriteElementString("itunes", "summary", null, context.Feed.Description);
+ writer.WriteElementString("atom", "summary", null, context.Feed.Description);
writer.WriteElementString("itunes", "author", null, context.Feed.Author);
+ writer.WriteElementString("atom", "author", null, context.Feed.Author);
//Itunes owner tag
writer.WriteStartElement("itunes", "owner", null);
@@ -106,11 +111,10 @@ namespace Content.Publishing.Blog.Admin
if(!context.Feed.ExtendedProperties.Any(static p => "generator".Equals(p.Name, StringComparison.OrdinalIgnoreCase)))
{
writer.WriteElementString("generator", GENERATOR_NAME);
+ writer.WriteElementString("atom", "generator", null, GENERATOR_NAME);
}
}
-
- //Author
- writer.WriteElementString("itunes", "author", null, context.Feed.Author);
+
//Itunes image url
if (context.Feed.ImageUrl != null)
@@ -134,13 +138,16 @@ namespace Content.Publishing.Blog.Admin
writer.WriteElementString("title", post.Title);
writer.WriteElementString("itunes","title", null, post.Title);
+ writer.WriteElementString("atom", "title", null, post.Title);
writer.WriteElementString("link", $"{context.Feed.PublihUrl}/{post.Id}");
writer.WriteElementString("itunes", "author", null, post.Author);
+ writer.WriteElementString("atom", "author", null, post.Author);
//Description is just the post summary
writer.WriteElementString("itunes", "summary", null, post.Summary);
+ writer.WriteElementString("atom", "summary", null, post.Summary);
//Allow an html description from the post meta itself
if (post.HtmlDescription != null)
@@ -161,9 +168,12 @@ namespace Content.Publishing.Blog.Admin
//Time as iso string from unix seconds timestamp
string pubDate = DateTimeOffset.FromUnixTimeSeconds(post.Created).ToString("R");
+ string updated = DateTimeOffset.FromUnixTimeSeconds(post.Date).ToString("R");
writer.WriteElementString("pubDate", pubDate);
writer.WriteElementString("published", pubDate);
+ writer.WriteElementString("atom", "published", null, pubDate);
+ writer.WriteElementString("atom", "updated", null, updated);
if (post.Image != null)
{
@@ -182,6 +192,7 @@ namespace Content.Publishing.Blog.Admin
//Set post id as the guid
writer.WriteElementString("guid", post.Id);
+ writer.WriteElementString("podcast", "guid", null, post.Id);
writer.WriteEndElement();
}