openwrite 0.14.0__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {openwrite-0.14.0/openwrite.egg-info → openwrite-0.15.0}/PKG-INFO +5 -3
  2. {openwrite-0.14.0 → openwrite-0.15.0}/README.md +4 -2
  3. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/__init__.py +11 -11
  4. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/gemini_multi.py +21 -1
  5. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/auth.py +1 -1
  6. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/blog.py +53 -58
  7. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/dashboard.py +76 -5
  8. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/main.py +29 -6
  9. openwrite-0.15.0/openwrite/static/favicon.png +0 -0
  10. openwrite-0.15.0/openwrite/static/feed/.gitkeep +0 -0
  11. openwrite-0.15.0/openwrite/static/feed/74.xml +12 -0
  12. openwrite-0.15.0/openwrite/static/feed/a.xml +2 -0
  13. openwrite-0.15.0/openwrite/static/feed/anim3sh1t.xml +12 -0
  14. openwrite-0.15.0/openwrite/static/feed/b.xml +2 -0
  15. openwrite-0.15.0/openwrite/static/feed/corrode.xml +4 -0
  16. openwrite-0.15.0/openwrite/static/feed/dirtysanchez.xml +6 -0
  17. openwrite-0.15.0/openwrite/static/feed/eikerammen.xml +165 -0
  18. openwrite-0.15.0/openwrite/static/feed/fedi.xml +9 -0
  19. openwrite-0.15.0/openwrite/static/feed/friskt.xml +2 -0
  20. openwrite-0.15.0/openwrite/static/feed/gordi.xml +6 -0
  21. openwrite-0.15.0/openwrite/static/feed/igilq.xml +226 -0
  22. openwrite-0.15.0/openwrite/static/feed/io.xml +50 -0
  23. openwrite-0.15.0/openwrite/static/feed/isyuricunha.xml +4 -0
  24. openwrite-0.15.0/openwrite/static/feed/kenny.xml +2 -0
  25. openwrite-0.15.0/openwrite/static/feed/m0bi.xml +6 -0
  26. openwrite-0.15.0/openwrite/static/feed/main.xml +236 -0
  27. openwrite-0.15.0/openwrite/static/feed/mywritese.xml +2 -0
  28. openwrite-0.15.0/openwrite/static/feed/nay-thoughs.xml +2 -0
  29. openwrite-0.15.0/openwrite/static/feed/nintendoflesz.xml +7 -0
  30. openwrite-0.15.0/openwrite/static/feed/phronima.xml +68 -0
  31. openwrite-0.15.0/openwrite/static/feed/ram.xml +75 -0
  32. openwrite-0.15.0/openwrite/static/feed/samah.xml +2 -0
  33. openwrite-0.15.0/openwrite/static/feed/themes-test.xml +18 -0
  34. openwrite-0.15.0/openwrite/static/feed/totylkoja.xml +78 -0
  35. openwrite-0.15.0/openwrite/static/feed/vegafjord.xml +48 -0
  36. openwrite-0.15.0/openwrite/static/feed/vijayvaradharaj.xml +2 -0
  37. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/style.css +5 -1
  38. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/blog.html +3 -1
  39. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/dashboard.html +1 -1
  40. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/edit.html +6 -0
  41. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/footer.html +1 -1
  42. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/new_page.html +1 -0
  43. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/new_post.html +54 -2
  44. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/post.html +9 -0
  45. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/stats.html +26 -0
  46. openwrite-0.15.0/openwrite/utils/create_db.py +28 -0
  47. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/utils/helpers.py +3 -2
  48. openwrite-0.15.0/openwrite/utils/migrations.py +136 -0
  49. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/utils/models.py +26 -1
  50. openwrite-0.15.0/openwrite/utils/tags.py +242 -0
  51. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/utils/worker.py +4 -3
  52. {openwrite-0.14.0 → openwrite-0.15.0/openwrite.egg-info}/PKG-INFO +5 -3
  53. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite.egg-info/SOURCES.txt +30 -0
  54. {openwrite-0.14.0 → openwrite-0.15.0}/pyproject.toml +1 -1
  55. openwrite-0.14.0/openwrite/utils/create_db.py +0 -15
  56. {openwrite-0.14.0 → openwrite-0.15.0}/LICENSE +0 -0
  57. {openwrite-0.14.0 → openwrite-0.15.0}/MANIFEST.in +0 -0
  58. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/cli.py +0 -0
  59. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/db/__init__.py +0 -0
  60. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/db/base.py +0 -0
  61. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/gemini_single.py +0 -0
  62. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/admin.py +0 -0
  63. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/federation.py +0 -0
  64. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/routes/upload.py +0 -0
  65. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/IBMPlexSans-Regular.ttf +0 -0
  66. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/Merriweather.ttf +0 -0
  67. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/avatar.png +0 -0
  68. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/emoji.json +0 -0
  69. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/favicon.ico +0 -0
  70. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/logo.png +0 -0
  71. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/chart.js +0 -0
  72. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/highlight.min.js +0 -0
  73. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/nativeEmoji.js +0 -0
  74. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/openwrite.js +0 -0
  75. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/papaparse.min.js +0 -0
  76. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/showdown.min.js +0 -0
  77. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/showdown.min.js.map +0 -0
  78. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/site.compat.min.js +0 -0
  79. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/script/site.min.js +0 -0
  80. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/blog_design.css +0 -0
  81. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/default.css +0 -0
  82. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/hl.css +0 -0
  83. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/nativeEmoji.min.css +0 -0
  84. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/cyberpunk.css +0 -0
  85. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/light.css +0 -0
  86. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/obscura.css +0 -0
  87. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/paper.css +0 -0
  88. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/stickynote.css +0 -0
  89. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/terminal.css +0 -0
  90. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/warmnight.css +0 -0
  91. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/static/style/themes/windows98.css +0 -0
  92. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/admin.html +0 -0
  93. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/blog_footer.html +0 -0
  94. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/blog_header.html +0 -0
  95. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/blog_preview.html +0 -0
  96. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/changepw.html +0 -0
  97. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/create.html +0 -0
  98. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/discover.html +0 -0
  99. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/header.html +0 -0
  100. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/import.html +0 -0
  101. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/index.html +0 -0
  102. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/instances.html +0 -0
  103. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/login.html +0 -0
  104. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/page_preview.html +0 -0
  105. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/preview.html +0 -0
  106. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/register.html +0 -0
  107. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/settings.html +0 -0
  108. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/templates/translations.html +0 -0
  109. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/utils/db.py +0 -0
  110. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/utils/i18n.json +0 -0
  111. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite/utils/valkey_client.py +0 -0
  112. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite.egg-info/dependency_links.txt +0 -0
  113. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite.egg-info/entry_points.txt +0 -0
  114. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite.egg-info/requires.txt +0 -0
  115. {openwrite-0.14.0 → openwrite-0.15.0}/openwrite.egg-info/top_level.txt +0 -0
  116. {openwrite-0.14.0 → openwrite-0.15.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwrite
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  License: GNU AFFERO GENERAL PUBLIC LICENSE
5
5
  Version 3, 19 November 2007
6
6
 
@@ -689,13 +689,13 @@ Requires-Dist: redis
689
689
  Dynamic: license-file
690
690
 
691
691
  ![logo](https://openwrite.io/static/logo.png)
692
+
692
693
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
693
694
 
694
695
  ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
695
696
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
696
697
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
697
698
  ![pythonver](https://img.shields.io/badge/python%20version-3.11-blue)
698
- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/openwriteio/openwrite/python-tests.yml)
699
699
 
700
700
  ---
701
701
 
@@ -715,7 +715,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
715
715
  - Lightweight
716
716
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
717
717
  - Custom CSS per blog
718
- - Federation using ActivityPub protocol: Follow and Like
718
+ - Federation using ActivityPub protocol
719
719
  - Likes system
720
720
  - Posts importing from xml/csv
721
721
  - Blog themes
@@ -724,6 +724,8 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
724
724
  - Multilanguage (currently English and Polish)
725
725
  - Custom favicon per blog
726
726
  - Subpages per blog
727
+ - Valkey support
728
+ - Post tags, filtering
727
729
 
728
730
 
729
731
  ## Installation
@@ -1,11 +1,11 @@
1
1
  ![logo](https://openwrite.io/static/logo.png)
2
+
2
3
  openwrite is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise.
3
4
 
4
5
  ![PyPI - Downloads](https://img.shields.io/pypi/dw/openwrite)
5
6
  ![PyPI - Version](https://img.shields.io/pypi/v/openwrite)
6
7
  [![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
7
8
  ![pythonver](https://img.shields.io/badge/python%20version-3.11-blue)
8
- ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/openwriteio/openwrite/python-tests.yml)
9
9
 
10
10
  ---
11
11
 
@@ -25,7 +25,7 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
25
25
  - Lightweight
26
26
  - No tracking, only data collected is anonymized(hashed) IP for post view counting and likes
27
27
  - Custom CSS per blog
28
- - Federation using ActivityPub protocol: Follow and Like
28
+ - Federation using ActivityPub protocol
29
29
  - Likes system
30
30
  - Posts importing from xml/csv
31
31
  - Blog themes
@@ -34,6 +34,8 @@ openwrite is a minimalist blogging platform built for writing freely, hosting in
34
34
  - Multilanguage (currently English and Polish)
35
35
  - Custom favicon per blog
36
36
  - Subpages per blog
37
+ - Valkey support
38
+ - Post tags, filtering
37
39
 
38
40
 
39
41
  ## Installation
@@ -25,17 +25,21 @@ def create_app(test_config=None):
25
25
  db_path = test_config.get("DB_PATH", "data.db")
26
26
  load_dotenv(test_config.get("env"), override=True)
27
27
 
28
+ # Always run database migrations to ensure schema is up to date
29
+ from .utils.create_db import init_db
30
+ init_db(db_type, db_path)
31
+
32
+ # Initialize database engine and session
33
+ from .utils.models import User, Blog, Post, Settings, Home, Page
34
+ from .utils.db import init_engine, SessionLocal
35
+ init_engine(db_type, db_path)
36
+ from .utils.db import SessionLocal
37
+
28
38
  if not os.path.exists(".initialized") or test_config is not None:
29
39
  mode = os.getenv("MODE")
30
40
  domain = os.getenv("DOMAIN")
31
41
  f_abs_path = os.path.abspath(__file__)
32
42
  file_cwd = "/".join(f_abs_path.split("/")[:-1])
33
- from .utils.create_db import init_db
34
- init_db(db_type, db_path)
35
- from .utils.models import User, Blog, Post, Settings, Home, Page
36
- from .utils.db import init_engine, SessionLocal
37
- init_engine(db_type, db_path)
38
- from .utils.db import SessionLocal
39
43
  admin_password = os.urandom(16).hex()
40
44
  hashed = bcrypt.hashpw("openwrite".encode("utf-8"), bcrypt.gensalt())
41
45
  admin_user = User(username="admin", email="", password_hash=hashed.decode("utf-8"), verified=1, admin=1)
@@ -171,14 +175,10 @@ def create_app(test_config=None):
171
175
  from .routes.main import main_bp
172
176
  from .routes.upload import upload_bp
173
177
  from .routes.federation import federation_bp
174
- from .utils.db import init_engine, SessionLocal
175
178
  from flask_cors import CORS
176
179
 
177
180
  CORS(app)
178
181
 
179
- init_engine(db_type, db_path)
180
- from .utils.db import SessionLocal
181
-
182
182
  app.register_blueprint(auth_bp)
183
183
  app.register_blueprint(dashboard_bp)
184
184
  app.register_blueprint(blog_bp)
@@ -212,7 +212,7 @@ def create_app(test_config=None):
212
212
 
213
213
  trans = g.db.query(Home).filter(Home.type == "translation", Home.language == lang).all()
214
214
 
215
- g.version = "0.14.0"
215
+ g.version = "0.15.0"
216
216
  g.mode = os.getenv("MODE")
217
217
  f_abs_path = os.path.abspath(__file__)
218
218
  g.mainpath = "/".join(f_abs_path.split("/")[:-1])
@@ -22,8 +22,10 @@ def root(req):
22
22
  global session
23
23
  resp = f"```\n{openwrite_logo}\n```"
24
24
  resp += "\nopenwrite.io is a minimalist blogging platform built for writing freely, hosting independently, and publishing without noise. To create your own blog, visit:\n"
25
- resp += "=> https://openwrite.io openwrite.io\n"
25
+ resp += "=> https://openwrite.io https://openwrite.io\n"
26
26
  resp += "In here you can read posts published.\n"
27
+ resp += "If you want to list all blogs (that did not disable indexing) find them in the link below:\n"
28
+ resp += "=> /blogs All Blogs list"
27
29
  resp += "\n## 🗞️ Latest posts:\n"
28
30
  try:
29
31
  posts = (session.query(Post).filter(Post.feed == "1", Post.isdraft == "0").order_by(Post.id.desc()).limit(10).all())
@@ -36,6 +38,20 @@ def root(req):
36
38
  finally:
37
39
  session.close()
38
40
 
41
+ def all_blogs(req):
42
+ global session
43
+ blogs = session.query(Blog).filter_by(index='on').order_by(Blog.title).all()
44
+ resp = "Here you will find a list of all blogs in openwrite that did not disable their indexing:\n"
45
+ resp += "\n"
46
+ for b in blogs:
47
+ posts = session.query(Post).filter_by(blog=b.id).count()
48
+ last = session.query(Post).filter_by(blog=b.id).order_by(Post.date.desc()).first()
49
+ resp += f"=> /b/{b.name} {b.title} \n"
50
+ resp += f"Posts: {posts}\n"
51
+ resp += f"Last posted: {last.date if last is not None else 'never'}\n\n"
52
+
53
+ return resp
54
+
39
55
  def blog_index(req):
40
56
  global session
41
57
  path = req.path
@@ -134,3 +150,7 @@ def init(capsule):
134
150
  capsule.add("/b/*",
135
151
  blog_index,
136
152
  protocol='gemini')
153
+
154
+ capsule.add("/blogs",
155
+ all_blogs,
156
+ protocol='gemini')
@@ -19,7 +19,7 @@ def register():
19
19
  return render_template('register.html')
20
20
 
21
21
  form_username = request.form.get('username')
22
- if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])?$", form_username):
22
+ if not re.match(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_-]{1,28}[a-zA-Z0-9])+$", form_username):
23
23
  return render_template('register.html', error=g.trans['wrong_username'])
24
24
  form_password = request.form.get('password')
25
25
  form_password2 = request.form.get('password2')
@@ -1,5 +1,6 @@
1
- from flask import Blueprint, render_template, redirect, request, g, Response, abort
1
+ from flask import Blueprint, render_template, redirect, request, g, Response, abort, send_file
2
2
  from openwrite.utils.models import Blog, Post, User, View, Like, Page
3
+ from openwrite.utils.tags import get_post_tags, parse_tags_from_posts_syntax, get_posts_by_tags
3
4
  from openwrite.utils.helpers import gen_link, sanitize_html, anonymize, get_ip
4
5
  from openwrite.utils.valkey_client import valkey_client
5
6
  from feedgen.feed import FeedGenerator
@@ -12,6 +13,26 @@ from datetime import timezone, datetime
12
13
 
13
14
  blog_bp = Blueprint("blog", __name__)
14
15
 
16
+ def process_posts_content(page, blog_id, blog_url, domain):
17
+ posts_matches = re.findall(r'{posts[^}]*}', page.content_raw)
18
+ if not posts_matches:
19
+ return [], page.content_html
20
+
21
+ posts_syntax = posts_matches[0]
22
+ tag_names = parse_tags_from_posts_syntax(posts_syntax)
23
+ posts = get_posts_by_tags(g.db, blog_id, tag_names)
24
+
25
+ posts_html = ""
26
+ for post in posts:
27
+ post_url = f"{blog_url}/{post.link}"
28
+ posts_html += f'<div class="postlist_item"><i class="datetime">{post.date}</i> - <a href="{post_url}">{post.title}</a></div>'
29
+
30
+ processed_content = page.content_html
31
+ for match in posts_matches:
32
+ processed_content = processed_content.replace(match, posts_html)
33
+
34
+ return posts, processed_content
35
+
15
36
  def create_activity_object(post, blog, url, full_content=True):
16
37
  """Create ActivityPub object (Article or Note) based on post content length"""
17
38
  dt = datetime.strptime(str(post.date), "%Y-%m-%d %H:%M:%S")
@@ -139,11 +160,9 @@ def show_blog(blog):
139
160
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
140
161
  blog_obj.url = f"/b/{blog_obj.name}"
141
162
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
142
- if "{posts}" in homepage.content_raw:
143
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
144
- return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
145
-
146
- return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
163
+
164
+ posts, processed_content = process_posts_content(homepage, blog_obj.id, blog_obj.url, g.main_domain)
165
+ return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages, processed_content=processed_content)
147
166
 
148
167
  @blog_bp.route("/", subdomain="<blog>")
149
168
  def show_subblog(blog):
@@ -167,11 +186,9 @@ def show_subblog(blog):
167
186
  blog_obj.url = f"https://{blog_obj.name}.{os.getenv('DOMAIN')}"
168
187
  pages = g.db.query(Page).filter_by(blog=blog_obj.id).all()
169
188
  homepage = g.db.query(Page).filter(Page.blog == blog_obj.id, Page.url == "").first()
170
- if "{posts}" in homepage.content_raw:
171
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
172
- return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages)
173
-
174
- return render_template("blog.html", blog=blog_obj, page=homepage, pages=pages)
189
+
190
+ posts, processed_content = process_posts_content(homepage, blog_obj.id, blog_obj.url, g.main_domain)
191
+ return render_template("blog.html", blog=blog_obj, page=homepage, posts=posts, pages=pages, processed_content=processed_content)
175
192
 
176
193
  @blog_bp.route("/b/<blog>/<post>")
177
194
  def show_post(blog, post):
@@ -182,7 +199,7 @@ def show_post(blog, post):
182
199
  return redirect("/")
183
200
 
184
201
  if post == "rss":
185
- return _generate_rss(blog_obj)
202
+ return serve_rss(blog_obj)
186
203
 
187
204
  if blog_obj.access == "domain":
188
205
  return redirect(f"https://{blog_obj.name}.{os.getenv('DOMAIN')}/{post}")
@@ -196,11 +213,8 @@ def show_post(blog, post):
196
213
  if not page:
197
214
  return redirect("/")
198
215
 
199
- if "{posts}" in page.content_raw:
200
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
201
- return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
202
-
203
- return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
216
+ posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
217
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
204
218
 
205
219
  if is_activitypub_request():
206
220
  url = f"https://{g.main_domain}/b/{blog}"
@@ -240,8 +254,10 @@ def show_post(blog, post):
240
254
  one_post.likes = likes
241
255
  one_post.liked = liked
242
256
 
257
+ post_tags = get_post_tags(g.db, one_post)
258
+
243
259
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
244
- return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
260
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
245
261
 
246
262
  @blog_bp.route("/<post>", subdomain="<blog>")
247
263
  def show_subpost(blog, post):
@@ -252,7 +268,7 @@ def show_subpost(blog, post):
252
268
  return redirect(f"https://{os.getenv('DOMAIN')}/")
253
269
 
254
270
  if post == "rss":
255
- return _generate_rss(blog_obj)
271
+ return serve_rss(blog_obj)
256
272
 
257
273
  if blog_obj.access == "path":
258
274
  return redirect(f"https://{os.getenv('DOMAIN')}/b/{blog_obj.name}/{post}")
@@ -267,11 +283,8 @@ def show_subpost(blog, post):
267
283
  if not page:
268
284
  return redirect("/")
269
285
 
270
- if "{posts}" in page.content_raw:
271
- posts = g.db.query(Post).filter(Post.blog == blog_obj.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
272
- return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages)
273
-
274
- return render_template("blog.html", blog=blog_obj, page=page, pages=pages)
286
+ posts, processed_content = process_posts_content(page, blog_obj.id, blog_obj.url, g.main_domain)
287
+ return render_template("blog.html", blog=blog_obj, page=page, posts=posts, pages=pages, processed_content=processed_content)
275
288
 
276
289
  if is_activitypub_request():
277
290
  url = f"https://{blog}.{g.main_domain}"
@@ -310,9 +323,11 @@ def show_subpost(blog, post):
310
323
 
311
324
  one_post.likes = likes
312
325
  one_post.liked = liked
326
+
327
+ post_tags = get_post_tags(g.db, one_post)
313
328
 
314
329
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
315
- return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages)
330
+ return render_template("post.html", blog=blog_obj, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
316
331
 
317
332
  @blog_bp.route("/.well-known/webfinger")
318
333
  def webfinger():
@@ -382,12 +397,8 @@ def single_showpost(post):
382
397
  if not page:
383
398
  return redirect("/")
384
399
 
385
- if "{posts}" in page.content_raw:
386
- posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").order_by(desc(Post.id)).all()
387
-
388
- return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages)
389
-
390
- return render_template("blog.html", blog=blog, page=page, pages=pages)
400
+ posts, processed_content = process_posts_content(page, blog.id, blog.url, g.main_domain)
401
+ return render_template("blog.html", blog=blog, page=page, posts=posts, pages=pages, processed_content=processed_content)
391
402
 
392
403
  if is_activitypub_request():
393
404
  url = f"https://{g.main_domain}"
@@ -426,8 +437,10 @@ def single_showpost(post):
426
437
  one_post.likes = likes
427
438
  one_post.liked = liked
428
439
 
440
+ post_tags = get_post_tags(g.db, one_post)
441
+
429
442
  user = g.db.query(User).filter_by(id=g.user) if g.user else None
430
- return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages)
443
+ return render_template("post.html", blog=blog, post=one_post, user=user, views=v, likes=likes, pages=pages, post_tags=post_tags)
431
444
 
