render-engine 2025.10.1a2__tar.gz → 2025.11.1__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 (123) hide show
  1. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.gitignore +2 -0
  2. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/PKG-INFO +1 -1
  3. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/cli.md +6 -0
  4. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/collection.md +2 -1
  5. render_engine-2025.11.1/docs/docs/content_manager.md +67 -0
  6. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/parsers.md +7 -3
  7. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/templates.md +7 -0
  8. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/mkdocs.yml +4 -3
  9. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/pyproject.toml +1 -1
  10. render_engine-2025.11.1/src/render_engine/__version__.py +34 -0
  11. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/collection.py +38 -11
  12. render_engine-2025.11.1/src/render_engine/content_managers/__init__.py +7 -0
  13. render_engine-2025.11.1/src/render_engine/content_managers/base_content_manager.py +22 -0
  14. render_engine-2025.11.1/src/render_engine/content_managers/file_content_manager.py +57 -0
  15. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/site.py +11 -1
  16. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/site_map.py +1 -1
  17. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine.egg-info/PKG-INFO +1 -1
  18. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine.egg-info/SOURCES.txt +5 -0
  19. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_collections.py +56 -0
  20. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_site_map.py +30 -30
  21. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.all-contributorsrc +0 -0
  22. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.devcontainer/Dockerfile +0 -0
  23. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.devcontainer/devcontainer.json +0 -0
  24. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.devcontainer/setup.sh +0 -0
  25. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/FUNDING.yml +0 -0
  26. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/ISSUE_TEMPLATE/config.yaml +0 -0
  27. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/ISSUE_TEMPLATE/form_issue_template.yml +0 -0
  28. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/dependabot-bot.yml +0 -0
  30. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/dependabot.yml +0 -0
  31. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/labeler.yml +0 -0
  32. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/release.yml +0 -0
  33. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/workflows/devcontainer-ci.yml +0 -0
  34. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/workflows/labeler.yml +0 -0
  35. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/workflows/lint.yml +0 -0
  36. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/workflows/publish.yml +0 -0
  37. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/workflows/scorecard.yml +0 -0
  38. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.github/workflows/test.yml +0 -0
  39. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.markdownlint.json +0 -0
  40. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.pre-commit-config.yaml +0 -0
  41. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.readthedocs.yml +0 -0
  42. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/.vscode/tasks.json +0 -0
  43. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/CONTRIBUTING.md +0 -0
  44. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/README.md +0 -0
  45. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/SECURITY.md +0 -0
  46. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/.markdownlint.json +0 -0
  47. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/archive.md +0 -0
  48. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/assets/create environment vs code.gif +0 -0
  49. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/assets/create-app-help.png +0 -0
  50. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/assets/create-codespace.gif +0 -0
  51. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/assets/launching a dev container.gif +0 -0
  52. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/assets/render-engine-init-help.png +0 -0
  53. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/assets/render-engine-init.png +0 -0
  54. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/contributing/CODE_OF_CONDUCT.md +0 -0
  55. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/contributing/CONTRIBUTING.md +0 -0
  56. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/contributing/environment_setup.md +0 -0
  57. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/custom_collections.md +0 -0
  58. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/feeds.md +0 -0
  59. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/building-your-site.md +0 -0
  60. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/creating-a-collection.md +0 -0
  61. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/creating-a-page.md +0 -0
  62. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/creating-your-app.md +0 -0
  63. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/getting-started.md +0 -0
  64. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/installation.md +0 -0
  65. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/getting-started/layout.md +0 -0
  66. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/index.md +0 -0
  67. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/page.md +0 -0
  68. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/plugins.md +0 -0
  69. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/site.md +0 -0
  70. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/site_map.md +0 -0
  71. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/docs/theme_management.md +0 -0
  72. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/docs/requirements.txt +0 -0
  73. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/noxfile.py +0 -0
  74. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/requirements.txt +0 -0
  75. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/setup.cfg +0 -0
  76. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/.gitignore +0 -0
  77. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/__init__.py +0 -0
  78. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/__main__.py +0 -0
  79. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/_base_object.py +0 -0
  80. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/archive.py +0 -0
  81. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/blog.py +0 -0
  82. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/engine.py +0 -0
  83. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/extras/__init__.py +0 -0
  84. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/feeds.py +0 -0
  85. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/hookspecs.py +0 -0
  86. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/links.py +0 -0
  87. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/page.py +0 -0
  88. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/parsers/markdown.py +0 -0
  89. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/plugins.py +0 -0
  90. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/py.typed +0 -0
  91. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/__init__.py +0 -0
  92. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/archive.html +0 -0
  93. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/base.html +0 -0
  94. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/base_collection_path.md +0 -0
  95. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/base_templates/_archive.html +0 -0
  96. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/base_templates/_base.html +0 -0
  97. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/base_templates/_page.html +0 -0
  98. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/components/footer.html +0 -0
  99. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/components/page_title.html +0 -0
  100. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/page.html +0 -0
  101. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/rss2.0.xml +0 -0
  102. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/rss2.0_items.xml +0 -0
  103. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/sitemap.xml +0 -0
  104. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/render_engine_templates/sitemap_item.xml +0 -0
  105. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/themes.py +0 -0
  106. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine/utils/__init__.py +0 -0
  107. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine.egg-info/dependency_links.txt +0 -0
  108. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine.egg-info/requires.txt +0 -0
  109. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/src/render_engine.egg-info/top_level.txt +0 -0
  110. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/conftest.py +0 -0
  111. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_archive.py +0 -0
  112. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_base_object.py +0 -0
  113. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_blog.py +0 -0
  114. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_engine.py +0 -0
  115. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_feeds/conftest_feed.py +0 -0
  116. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_feeds/test_feeds.py +0 -0
  117. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_page.py +0 -0
  118. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_parsers_remove_2024_3_1.py +0 -0
  119. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_plugins.py +0 -0
  120. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_site.py +0 -0
  121. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_templates/test_base_html.py +0 -0
  122. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/tests/test_theme_manager.py +0 -0
  123. {render_engine-2025.10.1a2 → render_engine-2025.11.1}/uv.lock +0 -0
