Loading editor/src/webedit_api.cpp +21 −19 Changes for editor/src/webedit_api.cpp: 21 added lines, 19 removed lines. Original line number Diff line number Diff line Loading @@ -3899,7 +3899,7 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore( autoPublishSurveysForDocument(docXml, authid, blogUrl, domain); std::string html = renderTree(doc.root); std::string llmsTxt = generateLlmsTxt(doc.root); std::string llmsTxt = generateLlmsTxt(doc.root, doc.currentDocName); // Build the publish command: // [{"authid":"..."}, {"command":"publish","target":"template","html":"...","params":{...}}] Loading Loading @@ -4744,29 +4744,31 @@ namespace { } } std::string webedit::Api::generateLlmsTxt(const blogi::webedit::EditPlugin *root) { std::string webedit::Api::generateLlmsTxt(const blogi::webedit::EditPlugin *root, const std::string &title) { if (!root) return ""; std::vector<std::string> blocks; collectLlmsBlocks(root, blocks); // llms.txt is expected to be Markdown with at least one H1: promote the // first heading found (whatever level) to H1 if the page has headings but // none is already top-level. bool hasH1 = false; int firstHeadingIdx = -1; for (size_t i = 0; i < blocks.size(); ++i) { if (blocks[i].size() > 1 && blocks[i][0] == '#' && blocks[i][1] == ' ') { hasH1 = true; break; } if (firstHeadingIdx < 0 && !blocks[i].empty() && blocks[i][0] == '#') firstHeadingIdx = static_cast<int>(i); } if (!hasH1 && firstHeadingIdx >= 0) { std::string &h = blocks[static_cast<size_t>(firstHeadingIdx)]; h = "#" + h.substr(h.find_first_not_of('#')); } std::string out; // Empty stays empty (not even a synthesized H1): the publish flow uses an // empty llms_txt to mean "this document's text was removed, clear it" — // see the comment where this return value is packed into paramsObj below. if (blocks.empty()) return ""; // llms.txt (https://llmstxt.org/) requires an H1 as the file's first // line — the spec's only mandatory section, and by convention a single // one. Lead with the document's own title rather than scavenging a // heading out of the body, so the H1 is always present and always first // no matter what widgets the page happens to contain; demote any // H1-level block already in the body (e.g. a TextBox styled at the // top heading size) to H2 so it doesn't compete with the title. for (auto &block : blocks) { if (block.size() > 1 && block[0] == '#' && block[1] == ' ') block.insert(0, "#"); } std::string out = "# " + (title.empty() ? "Untitled" : title); for (const auto &block : blocks) { if (!out.empty()) out += "\n\n"; out += "\n\n"; out += block; } return out; Loading editor/src/webedit_api.h +1 −1 Changes for editor/src/webedit_api.h: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -241,7 +241,7 @@ namespace webedit { std::string renderTree(const blogi::webedit::EditPlugin *node); std::string exportTreeXml(DocumentState &doc); std::string serializeElementXml(blogi::webedit::EditPlugin *node); std::string generateLlmsTxt(const blogi::webedit::EditPlugin *root); std::string generateLlmsTxt(const blogi::webedit::EditPlugin *root, const std::string &title); void persistDocumentStateToSession(const std::string &sessionid, DocumentState &doc); void restoreDocumentStateFromSession(const std::string &sessionid, DocumentState &doc); void loadDocumentTreeFromXml(DocumentState &doc, const std::string &xml); Loading plugins/content/content.cpp +15 −0 Changes for plugins/content/content.cpp: 15 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -132,6 +132,21 @@ namespace blogi { } } void GetLlmsLinks(const int tid,std::vector<blogi::LlmsLink> &links){ dbpp::SQL sql; dbpp::DBResult res; sql="SELECT content.slug,content.title,content.descrition FROM content;"; int ncount=Args->database[tid]->exec(sql,res); for(int i=0; i<ncount; ++i){ blogi::LlmsLink link; link.section = "Blog"; link.title = res[i][1]; link.url = Args->config->getsiteurl() + "/content/read/" + res[i][0]; link.description = res[i][2]; links.push_back(std::move(link)); } } void contentIdxPage(const int tid,libhttppp::HttpRequest &curreq,libhtmlpp::HtmlElement *page,const std::string &tag,int start,int end,const std::string &sessionid){ std::string _lang = getLang(*Args->database[tid]); libhttppp::HTTPException excep; libhtmlpp::HtmlString condat; Loading plugins/staticpage/static.cpp +35 −0 Changes for plugins/staticpage/static.cpp: 35 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ #include <iostream> #include <algorithm> #include <cctype> #include <htmlpp/html.h> #include <htmlpp/exception.h> Loading Loading @@ -90,6 +91,40 @@ void blogi::StaticPage::GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std } } namespace { // Static pages have no dedicated title field (just a URL slug), so turn // "some-page_name" into "Some Page Name" for the llms.txt link text. std::string humanizeSlug(const std::string &slug) { std::string title; bool startOfWord = true; for (char c : slug) { if (c == '-' || c == '_') { if (!title.empty()) title += ' '; startOfWord = true; continue; } title += startOfWord ? static_cast<char>(std::toupper(static_cast<unsigned char>(c))) : c; startOfWord = false; } return title; } } void blogi::StaticPage::GetLlmsLinks(const int tid,std::vector<blogi::LlmsLink> &links){ dbpp::SQL sql; dbpp::DBResult res; sql << "select url,meta from static_content"; int result = Args->database[tid]->exec(sql,res); for(int i=0; i<result; ++i){ blogi::LlmsLink link; link.section = "Pages"; link.title = humanizeSlug(res[i][0]); link.url = Args->config->getsiteurl()+"/staticpage/"+res[i][0]; link.description = res[i][1]; links.push_back(std::move(link)); } } json_object* blogi::StaticPage::Settings(const int tid,libhttppp::HttpRequest &req,const std::string &sessionid){ std::string surl,curl=req.getRequestURL(); std::string _lang = getLang(*Args->database[tid]); Loading plugins/staticpage/static.h +1 −0 Changes for plugins/staticpage/static.h: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ namespace blogi { const std::string getAuthor(); void GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std::chrono::year_month_day> &sitemap); void GetLlmsLinks(const int tid,std::vector<blogi::LlmsLink> &links); void initPlugin(); bool haveSettings(); Loading Loading
editor/src/webedit_api.cpp +21 −19 Changes for editor/src/webedit_api.cpp: 21 added lines, 19 removed lines. Original line number Diff line number Diff line Loading @@ -3899,7 +3899,7 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore( autoPublishSurveysForDocument(docXml, authid, blogUrl, domain); std::string html = renderTree(doc.root); std::string llmsTxt = generateLlmsTxt(doc.root); std::string llmsTxt = generateLlmsTxt(doc.root, doc.currentDocName); // Build the publish command: // [{"authid":"..."}, {"command":"publish","target":"template","html":"...","params":{...}}] Loading Loading @@ -4744,29 +4744,31 @@ namespace { } } std::string webedit::Api::generateLlmsTxt(const blogi::webedit::EditPlugin *root) { std::string webedit::Api::generateLlmsTxt(const blogi::webedit::EditPlugin *root, const std::string &title) { if (!root) return ""; std::vector<std::string> blocks; collectLlmsBlocks(root, blocks); // llms.txt is expected to be Markdown with at least one H1: promote the // first heading found (whatever level) to H1 if the page has headings but // none is already top-level. bool hasH1 = false; int firstHeadingIdx = -1; for (size_t i = 0; i < blocks.size(); ++i) { if (blocks[i].size() > 1 && blocks[i][0] == '#' && blocks[i][1] == ' ') { hasH1 = true; break; } if (firstHeadingIdx < 0 && !blocks[i].empty() && blocks[i][0] == '#') firstHeadingIdx = static_cast<int>(i); } if (!hasH1 && firstHeadingIdx >= 0) { std::string &h = blocks[static_cast<size_t>(firstHeadingIdx)]; h = "#" + h.substr(h.find_first_not_of('#')); } std::string out; // Empty stays empty (not even a synthesized H1): the publish flow uses an // empty llms_txt to mean "this document's text was removed, clear it" — // see the comment where this return value is packed into paramsObj below. if (blocks.empty()) return ""; // llms.txt (https://llmstxt.org/) requires an H1 as the file's first // line — the spec's only mandatory section, and by convention a single // one. Lead with the document's own title rather than scavenging a // heading out of the body, so the H1 is always present and always first // no matter what widgets the page happens to contain; demote any // H1-level block already in the body (e.g. a TextBox styled at the // top heading size) to H2 so it doesn't compete with the title. for (auto &block : blocks) { if (block.size() > 1 && block[0] == '#' && block[1] == ' ') block.insert(0, "#"); } std::string out = "# " + (title.empty() ? "Untitled" : title); for (const auto &block : blocks) { if (!out.empty()) out += "\n\n"; out += "\n\n"; out += block; } return out; Loading
editor/src/webedit_api.h +1 −1 Changes for editor/src/webedit_api.h: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -241,7 +241,7 @@ namespace webedit { std::string renderTree(const blogi::webedit::EditPlugin *node); std::string exportTreeXml(DocumentState &doc); std::string serializeElementXml(blogi::webedit::EditPlugin *node); std::string generateLlmsTxt(const blogi::webedit::EditPlugin *root); std::string generateLlmsTxt(const blogi::webedit::EditPlugin *root, const std::string &title); void persistDocumentStateToSession(const std::string &sessionid, DocumentState &doc); void restoreDocumentStateFromSession(const std::string &sessionid, DocumentState &doc); void loadDocumentTreeFromXml(DocumentState &doc, const std::string &xml); Loading
plugins/content/content.cpp +15 −0 Changes for plugins/content/content.cpp: 15 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -132,6 +132,21 @@ namespace blogi { } } void GetLlmsLinks(const int tid,std::vector<blogi::LlmsLink> &links){ dbpp::SQL sql; dbpp::DBResult res; sql="SELECT content.slug,content.title,content.descrition FROM content;"; int ncount=Args->database[tid]->exec(sql,res); for(int i=0; i<ncount; ++i){ blogi::LlmsLink link; link.section = "Blog"; link.title = res[i][1]; link.url = Args->config->getsiteurl() + "/content/read/" + res[i][0]; link.description = res[i][2]; links.push_back(std::move(link)); } } void contentIdxPage(const int tid,libhttppp::HttpRequest &curreq,libhtmlpp::HtmlElement *page,const std::string &tag,int start,int end,const std::string &sessionid){ std::string _lang = getLang(*Args->database[tid]); libhttppp::HTTPException excep; libhtmlpp::HtmlString condat; Loading
plugins/staticpage/static.cpp +35 −0 Changes for plugins/staticpage/static.cpp: 35 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ #include <iostream> #include <algorithm> #include <cctype> #include <htmlpp/html.h> #include <htmlpp/exception.h> Loading Loading @@ -90,6 +91,40 @@ void blogi::StaticPage::GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std } } namespace { // Static pages have no dedicated title field (just a URL slug), so turn // "some-page_name" into "Some Page Name" for the llms.txt link text. std::string humanizeSlug(const std::string &slug) { std::string title; bool startOfWord = true; for (char c : slug) { if (c == '-' || c == '_') { if (!title.empty()) title += ' '; startOfWord = true; continue; } title += startOfWord ? static_cast<char>(std::toupper(static_cast<unsigned char>(c))) : c; startOfWord = false; } return title; } } void blogi::StaticPage::GetLlmsLinks(const int tid,std::vector<blogi::LlmsLink> &links){ dbpp::SQL sql; dbpp::DBResult res; sql << "select url,meta from static_content"; int result = Args->database[tid]->exec(sql,res); for(int i=0; i<result; ++i){ blogi::LlmsLink link; link.section = "Pages"; link.title = humanizeSlug(res[i][0]); link.url = Args->config->getsiteurl()+"/staticpage/"+res[i][0]; link.description = res[i][1]; links.push_back(std::move(link)); } } json_object* blogi::StaticPage::Settings(const int tid,libhttppp::HttpRequest &req,const std::string &sessionid){ std::string surl,curl=req.getRequestURL(); std::string _lang = getLang(*Args->database[tid]); Loading
plugins/staticpage/static.h +1 −0 Changes for plugins/staticpage/static.h: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -41,6 +41,7 @@ namespace blogi { const std::string getAuthor(); void GetSiteMap(const int tid,std::map<libhttppp::HttpUrl,std::chrono::year_month_day> &sitemap); void GetLlmsLinks(const int tid,std::vector<blogi::LlmsLink> &links); void initPlugin(); bool haveSettings(); Loading