432
445
  @blog_bp.route("/rss")
433
446
  def single_rss():
@@ -435,7 +448,7 @@ def single_rss():
435
448
  return redirect("/")
436
449
 
437
450
  blog = g.db.query(Blog).first()
438
- return _generate_rss(blog)
451
+ return serve_rss(blog)
439
452
 
440
453
  @blog_bp.route("/like", methods=["POST"])
441
454
  def like():
@@ -483,29 +496,11 @@ def like():
483
496
  status = 201
484
497
 
485
498
  return resp, status
486
-
487
499
 
488
- def _generate_rss(blog):
489
- posts = g.db.query(Post).filter(Post.blog == blog.id, Post.isdraft == "0").all()
490
- fg = FeedGenerator()
491
- fg.title(blog.title)
492
-
493
- if blog.access == "domain":
494
- blog_url = f"https://{blog.name}.{g.main_domain}"
495
- post_base_url = blog_url
496
- else:
497
- blog_url = f"https://{g.main_domain}/b/{blog.name}"
498
- post_base_url = blog_url
499
-
500
- fg.link(href=blog_url, rel="alternate")
501
- fg.description(blog.description_html)
502
-
503
- for p in posts:
504
- soup = BeautifulSoup(p.content_html, "html.parser")
505
- fe = fg.add_entry()
506
- fe.title(p.title)
507
- fe.link(href=f"{post_base_url}/{p.link}")
508
- fe.description(soup.get_text())
509
- fe.published(p.date.replace(tzinfo=timezone.utc))
510
-
511
- return Response(fg.rss_str(pretty=True).decode("utf-8"), mimetype="application/rss+xml")
500
+
501
+
502
+ def serve_rss(blog):
503
+ """
504
+ Here we just serve static RSS files :)
505
+ """
506
+ return send_file(f"{g.mainpath}/static/feed/{blog.name}.xml")
@@ -1,6 +1,7 @@
1
1
  from flask import Blueprint, render_template, redirect, request, g, abort, Response