@@ -166,3 +166,5 @@ untitled-site/
166
166
 
167
167
  .DS_Store
168
168
  .python-version
169
+
170
+ __version__.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: render_engine
3
- Version: 2025.10.1a2
3
+ Version: 2025.11.1
4
4
  Summary: A Flexible Static Site Generator for Python
5
5
  Project-URL: homepage, https://github.com/render-engine/render-engine/
6
6
  Project-URL: repository, https://github.com/render-engine/render-engine/
@@ -7,6 +7,12 @@ tags: ["render-engine", "cli", "site-setup", "build", "serve"]
7
7
 
8
8
  Render Engine comes with a CLI that can be used to create, build, and serve your site.
9
9
 
10
+ > !!! Note
11
+ The CLI is no longer a part of the Render Engine package and must be installed
12
+ separately. To install run `pip install render-engine-cli`. Render Engine can be
13
+ installed together with the CLI as well using
14
+ `pip install render-engine[cli]`.
15
+
10
16
  ## pyproject.toml Configuration
11
17
 
12
18
  The Render Engine CLI can be configured through your `pyproject.toml` file to set default values and avoid repetitive command-line arguments.
@@ -46,7 +46,8 @@ sort_reverse: bool = False
46
46
  title: str
47
47
  template: str | None
48
48
  archive_template str | None: The template to use for the archive pages.