2
- from openwrite.utils.models import Blog, Post, User, View, Like, Page
2
+ from openwrite.utils.models import Blog, Post, User, View, Like, Page, Settings, Tag, PostTag
3
3
  from openwrite.utils.helpers import sanitize_html, gen_link, safe_css, send_activity, is_html, get_themes
4
+ from openwrite.utils.tags import parse_tags_from_input, update_post_tags, get_post_tags
4
5
  from openwrite.routes.blog import create_activity_object, create_person_object
5
6
  from openwrite.utils.valkey_client import valkey_client
6
7
  import requests
@@ -17,6 +18,7 @@ from user_agents import parse
17
18
  import re
18
19
  import csv
19
20
  from io import StringIO
21
+ from feedgen.feed import FeedGenerator
20
22
 
21
23
  dashboard_bp = Blueprint("dashboard", __name__)
22
24
 
@@ -277,6 +279,14 @@ def new_post(name):
277
279
  isdraft=request.form.get('draft', '0')
278
280
  )
279
281
  g.db.add(post)
282
+ g.db.flush() # Get the post ID
283
+
284
+ # Handle tags
285
+ tags_input = request.form.get('tags', '').strip()
286
+ if tags_input:
287
+ tag_names = parse_tags_from_input(tags_input)
288
+ update_post_tags(g.db, post, tag_names)
289
+
280
290
  g.db.commit()
281
291
 
282
292
  if blog.access == "domain":
@@ -308,7 +318,8 @@ def new_post(name):
308
318
  valkey_client.queue_federation_activity(activity, blog.id, blog.priv_key, blog_url, f"{actor}/inbox")
309
319
  else:
310
320
  send_activity(activity, blog.priv_key, blog_url, f"{actor}/inbox")
311
-
321
+
322
+ generate_rss(blog.name)
312
323
  return redirect(f"/dashboard/edit/{blog.name}")
313
324
 
314
325
  @dashboard_bp.route("/dashboard/preview", methods=['POST'])
@@ -342,11 +353,18 @@ def blog_preview():
342
353
  abort(404)
343
354
 
344
355
  homepage = g.db.query(Page).filter(Page.blog == blog.id, Page.url == "").first()
356
+
357
+ # Process content to handle {posts} patterns with tags
358
+ content = homepage.content_html
359
+ if '{posts}' in content:
360
+ # Replace all {posts} patterns (including {posts:#tags}) with placeholder
361
+ content = re.sub(r'{posts[^}]*}', '{posts}', content)
362
+
345
363
  data = {
346
364
  'title': request.form.get('title'),
347
365
  'css': request.form.get('css'),
348
366
  'icon': request.form.get('icon'),
349
- 'content': homepage.content_html,
367
+ 'content': content,
350
368
  'theme': request.form.get('theme')
351
369
  }