49
- skip_site_map: bool = False: Flag to indicate whether this Collection should not be included in the SiteMap.
49
+ ContentManager: type[ContentManager] | None = FileContentManager: The `ContentManager` to use.
50
+ content_manager_extras: dict[str, Any]: Configuration options to send to the `ContentManager` during instantiation.
50
51
  ```
51
52
 
52
53
  ## Attributes
@@ -0,0 +1,67 @@
1
+ ---
2
+ title: "Enhancing Functionality with Content Managers"
3
+ description: "Guide to using creating Content Managers to use alternate content storage systems."
4
+ date: August 22, 2024
5
+ tags: ["content_manager", "render-engine", "customization"]
6
+ ---
7
+
8
+ Content Managers are a way to control how and where your content is stored.
9
+
10
+ ## Introduction
11
+
12
+ Render Engine `Collection` uses a `ContentManager` to manage the storage of content for site generation. By default
13
+ content is stored in the file system with each piece of content existing in a discrete file. With an alternate
14
+ `ContentManager` the content can be stored in a database, JSON file, or other alternate data store.
15
+
16
+ ## Selecting a `ContentManager`
17
+
18
+ The `ContentManager` for a given [`Collection`](collection.md) is controlled by the `ContentManager` attribute. When
19
+ the class is instantiated the `ContentManager` is also instantiated with any `content_manager_extras` being passed
20
+ as arguments. To access the `ContentManager` of a given `Collection` use the `content_manager` attribute.
21
+
22
+ ## Creating a `ContentManager`
23
+
24
+ To create a `ContentManager` create a sub-class of `ContentManager` that implements the following methods:
25
+
26
+ ```python
27
+ @property
28
+ @abstractmethod
29
+ def pages(self) -> Iterable:
30
+ """The Page objects managed by the content manager"""
31
+ ...
32
+
33
+ @abstractmethod
34
+ def create_entry(self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None):
35
+ """Create a new entry"""
36
+ ...
37
+ ```
38
+
39
+ ### `pages`
40
+
41
+ The `pages` property is how the `Collection` accesses its content. It is a method that _must_ be implemented by
42
+ every `ContentManager`. An example `pages` implementation (from the
43
+ [`FileContentManger`](https://github.com/render-engine/render-engine/blob/main/src/render_engine/content_managers/file_content_manager.py)) is:
44
+
45
+ ```python
46
+ @property
47
+ def pages(self) -> Iterable:
48
+ if self._pages is None:
49
+ self._pages = [self.collection.get_page(page) for page in self.iter_content_path()]
50
+ yield from self._pages
51
+ ```
52
+
53
+ ### `create_entry`
54
+
55
+ The `create_entry` method is used by the Render Engine CLI tool to add new entries. It is responsible for adding
56
+ the new entry to the datastore and, if an `editor` is specified, giving the user the ability to edit the new entry
57
+ prior to committing the entry to the datastore.
58
+
59
+ #### Arguments
60
+
61
+ - `filepath: Path`: The path on the filesystem to store the new entry.
62
+ - `editor: str`: The text editor to open for editing the new entry.
63
+ - `content: str`: The initial content for the new entry.
64
+ - `metadata: dict`: The metadata for the new entry.
65
+
66
+ !!! Note
67
+ Not every `ContentManager` actually needs all the arguments that are passed.
@@ -51,7 +51,7 @@ my_page._render_content()
51
51
  In many cases, you will want to create rich content. The `MarkdownPageParser`. You can also pass in attributes to the page via frontmatter at the top of the markdown file.
52
52
 
53
53
  ```python
54
- from render_engine.parsers.base_parsers import BasePageParser
54
+ from render_engine_markdown import MarkdownPageParser
55
55
  from render_engine.page import Page
56
56
 
57
57
  base_markdown = """
@@ -63,8 +63,8 @@ This is **dynamic** content
63
63
  """
64
64
 
65
65
  class MyPage(Page):
66
- parser = BasePageParser
67
- content = base_text
66
+ parser = MarkdownPageParser
67
+ content = base_markdown
68
68
 
69
69
  my_page = MyPage()
70
70
  my_page.title
@@ -78,6 +78,10 @@ my_page._render_content()
78
78
 
79
79
  ```
80
80
 
81
+ > !!! Note
82
+ `MarkdownPageParser` is found in the `render_engine_markdown` package.
83
+ To install run `pip install render_engine_markdown`.
84
+
81
85
  ## Creating Custom Parsers
82
86
 
83
87
  You can create custom parsers.
@@ -95,3 +95,10 @@ By default this will format to the site's `DATETIME_FORMAT` (default: `"%d %b %Y
95
95
  {{ page.date | format_datetime("%B %d, %Y") }} --> January 01, 2000
96
96
 