352
370
 
@@ -364,7 +382,9 @@ def edit_post(blog, post):
364
382
 
365
383
  e_post = g.db.query(Post).filter_by(link=post).first()
366
384
  if request.method == "GET":
367
- return render_template("new_post.html", blog=blog_obj, post=e_post)
385
+ # Get existing tags for the post
386
+ existing_tags = get_post_tags(g.db, e_post) if e_post else []
387
+ return render_template("new_post.html", blog=blog_obj, post=e_post, existing_tags=existing_tags)
368
388
 
369
389
  p = g.db.query(Post).filter_by(link=post, blog=blog_obj.id).first()
370
390
  if not p:
@@ -394,6 +414,12 @@ def edit_post(blog, post):
394
414
  else:
395
415
  p.updated = now
396
416
  p.isdraft = request.form.get("draft", "0")
417
+
418
+ # Handle tags
419
+ tags_input = request.form.get('tags', '').strip()
420
+ tag_names = parse_tags_from_input(tags_input)
421
+ update_post_tags(g.db, p, tag_names)
422
+
397
423
  g.db.commit()
398
424
 
399
425
  if blog_obj.access == "domain":
@@ -425,6 +451,7 @@ def edit_post(blog, post):
425
451
  else:
426
452
  send_activity(update_activity, blog_obj.priv_key, blog_url, f"{actor}/inbox")
427
453
 
454
+ generate_rss(blog)
428
455
  return redirect(f"/dashboard/edit/{blog}")
429
456
 
430
457
  @dashboard_bp.route("/dashboard/edit/<blog>/<post>/delete")
@@ -475,6 +502,7 @@ def delete_post(blog, post):
475
502
  g.db.delete(p)
476
503
  g.db.commit()
477
504
 
505
+ generate_rss(blog)
478
506
  return redirect(f"/dashboard/edit/{blog_obj.name}")
479
507
 
480
508
 
@@ -535,6 +563,7 @@ def migrate():
535
563
  g.db.add(new_post)
536
564
  g.db.commit()
537
565
 
566
+ generate_rss(blog.name)
538
567
  return "ok", 200
539
568
 
540
569
  @dashboard_bp.route("/dashboard/stats/<blog>")
@@ -675,9 +704,15 @@ def page_preview():
675
704
  if g.user is None:
676
705
  return redirect("/")
677
706
 
707
+ # Process content to handle {posts} patterns with tags
708
+ content = sanitize_html(request.form.get('content'))
709
+ if '{posts}' in content:
710
+ # Replace all {posts} patterns (including {posts:#tags}) with placeholder
711
+ content = re.sub(r'{posts[^}]*}', '{posts}', content)
712
+
678
713
  data = {
679
714
  'name': request.form.get('name'),
680
- 'content': sanitize_html(request.form.get('content')),
715
+ 'content': content,
681
716
  'blog_title': request.form.get('blog_title'),
682
717
  'blog_name': request.form.get('blog_name'),
683
718
  'theme': request.form.get('theme')
@@ -744,3 +779,39 @@ def export_posts(blog):
744
779
  'Content-Disposition': f'attachment; filename={blog}_posts.csv'
745
780
  }
746
781
  )