97
97
  ```
98
+
99
+ ## Site Map
100
+
101
+ Render builds a site map prior to rendering the content. The contents of this
102
+ site map are available to be used in both Page objects (see
103
+ [Page object documentation](/page/#accessing-urls-for-other-pages-in-the-site-from-within-the-page-content).)
104
+ Please see the [site map documentation](/site_map.html) for more information.
@@ -18,20 +18,21 @@ nav:
18
18
  - contributing/CONTRIBUTING.md
19
19
  - Components:
20
20
  - Page Objects: page.md
21
- - Collection:
21
+ - Collection Objects:
22
22
  - Collection: collection.md
23
23
  - Custom Collection: custom_collections.md
24
24
  - Archive: archive.md
25
25
  - RSS Feed: feeds.md
26
- - Site:
27
- - Site: site.md
26
+ - Site Objects: site.md
28
27
  - Extending Render Engine:
29
28
  - Plugins: plugins.md
29
+ - Content Managers: content_manager.md
30
30
  - Parsers: parsers.md
31
31
  - CLI: cli.md
32
32
  - Templates and Themes:
33
33
  - Templates: templates.md
34
34
  - ThemeManager: theme_management.md
35
+ - Site Map: site_map.md
35
36
 
36
37
  markdown_extensions:
37
38
  - toc
@@ -42,9 +42,9 @@ dev = [
42
42
  "watchfiles",
43
43
  ]
44
44
 
45
-
46
45
  [tool.setuptools_scm]
47
46
  local_scheme = "no-local-version"
47
+ version_file = "src/render_engine/__version__.py"
48
48
  # version_scheme = "python-simplified-semver"
49
49
 
50
50
  [project.urls]
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '2025.11.1'
32
+ __version_tuple__ = version_tuple = (2025, 11, 1)
33
+
34
+ __commit_id__ = commit_id = 'g3ef9c0acb'
@@ -6,10 +6,12 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  import dateutil.parser as dateparse
9
- from more_itertools import batched, flatten
9
+ from more_itertools import batched
10
10
  from render_engine_parser import BasePageParser
11
11
  from slugify import slugify
12
12
 
13
+ from render_engine.content_managers import ContentManager, FileContentManager
14
+
13
15
  from ._base_object import BaseObject
14
16
  from .archive import Archive
15
17
  from .feeds import RSSFeed
@@ -56,6 +58,9 @@ class Collection(BaseObject):
56
58
  title: str
57
59
  template: str | None
58
60
  archive_template str | None: The template to use for the archive pages.
61
+ ContentManager: type[ContentManager] | None = FileContentManager
62
+ content_manager: ContentManager
63
+ content_manager_extras: dict[str, Any]: kwargs to pass to the ContentManager when instantiating
59
64
 
60
65
  Methods:
61
66
 
@@ -84,6 +89,8 @@ class Collection(BaseObject):
84
89
  template_vars: dict[str, Any]
85
90
  template: str | None
86
91
  plugin_manager: PluginManager | None
92
+ ContentManager: type[ContentManager] | None = FileContentManager
93
+ content_manager_extras: dict[str, Any]
87
94
 
88
95
  def __init__(
89
96
  self,
@@ -102,9 +109,16 @@ class Collection(BaseObject):
102
109
  self.title = self._title
103
110
  self.template_vars = getattr(self, "template_vars", {})
104
111
 
105
- def iter_content_path(self):
106
- """Iterate through in the collection's content path."""
107
- return flatten([Path(self.content_path).glob(suffix) for suffix in self.include_suffixes])
112
+ cm_extras = {
113
+ "content_path": getattr(self, "content_path", None),
114
+ "include_suffixes": getattr(self, "include_suffixes", None),
115
+ "collection": self,
116
+ }
117
+ if hasattr(self, "content_manager_extras"):
118
+ cm_extras.update(self.content_manager_extras)
119
+ self.content_manager = self.ContentManager(**cm_extras)
120
+ if hasattr(self, "pages"):
121
+ self.content_manager.pages = self.pages
108
122
 
109
123
  def get_page(
110
124
  self,
@@ -116,8 +130,6 @@ class Collection(BaseObject):
116
130
  Parser=self.Parser,
117
131
  )
118
132
 
119
- if getattr(self, "_pm", None):
120
- _page.register_plugins(self.plugins, **self.plugin_settings)
121
133
  _page.parser_extras = getattr(self, "parser_extras", {})
122
134
  _page.routes = self.routes
123
135
  _page.template = getattr(self, "template", None)
@@ -162,7 +174,7 @@ class Collection(BaseObject):
162
174
  """
163
175
  try:
164
176
  return sorted(
165
- (page for page in self.__iter__()),
177
+ (page for page in self),
166
178
  key=self._sort_key(self.sort_by),
167
179
  reverse=self.sort_reverse,
168
180
  )
@@ -231,10 +243,7 @@ class Collection(BaseObject):
231
243
  return f"{__name__}"
232
244
 
233
245
  def __iter__(self):
234
- if not hasattr(self, "pages"):
235
- self.pages = [self.get_page(page) for page in self.iter_content_path()]
236
- for page in self.pages: # noqa: UP028
237
- yield page
246
+ yield from self.content_manager
238
247
 
239
248
  def _run_collection_plugins(self, site, hook_type: str):
240
249
  """
@@ -279,6 +288,24 @@ class Collection(BaseObject):
279
288
  feed.site = self.site
280
289
  feed.render(route="./", theme_manager=self.site.theme_manager)
281
290
 
291
+ def create_entry(
292
+ self, filepath: Path = None, editor: str = None, content: str = None, metadata: dict = None
293
+ ) -> str:
294
+ """
295
+ Create a new entry for the Collection
296
+
297
+ :param filepath: Path object for the new entry
298
+ :param editor: Editor to open to edit the entry.
299
+ :param content: Content for the new entry
300
+ :param metadata: Metadata for the new entry
301
+ """
302
+ context = copy.deepcopy(self._metadata_attrs())
303
+ if metadata:
304
+ context.update(metadata)
305
+ return self.content_manager.create_entry(
306
+ filepath=filepath, editor=editor, metadata=context, content=content or "Hello, world!"
307
+ )
308
+
282
309
 
283
310
  def render_archives(archive, **kwargs) -> list[Archive]:
284
311
  return [archive.render(pages=archive.pages, **kwargs) for archive in archive]
@@ -0,0 +1,7 @@
1
+ from .base_content_manager import ContentManager
2
+ from .file_content_manager import FileContentManager
3
+
4
+ __all__ = [
5
+ ContentManager,
6
+ FileContentManager,
7
+ ]
@@ -0,0 +1,22 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Generator, Iterable
3
+ from pathlib import Path
4
+
5
+
6
+ class ContentManager(ABC):
7
+ """Base ContentManager abstract class"""
8
+
9
+ @property
10
+ @abstractmethod
11
+ def pages(self) -> Iterable:
12
+ """The Page objects managed by the content manager"""
13
+ ...
14
+
15
+ def __iter__(self) -> Generator:
16
+ """Iterator for the ContentManager"""
17
+ yield from self.pages
18
+
19
+ @abstractmethod
20
+ def create_entry(self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None):
21
+ """Create a new entry"""
22
+ ...
@@ -0,0 +1,57 @@
1
+ import subprocess
2
+ from collections.abc import Iterable
3
+ from pathlib import Path
4
+
5
+ from more_itertools import flatten
6
+
7
+ from render_engine.content_managers import ContentManager
8
+
9
+
10
+ class FileContentManager(ContentManager):
11
+ """Content manager for content stored on the file system as individual files"""
12
+
13
+ def __init__(
14
+ self,
15
+ content_path: Path | str,
16
+ collection,
17
+ include_suffixes: Iterable[str] = ("*.md", "*.html"),
18
+ **kwargs,
19
+ ):
20
+ self.content_path = content_path
21
+ self.include_suffixes = include_suffixes
22
+ self.collection = collection
23
+ self._pages = None
24
+
25
+ def iter_content_path(self):
26
+ """Iterate through in the collection's content path."""
27
+ return flatten([Path(self.content_path).glob(suffix) for suffix in self.include_suffixes])
28
+
29
+ @property
30
+ def pages(self) -> Iterable:
31
+ if self._pages is None:
32
+ self._pages = [self.collection.get_page(page) for page in self.iter_content_path()]
33
+ yield from self._pages
34
+
35
+ @pages.setter
36
+ def pages(self, value: Iterable):
37
+ self._pages = value
38
+
39
+ def create_entry(
40
+ self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None
41
+ ) -> str:
42
+ """
43
+ Create a new entry for the Collection
44
+
45
+ :param filepath: Path object for the new entry
46
+ :param editor: Editor to open to edit the entry.
47
+ :param content: The content for the entry
48
+ :param metadata: Metadata for the new entry
49
+ """
50
+ if not filepath:
51
+ raise ValueError("filepath needs to be specified.")
52
+
53
+ parsed_content = self.collection.Parser.create_entry(content=content, **metadata)
54
+ filepath.write_text(parsed_content)
55
+ if editor:
56
+ subprocess.run([editor, filepath])
57
+ return f"New entry created at {filepath} ."
@@ -3,6 +3,7 @@ import logging
3
3
  from collections import defaultdict
4
4
  from pathlib import Path
5
5
 
6
+ import rich
6
7
  from jinja2 import FileSystemLoader, PrefixLoader
7
8
  from rich.progress import Progress
8
9
 
@@ -13,6 +14,12 @@ from .plugins import PluginManager, handle_plugin_registration
13
14
  from .site_map import SiteMap
14
15
  from .themes import Theme, ThemeManager
15
16
 
17
+ try:
18
+ # Get the RE version for display. If it's not set it means we're working locally.
19
+ from render_engine.__version__ import __version__ as re_version
20
+ except ImportError:
21
+ re_version = "development"
22
+
16
23
 
17
24
  class Site:
18
25
  """
@@ -251,7 +258,10 @@ class Site:
251
258
  You can choose to call it manually in your file or
252
259
  use the CLI command [`render-engine build`][src.render_engine.cli.build]
253
260
  """
254
-
261
+ rich.print(
262
+ f"[green]Building {repr(self.site_vars.get('SITE_TITLE', 'your site'))} "
263
+ f"with Render Engine version {re_version}"
264
+ )
255
265
  with Progress() as progress:
256
266
  task_site_map = progress.add_task("Generating site map", total=1)
257
267
  self._site_map = SiteMap(self.route_list, self.site_vars.get("SITE_URL", ""))
@@ -22,7 +22,7 @@ class SiteMapEntry:
22
22
  self._route = f"/{route.lstrip('/')}/{self.path_name}" if from_collection else f"/{self.path_name}"
23
23
  self.entries = list()
24
24
  case Collection():
25
- self._route = f"/{route.lstrip('/')}"
25
+ self._route = f"/{entry.routes[0].lstrip('/')}"
26
26
  self.entries = [
27
27
  SiteMapEntry(collection_entry, self._route, from_collection=True) for collection_entry in entry
28
28
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: render_engine
3
- Version: 2025.10.1a2
3
+ Version: 2025.11.1
4
4
  Summary: A Flexible Static Site Generator for Python
5
5
  Project-URL: homepage, https://github.com/render-engine/render-engine/
6
6
  Project-URL: repository, https://github.com/render-engine/render-engine/
@@ -34,6 +34,7 @@ docs/requirements.txt
34
34
  docs/docs/archive.md
35
35
  docs/docs/cli.md
36
36
  docs/docs/collection.md
37
+ docs/docs/content_manager.md
37
38
  docs/docs/custom_collections.md
38
39
  docs/docs/feeds.md
39
40
  docs/docs/index.md
@@ -63,6 +64,7 @@ docs/docs/getting-started/layout.md
63
64
  src/render_engine/.gitignore
64
65
  src/render_engine/__init__.py
65
66
  src/render_engine/__main__.py
67
+ src/render_engine/__version__.py
66
68
  src/render_engine/_base_object.py
67
69
  src/render_engine/archive.py
68
70
  src/render_engine/blog.py
@@ -82,6 +84,9 @@ src/render_engine.egg-info/SOURCES.txt
82
84
  src/render_engine.egg-info/dependency_links.txt
83
85
  src/render_engine.egg-info/requires.txt
84
86
  src/render_engine.egg-info/top_level.txt
87
+ src/render_engine/content_managers/__init__.py
88
+ src/render_engine/content_managers/base_content_manager.py
89
+ src/render_engine/content_managers/file_content_manager.py
85
90
  src/render_engine/extras/__init__.py
86
91
  src/render_engine/parsers/markdown.py
87
92
  src/render_engine/render_engine_templates/__init__.py
@@ -1,4 +1,5 @@
1
1
  import pathlib
2
+ import textwrap
2
3
 
3
4
  import pluggy
4
5
  import pytest
@@ -360,3 +361,58 @@ def test_collection_custom_sort_by_list_with_date():
360
361
 
361
362
  # Verify the sorted order by checking custom_sort_content values
362
363
  assert [page.custom_sort_content for page in sorted_pages] == [page.custom_sort_content for page in expected_pages]
364
+
365
+
366
+ @pytest.mark.parametrize(
367
+ "content, context, expected",
368
+ [
369
+ (
370
+ "",
371
+ {"title": "title"},
372
+ textwrap.dedent("""---
373
+ title: title
374
+ ---
375
+
376
+ Hello, world!"""),
377
+ ),
378
+ (
379
+ "test",
380
+ {},
381
+ textwrap.dedent("""---
382
+ title: Untitled Entry
383
+ ---
384
+
385
+ test"""),
386
+ ),
387
+ (
388
+ "",
389
+ {},
390
+ textwrap.dedent("""---
391
+ title: Untitled Entry
392
+ ---
393
+
394
+ Hello, world!"""),
395
+ ),
396
+ ],
397
+ )
398
+ def test_create_entry(tmp_path: pathlib.Path, content, context, expected):
399
+ """Test the create_entry method"""
400
+ tmp_dir = tmp_path / "content"
401
+ tmp_dir.mkdir()
402
+
403
+ class BasicCollection(Collection):
404
+ content_path = tmp_dir.absolute()
405
+
406
+ filename = "test.md"
407
+ filepath = tmp_dir / filename
408
+ assert str(filepath) in BasicCollection().create_entry(
409
+ filepath=filepath, editor=None, content=content, metadata=context
410
+ )
411
+ assert filepath.read_text().strip() == expected
412
+
413
+
414
+ def test_create_entry_no_filename():
415
+ """Test create_entry with no filename raises an exception"""
416
+ # Since the base Collection object uses a FileContentManager
417
+ with pytest.raises(ValueError):
418
+ Collection().create_entry()
@@ -60,7 +60,7 @@ def site(tmp_path_factory):
60
60
  for collection in ["coll1", "coll2"]:
61
61
 
62
62
  class MyColl(Collection):
63
- routes = [collection]
63
+ routes = [f"{collection}-route"]
64
64
  pages = coll_pages[collection]
65
65
 
66
66
  collection_class = MyColl
@@ -82,19 +82,19 @@ def test_site_map_to_html(site):
82
82
  sm = SiteMap(site.route_list, "")
83
83
  assert sm.html == (
84
84
  "<ul>\n"
85
- '\t<li><a href="/coll1">coll1</a></li>\n'
85
+ '\t<li><a href="/coll1-route">coll1</a></li>\n'
86
86
  "\t<ul>\n"
87
- '\t\t<li><a href="/coll1/page0.html">coll1 -- Page 0</a></li>\n'
88
- '\t\t<li><a href="/coll1/page1.html">coll1 -- Page 1</a></li>\n'
89
- '\t\t<li><a href="/coll1/page2.html">coll1 -- Page 2</a></li>\n'
90
- '\t\t<li><a href="/coll1/page3.html">coll1 -- Page 3</a></li>\n'
87
+ '\t\t<li><a href="/coll1-route/page0.html">coll1 -- Page 0</a></li>\n'
88
+ '\t\t<li><a href="/coll1-route/page1.html">coll1 -- Page 1</a></li>\n'
89
+ '\t\t<li><a href="/coll1-route/page2.html">coll1 -- Page 2</a></li>\n'
90
+ '\t\t<li><a href="/coll1-route/page3.html">coll1 -- Page 3</a></li>\n'
91
91
  "\t</ul>\n"
92
- '\t<li><a href="/coll2">coll2</a></li>\n'
92
+ '\t<li><a href="/coll2-route">coll2</a></li>\n'
93
93
  "\t<ul>\n"
94
- '\t\t<li><a href="/coll2/page0.html">coll2 -- Page 0</a></li>\n'
95
- '\t\t<li><a href="/coll2/page1.html">coll2 -- Page 1</a></li>\n'
96
- '\t\t<li><a href="/coll2/page2.html">coll2 -- Page 2</a></li>\n'
97
- '\t\t<li><a href="/coll2/page3.html">coll2 -- Page 3</a></li>\n'
94
+ '\t\t<li><a href="/coll2-route/page0.html">coll2 -- Page 0</a></li>\n'
95
+ '\t\t<li><a href="/coll2-route/page1.html">coll2 -- Page 1</a></li>\n'
96
+ '\t\t<li><a href="/coll2-route/page2.html">coll2 -- Page 2</a></li>\n'
97
+ '\t\t<li><a href="/coll2-route/page3.html">coll2 -- Page 3</a></li>\n'
98
98
  "\t</ul>\n"
99
99
  '\t<li><a href="/page0.html">Page 0</a></li>\n'
100
100
  '\t<li><a href="/page1.html">Page 1</a></li>\n'
@@ -109,21 +109,21 @@ def test_site_map_to_html(site):
109
109
  ("page1", {}, "/page1.html"),
110
110
  ("page1", {"attr": "slug"}, "/page1.html"),
111
111
  ("page3", {"attr": "slug"}, None),
112
- ("page3", {"attr": "slug", "full_search": True}, "/coll1/page3.html"),
113
- ("page3", {"attr": "slug", "collection": "coll2"}, "/coll2/page3.html"),
112
+ ("page3", {"attr": "slug", "full_search": True}, "/coll1-route/page3.html"),
113
+ ("page3", {"attr": "slug", "collection": "coll2"}, "/coll2-route/page3.html"),
114
114
  ("page1.html", {"attr": "path_name"}, "/page1.html"),
115
115
  ("page3.html", {"attr": "path_name"}, None),
116
- ("page3.html", {"attr": "path_name", "full_search": True}, "/coll1/page3.html"),
117
- ("page3.html", {"attr": "path_name", "collection": "coll2"}, "/coll2/page3.html"),
116
+ ("page3.html", {"attr": "path_name", "full_search": True}, "/coll1-route/page3.html"),
117
+ ("page3.html", {"attr": "path_name", "collection": "coll2"}, "/coll2-route/page3.html"),
118
118
  ("page1", {"attr": "slug"}, "/page1.html"),
119
119
  ("page3", {"attr": "slug"}, None),
120
- ("page3", {"attr": "slug", "full_search": True}, "/coll1/page3.html"),
121
- ("page3", {"attr": "slug", "collection": "coll2"}, "/coll2/page3.html"),
120
+ ("page3", {"attr": "slug", "full_search": True}, "/coll1-route/page3.html"),
121
+ ("page3", {"attr": "slug", "collection": "coll2"}, "/coll2-route/page3.html"),
122
122
  ("Page 1", {"attr": "title"}, "/page1.html"),
123
123
  ("coll1 -- Page 3", {"attr": "title"}, None),
124
- ("coll1 -- Page 3", {"attr": "title", "full_search": True}, "/coll1/page3.html"),
124
+ ("coll1 -- Page 3", {"attr": "title", "full_search": True}, "/coll1-route/page3.html"),
125
125
  ("coll1 -- Page 3", {"attr": "title", "collection": "coll2"}, None),
126
- ("coll2 -- Page 3", {"attr": "title", "collection": "coll2"}, "/coll2/page3.html"),
126
+ ("coll2 -- Page 3", {"attr": "title", "collection": "coll2"}, "/coll2-route/page3.html"),
127
127
  ],
128
128
  )
129
129
  def test_site_map_search(site, value, params, expected):
@@ -137,7 +137,7 @@ def test_site_map_search(site, value, params, expected):
137
137
 
138
138
  def test_find_in_template(site):
139
139
  site.render()
140
- assert (site.output_path / "page1.html").read_text() == "Page 1\n/coll1/page0.html"
140
+ assert (site.output_path / "page1.html").read_text() == "Page 1\n/coll1-route/page0.html"
141
141
 
142
142
 
143
143
  def test_site_map_to_xml(site):
@@ -148,34 +148,34 @@ def test_site_map_to_xml(site):
148
148
  == """<?xml version="1.0" encoding="UTF-8"?>
149
149
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
150
150
  <url>
151
- <loc>http://localhost:8000/coll1</loc>
151
+ <loc>http://localhost:8000/coll1-route</loc>
152
152
  </url>
153
153
  <url>
154
- <loc>http://localhost:8000/coll1/page0.html</loc>
154
+ <loc>http://localhost:8000/coll1-route/page0.html</loc>
155
155
  </url>
156
156
  <url>
157
- <loc>http://localhost:8000/coll1/page1.html</loc>
157
+ <loc>http://localhost:8000/coll1-route/page1.html</loc>
158
158
  </url>
159
159
  <url>
160
- <loc>http://localhost:8000/coll1/page2.html</loc>
160
+ <loc>http://localhost:8000/coll1-route/page2.html</loc>
161
161
  </url>
162
162
  <url>
163
- <loc>http://localhost:8000/coll1/page3.html</loc>
163
+ <loc>http://localhost:8000/coll1-route/page3.html</loc>
164
164
  </url>
165
165
  <url>
166
- <loc>http://localhost:8000/coll2</loc>
166
+ <loc>http://localhost:8000/coll2-route</loc>
167
167
  </url>
168
168
  <url>
169
- <loc>http://localhost:8000/coll2/page0.html</loc>
169
+ <loc>http://localhost:8000/coll2-route/page0.html</loc>
170
170
  </url>
171
171
  <url>
172
- <loc>http://localhost:8000/coll2/page1.html</loc>
172
+ <loc>http://localhost:8000/coll2-route/page1.html</loc>
173
173
  </url>
174
174
  <url>
175
- <loc>http://localhost:8000/coll2/page2.html</loc>
175
+ <loc>http://localhost:8000/coll2-route/page2.html</loc>
176
176
  </url>
177
177
  <url>
178
- <loc>http://localhost:8000/coll2/page3.html</loc>
178
+ <loc>http://localhost:8000/coll2-route/page3.html</loc>
179
179
  </url>
180
180
  <url>
181
181
  <loc>http://localhost:8000/page0.html</loc>