782
+
783
+ def generate_rss(blog):
784
+ """
785
+ Function to generate new static RSS feeds
786
+ """
787
+ b = g.db.query(Blog).filter_by(name=blog).first()
788
+ fg = FeedGenerator()
789
+ fg.ttl(5)
790
+ fg.logo("https://openwrite.io/static/logo.png")
791
+ fg.image(url="https://openwrite.io/static/favicon.png")
792
+ fg.title(b.title)
793
+
794
+ if b.access == "domain":
795
+ blog_url = f"https://{b.name}.{g.main_domain}"
796
+ post_base_url = blog_url
797
+ else:
798
+ blog_url = f"https://{g.main_domain}/b/{b.name}"
799
+ post_base_url = blog_url
800
+
801
+ fg.link(href=blog_url, rel="alternate")
802
+ soup = BeautifulSoup(b.description_html, "html.parser")
803
+ fg.description(soup.get_text())
804
+
805
+ posts = g.db.query(Post).filter(Post.blog == b.id, Post.isdraft == "0").limit(10).all()
806
+ for p in posts:
807
+ soup = BeautifulSoup(p.content_html, "html.parser")
808
+ content = soup.get_text()
809
+ fe = fg.add_entry()
810
+ fe.title(p.title)
811
+ fe.link(href=f"{post_base_url}/{p.link}")
812
+ fe.description(" ".join(content.split(" ")[:30]) + "...", True)
813
+ fe.content(content=p.content_html, type="CDATA")
814
+ fe.published(p.date.replace(tzinfo=timezone.utc))
815
+
816
+ rssfeed = fg.rss_str(pretty=True)
817
+ fg.rss_file(f"{g.mainpath}/static/feed/{b.name}.xml")
@@ -3,7 +3,9 @@ import os
3
3
  import json
4
4
  import datetime
5
5
  import requests
6
- from openwrite.utils.models import Post, Blog, Home, Settings, Page, Like
6
+ import re
7
+ from openwrite.utils.models import Blog, Post, User, View, Like, Page, Settings, Home
8
+ from openwrite.utils.tags import parse_tags_from_posts_syntax, get_posts_by_tags
7
9
  from openwrite.routes.blog import create_person_object, is_activitypub_request
8
10
  import time
9
11
  from openwrite import start_time
@@ -24,7 +26,6 @@ def index():
24
26
  elif g.mode == "single":
25
27
  blog = g.db.query(Blog).filter_by(name="default").first()
26
28
 
27
- # Check for ActivityPub request
28
29
  if is_activitypub_request():
29
30
  blog_url = f"https://{g.main_domain}"
30
31
  person = create_person_object(blog, blog_url)
@@ -36,11 +37,33 @@ def index():
36
37
  blog.url = f"http://{g.main_domain}"
37
38
  page = g.db.query(Page).filter_by(blog=blog.id, url="").first()
38
39
  pages = g.db.query(Page).filter_by(blog=blog.id).all()
40
+
39
41
  if '{posts}' in page.content_raw:
40
- posts = g.db.query(Post).filter_by(blog=blog.id).order_by(desc(Post.id)).all()
41
- return render_template('blog.html', blog=blog, page=page, posts=posts, pages=pages)
42
- posts = g.db.query(Post).filter_by(blog=blog.id).all()
43
- return render_template('blog.html', blog=blog, posts=posts, page=page, pages=pages)
42
+ posts_matches = re.findall(r'{posts[^}]*}', page.content_raw)
43
+ if posts_matches:
44
+ posts_syntax = posts_matches[0]
45
+ tag_names = parse_tags_from_posts_syntax(posts_syntax)
46
+ posts = get_posts_by_tags(g.db, blog.id, tag_names)
47
+
48
+ posts_html = ""
49
+ for post in posts:
50
+ if g.mode == "multi":
51
+ post_url = f"{blog.url}/{post.link}"
52
+ else:
53
+ post_url = f"http://{g.main_domain}/p/{post.link}"
54
+ posts_html += f'<div class="postlist_item"><i class="datetime">{post.date}</i> - <a href="{post_url}">{post.title}</a></div>'
55
+
56
+ processed_content = page.content_html
57
+ for match in posts_matches:
58
+ processed_content = processed_content.replace(match, posts_html)
59
+ else:
60
+ posts = g.db.query(Post).filter_by(blog=blog.id, isdraft='0').order_by(desc(Post.id)).all()
61
+ processed_content = page.content_html
62
+
63
+ return render_template('blog.html', blog=blog, page=page, posts=posts, pages=pages, processed_content=processed_content)
64
+
65
+ posts = g.db.query(Post).filter_by(blog=blog.id, isdraft='0').all()
66
+ return render_template('blog.html', blog=blog, posts=posts, page=page, pages=pages, processed_content=page.content_html)
44
67
 
45
68
  @main_bp.route("/set-lang/<lang_code>")
46
69
  def set_lang(lang_code):
File without changes
@@ -0,0 +1,12 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>Kierunkowy74</title><link>https://74.openwrite.io</link><description>x</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>Kierunkowy74</title><link>https://74.openwrite.io</link></image><lastBuildDate>Sat, 13 Sep 2025 16:34:24 +0000</lastBuildDate><ttl>5</ttl><item><title>Pierwsze wrażenia na openwrite</title><link>https://74.openwrite.io/pierwsze-wrazenia-na-openwrite</link><description>Ponieważ cały FTdL jest teraz nieczynny (a w związku z tym również i polskie WriteFreely) nastąpiła pora sprawdzić świeżo powstałe openwrite.
3
+ Muszę zmartwić tych, którzy chcieliby zaobserwować blog przez np....</description><content:encoded><![CDATA[<p><del>Ponieważ cały FTdL jest teraz nieczynny (a w związku z tym również i polskie WriteFreely)</del> nastąpiła pora sprawdzić świeżo powstałe openwrite. </p>
4
+ <p><del>Muszę zmartwić tych, którzy chcieliby zaobserwować blog przez np. Mastodona, bo <strong>federacji na tę chwilę jeszcze nie ma</strong></del> federacja jest, zaobserwujesz mnie z np. Mastodona jako @74@openwrite.io . Tak, są <em>lajki</em> - ale <del>skoro openwrite nie jest jeszcze fedi, to</del> ten wpis można zalajkować… z ulicy.</p>
5
+ <p>Różnic z WriteFreely jest więcej. openwrite nie jest tak minimalistyczny jak jego rywal i wiele rzecz pozwala po prostu wyklikać. <img src="https://openwrite.b-cdn.net/cf1167fc770ab887cdd2f26a4466ed5c.png" alt="Interfejs openwrite"></p>
6
+ <p>Są przyciski do wstawiania formatowania Markdown. Można normalnie zamieścić obrazek, który będzie znajdował się na openwrite. (czyli nie musisz jak na WriteFreely korzystać z serwisów zewnętrznych do podlinkowania obrazka) Jest możliwość by dołączyć informacje o autorze (nazwa użytkownika). Jest podgląd.</p>
7
+ <p>Można sobie wyklikać motyw lub zrobić własny CSS. Są dostępne statystyki, które są bardziej rozbudowane niż na WriteFreely. Można zobaczyć sobie liczbę wyświetleń na wykresie, w tym według przeglądarki i systemu operacyjnego.</p>
8
+ <h3>Czego brakuje?</h3>
9
+ <p>Na pewno federacji, chociaż (jak wynika z opisu na GitHubie) powinna być. Szkiców i autozapisu. Wpisów publikowanych według harmonogramu (ale wypadałoby wspomnieć, że na WriteFreely takie wpisy nie federują i ślamazarny rozwój WF nie daje wielkich nadziei na zmianę). W końcu zaś biuletynów, tudzież subskrypcji mailem.</p>
10
+ <p>A, i przy okazji zauważyłem jedną usterkę. Dodanie przyciskiem np. <strong>pogrubienia</strong>, <em>kursywy</em> lub <u>przekreślenia</u> dodaje znaczniki początkowy i końcowy, ale umieszcza kursor po nich, a nie pomiędzy nimi. Edytor Markdown w np. PieFedzie robi to lepiej. (edycja: druga - da się dodać wpis bez tytułu. W Odkrywaj wpis będzie bez tytułu i nie będzie dało się weń kliknąć xd)</p>
11
+ <p>Serwis na pewno jest ciekawy. No i chyba tyle.</p>
12
+ <h1>Czy ten wpis jest widoczny na Gemini?</h1>]]></content:encoded><pubDate>Fri, 18 Jul 2025 11:07:52 +0000</pubDate></item></channel></rss>
@@ -0,0 +1,2 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>A</title><link>https://openwrite.io/b/a</link><description>Hello there! 👋You can edit your blog description in dashboard</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>A</title><link>https://openwrite.io/b/a</link></image><lastBuildDate>Sat, 13 Sep 2025 16:34:24 +0000</lastBuildDate><ttl>5</ttl></channel></rss>
@@ -0,0 +1,12 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>4nim3 typ3 sh1ft</title><link>https://anim3sh1t.openwrite.io</link><description>x</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>4nim3 typ3 sh1ft</title><link>https://anim3sh1t.openwrite.io</link></image><lastBuildDate>Sat, 13 Sep 2025 16:34:24 +0000</lastBuildDate><ttl>5</ttl><item><title>An1m3 sH1t</title><link>https://anim3sh1t.openwrite.io/an1m3-sh1t</link><description>
3
+ M3ga like click this all links:
4
+ https://ptoszek.pl/
5
+ https://www.youtube.com/watch?v=dQw4w9WgXcQ
6
+ https://sekurwaposzukaj.pl/
7
+ &lt;33 ...</description><content:encoded><![CDATA[<p><img src="https://openwrite.b-cdn.net/01a494ce9056e43757ca5161ba8a496c.png" alt="alt text">
8
+ M3ga like click this all links:
9
+ https://ptoszek.pl/
10
+ https://www.youtube.com/watch?v=dQw4w9WgXcQ
11
+ https://sekurwaposzukaj.pl/
12
+ &lt;33 </p>]]></content:encoded><pubDate>Thu, 24 Jul 2025 13:24:35 +0000</pubDate></item></channel></rss>
@@ -0,0 +1,2 @@
1
+ <?xml version='1.0' encoding='UTF-8'?>
2
+ <rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><title>B</title><link>https://openwrite.io/b/b</link><description>Hello there! 👋You can edit your blog description in dashboard</description><docs>http://www.rssboard.org/rss-specification</docs><generator>python-feedgen</generator><image><url>https://openwrite.io/static/favicon.png</url><title>B</title><link>https://openwrite.io/b/b</link></image><lastBuildDate>Sat, 13 Sep 2025 16:34:24 +0000</lastBuildDate><ttl>5</ttl></channel></rss>