render-engine 2025.11.1a1__tar.gz → 2026.1.1a2__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 (124) hide show
  1. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/workflows/devcontainer-ci.yml +2 -2
  2. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/workflows/labeler.yml +1 -1
  3. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/workflows/lint.yml +2 -2
  4. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/workflows/publish.yml +1 -1
  5. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/workflows/scorecard.yml +3 -3
  6. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/workflows/test.yml +3 -3
  7. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/PKG-INFO +1 -1
  8. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/collection.md +2 -1
  9. render_engine-2026.1.1a2/docs/docs/content_manager.md +85 -0
  10. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/mkdocs.yml +1 -0
  11. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/requirements.txt +35 -35
  12. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/__version__.py +3 -3
  13. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/_base_object.py +1 -0
  14. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/collection.py +43 -24
  15. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/content_managers/base_content_manager.py +18 -1
  16. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/content_managers/file_content_manager.py +30 -1
  17. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/page.py +23 -18
  18. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/site.py +5 -8
  19. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine.egg-info/PKG-INFO +1 -1
  20. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine.egg-info/SOURCES.txt +2 -0
  21. render_engine-2026.1.1a2/tests/test_file_content_manager.py +78 -0
  22. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_plugins.py +6 -6
  23. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_templates/test_base_html.py +2 -2
  24. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.all-contributorsrc +0 -0
  25. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.devcontainer/Dockerfile +0 -0
  26. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.devcontainer/devcontainer.json +0 -0
  27. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.devcontainer/setup.sh +0 -0
  28. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/FUNDING.yml +0 -0
  29. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/ISSUE_TEMPLATE/config.yaml +0 -0
  30. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/ISSUE_TEMPLATE/form_issue_template.yml +0 -0
  31. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/dependabot-bot.yml +0 -0
  33. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/dependabot.yml +0 -0
  34. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/labeler.yml +0 -0
  35. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.github/release.yml +0 -0
  36. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.gitignore +0 -0
  37. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.markdownlint.json +0 -0
  38. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.pre-commit-config.yaml +0 -0
  39. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.readthedocs.yml +0 -0
  40. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/.vscode/tasks.json +0 -0
  41. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/CONTRIBUTING.md +0 -0
  42. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/README.md +0 -0
  43. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/SECURITY.md +0 -0
  44. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/.markdownlint.json +0 -0
  45. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/archive.md +0 -0
  46. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/assets/create environment vs code.gif +0 -0
  47. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/assets/create-app-help.png +0 -0
  48. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/assets/create-codespace.gif +0 -0
  49. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/assets/launching a dev container.gif +0 -0
  50. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/assets/render-engine-init-help.png +0 -0
  51. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/assets/render-engine-init.png +0 -0
  52. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/cli.md +0 -0
  53. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/contributing/CODE_OF_CONDUCT.md +0 -0
  54. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/contributing/CONTRIBUTING.md +0 -0
  55. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/contributing/environment_setup.md +0 -0
  56. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/custom_collections.md +0 -0
  57. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/feeds.md +0 -0
  58. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/building-your-site.md +0 -0
  59. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/creating-a-collection.md +0 -0
  60. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/creating-a-page.md +0 -0
  61. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/creating-your-app.md +0 -0
  62. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/getting-started.md +0 -0
  63. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/installation.md +0 -0
  64. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/getting-started/layout.md +0 -0
  65. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/index.md +0 -0
  66. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/page.md +0 -0
  67. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/parsers.md +0 -0
  68. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/plugins.md +0 -0
  69. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/site.md +0 -0
  70. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/site_map.md +0 -0
  71. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/templates.md +0 -0
  72. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/docs/theme_management.md +0 -0
  73. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/docs/requirements.txt +0 -0
  74. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/noxfile.py +0 -0
  75. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/pyproject.toml +0 -0
  76. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/setup.cfg +0 -0
  77. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/.gitignore +0 -0
  78. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/__init__.py +0 -0
  79. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/__main__.py +0 -0
  80. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/archive.py +0 -0
  81. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/blog.py +0 -0
  82. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/content_managers/__init__.py +0 -0
  83. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/engine.py +0 -0
  84. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/extras/__init__.py +0 -0
  85. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/feeds.py +0 -0
  86. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/hookspecs.py +0 -0
  87. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/links.py +0 -0
  88. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/parsers/markdown.py +0 -0
  89. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/plugins.py +0 -0
  90. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/py.typed +0 -0
  91. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/__init__.py +0 -0
  92. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/archive.html +0 -0
  93. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/base.html +0 -0
  94. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/base_collection_path.md +0 -0
  95. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/base_templates/_archive.html +0 -0
  96. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/base_templates/_base.html +0 -0
  97. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/base_templates/_page.html +0 -0
  98. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/components/footer.html +0 -0
  99. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/components/page_title.html +0 -0
  100. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/page.html +0 -0
  101. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/rss2.0.xml +0 -0
  102. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/rss2.0_items.xml +0 -0
  103. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/sitemap.xml +0 -0
  104. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/render_engine_templates/sitemap_item.xml +0 -0
  105. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/site_map.py +0 -0
  106. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/themes.py +0 -0
  107. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine/utils/__init__.py +0 -0
  108. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine.egg-info/dependency_links.txt +0 -0
  109. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine.egg-info/requires.txt +0 -0
  110. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/src/render_engine.egg-info/top_level.txt +0 -0
  111. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/conftest.py +0 -0
  112. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_archive.py +0 -0
  113. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_base_object.py +0 -0
  114. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_blog.py +0 -0
  115. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_collections.py +0 -0
  116. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_engine.py +0 -0
  117. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_feeds/conftest_feed.py +0 -0
  118. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_feeds/test_feeds.py +0 -0
  119. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_page.py +0 -0
  120. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_parsers_remove_2024_3_1.py +0 -0
  121. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_site.py +0 -0
  122. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_site_map.py +0 -0
  123. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/tests/test_theme_manager.py +0 -0
  124. {render_engine-2025.11.1a1 → render_engine-2026.1.1a2}/uv.lock +0 -0
@@ -19,9 +19,9 @@ jobs:
19
19
  build:
20
20
  runs-on: ubuntu-latest
21
21
  steps:
22
- - uses: actions/checkout@v5
22
+ - uses: actions/checkout@v6.0.1
23
23
  - name: Use Node.js 20.x
24
- uses: actions/setup-node@v4
24
+ uses: actions/setup-node@v6
25
25
  with:
26
26
  node-version: 20.x
27
27
  - run: npm install -g @devcontainers/cli
@@ -12,5 +12,5 @@ jobs:
12
12
  labeler:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v5
15
+ - uses: actions/checkout@v6.0.1
16
16
  - uses: actions/labeler@v5
@@ -13,7 +13,7 @@ jobs:
13
13
  name: Format and Lint Python
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
- - uses: actions/checkout@v5
16
+ - uses: actions/checkout@v6.0.1
17
17
  with:
18
18
  fetch-depth: 0
19
19
  - name: Install Python
@@ -34,7 +34,7 @@ jobs:
34
34
  name: Lint Markdown
35
35
  runs-on: ubuntu-latest
36
36
  steps:
37
- - uses: actions/checkout@v5
37
+ - uses: actions/checkout@v6.0.1
38
38
  - name: Markdown Lint base-files
39
39
  uses: DavidAnson/markdownlint-cli2-action@v20
40
40
  with:
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  steps:
17
17
  - name: Checkout source
18
- uses: actions/checkout@v5
18
+ uses: actions/checkout@v6.0.1
19
19
 
20
20
  - name: Set up Python 3.11
21
21
  uses: actions/setup-python@v5
@@ -32,7 +32,7 @@ jobs:
32
32
 
33
33
  steps:
34
34
  - name: "Checkout code"
35
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v4.1.1
35
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.1.1
36
36
  with:
37
37
  persist-credentials: false
38
38
 
@@ -59,7 +59,7 @@ jobs:
59
59
  # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
60
60
  # format to the repository Actions tab.
61
61
  - name: "Upload artifact"
62
- uses: actions/upload-artifact@v4
62
+ uses: actions/upload-artifact@v6
63
63
  with:
64
64
  name: SARIF
65
65
  path: results.sarif
@@ -68,6 +68,6 @@ jobs:
68
68
  # Upload the results to GitHub's code scanning dashboard (optional).
69
69
  # Commenting out will disable upload of results to your repo's Code Scanning dashboard
70
70
  - name: "Upload to code-scanning"
71
- uses: github/codeql-action/upload-sarif@v3
71
+ uses: github/codeql-action/upload-sarif@v4
72
72
  with:
73
73
  sarif_file: results.sarif
@@ -23,7 +23,7 @@ jobs:
23
23
  python-version: ["3.10", "3.11", "3.12", "3.13"]
24
24
  fail-fast: true
25
25
  steps:
26
- - uses: actions/checkout@v5
26
+ - uses: actions/checkout@v6.0.1
27
27
  - uses: actions/setup-python@v5
28
28
  with:
29
29
  python-version: ${{ matrix.python-version }}
@@ -44,7 +44,7 @@ jobs:
44
44
  fail-fast: true
45
45
  runs-on: ${{ matrix.os }}
46
46
  steps:
47
- - uses: actions/checkout@v5
47
+ - uses: actions/checkout@v6.0.1
48
48
  - uses: actions/setup-python@v5
49
49
  with:
50
50
  python-version: "3.12"
@@ -56,7 +56,7 @@ jobs:
56
56
  run: |
57
57
  python -m pytest
58
58
  - name: coverage
59
- uses: actions/upload-artifact@v4
59
+ uses: actions/upload-artifact@v6
60
60
  with:
61
61
  name: code-coverage-report-${{ matrix.os }}
62
62
  path: .coverage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: render_engine
3
- Version: 2025.11.1a1
3
+ Version: 2026.1.1a2
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/
@@ -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,85 @@
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.
68
+
69
+ ### `find_entry`
70
+
71
+ The `find_entry` method is used to find a specific page in a `Collection`. It will search through all the pages
72
+ known to the `ContentManager` until it finds one that has all the attributes specified. Even if multiple entries
73
+ would satisfy the criteria, only the first found will be returned.
74
+
75
+ ### `update_entry`
76
+
77
+ The `update_entry` is used to update an existing entry in a `ContentManager`.
78
+
79
+ Parameters:
80
+
81
+ ```python
82
+ page: Page # The Page to update
83
+ content: str = None # Updated content
84
+ **kwargs: dict # The other attributes for the updated page
85
+ ```
@@ -26,6 +26,7 @@ nav:
26
26
  - Site Objects: site.md
27
27
  - Extending Render Engine:
28
28
  - Plugins: plugins.md
29
+ - Content Managers: content_manager.md
29
30
  - Parsers: parsers.md
30
31
  - CLI: cli.md
31
32
  - Templates and Themes:
@@ -4,30 +4,30 @@
4
4
  #
5
5
  # pip-compile --extra=dev --output-file=requirements.txt pyproject.toml
6
6
  #
7
- anyio==4.10.0
7
+ anyio==4.12.0
8
8
  # via
9
9
  # httpx
10
10
  # watchfiles
11
- arrow==1.3.0
11
+ arrow==1.4.0
12
12
  # via cookiecutter
13
13
  babel==2.17.0
14
14
  # via mkdocs-material
15
- backrefs==6.0.1
15
+ backrefs==6.1
16
16
  # via mkdocs-material
17
17
  binaryornot==0.4.4
18
18
  # via cookiecutter
19
- certifi==2025.8.3
19
+ certifi==2025.11.12
20
20
  # via
21
21
  # httpcore
22
22
  # httpx
23
23
  # requests
24
- cfgv==3.4.0
24
+ cfgv==3.5.0
25
25
  # via pre-commit
26
26
  chardet==5.2.0
27
27
  # via binaryornot
28
- charset-normalizer==3.4.3
28
+ charset-normalizer==3.4.4
29
29
  # via requests
30
- click==8.2.1
30
+ click==8.3.1
31
31
  # via
32
32
  # cookiecutter
33
33
  # mkdocs
@@ -38,13 +38,13 @@ colorama==0.4.6
38
38
  # mkdocs-material
39
39
  cookiecutter==2.6.0
40
40
  # via render_engine (pyproject.toml)
41
- coverage[toml]==7.10.6
41
+ coverage[toml]==7.13.1
42
42
  # via pytest-cov
43
43
  distlib==0.4.0
44
44
  # via virtualenv
45
45
  ephemeral-port-reserve==1.1.4
46
46
  # via render_engine (pyproject.toml)
47
- filelock==3.19.1
47
+ filelock==3.20.1
48
48
  # via virtualenv
49
49
  ghp-import==2.1.0
50
50
  # via mkdocs
@@ -52,7 +52,7 @@ gitdb==4.0.12
52
52
  # via gitpython
53
53
  gitpython==3.1.45
54
54
  # via render_engine (pyproject.toml)
55
- griffe==1.13.0
55
+ griffe==1.15.0
56
56
  # via mkdocstrings-python
57
57
  h11==0.16.0
58
58
  # via httpcore
@@ -60,14 +60,14 @@ httpcore==1.0.9
60
60
  # via httpx
61
61
  httpx==0.28.1
62
62
  # via render_engine (pyproject.toml)
63
- identify==2.6.13
63
+ identify==2.6.15
64
64
  # via pre-commit
65
- idna==3.10
65
+ idna==3.11
66
66
  # via
67
67
  # anyio
68
68
  # httpx
69
69
  # requests
70
- iniconfig==2.1.0
70
+ iniconfig==2.3.0
71
71
  # via pytest
72
72
  jinja2==3.1.6
73
73
  # via
@@ -76,7 +76,7 @@ jinja2==3.1.6
76
76
  # mkdocs-material
77
77
  # mkdocstrings
78
78
  # render_engine (pyproject.toml)
79
- markdown==3.8.2
79
+ markdown==3.10
80
80
  # via
81
81
  # mkdocs
82
82
  # mkdocs-autorefs
@@ -89,7 +89,7 @@ markdown2==2.5.4
89
89
  # via
90
90
  # render-engine-markdown
91
91
  # render_engine (pyproject.toml)
92
- markupsafe==3.0.2
92
+ markupsafe==3.0.3
93
93
  # via
94
94
  # jinja2
95
95
  # mkdocs
@@ -113,23 +113,23 @@ mkdocs-autorefs==1.4.3
113
113
  # mkdocstrings-python
114
114
  mkdocs-get-deps==0.2.0
115
115
  # via mkdocs
116
- mkdocs-material==9.6.18
116
+ mkdocs-material==9.7.1
117
117
  # via render_engine (pyproject.toml)
118
118
  mkdocs-material-extensions==1.3.1
119
119
  # via mkdocs-material
120
- mkdocstrings[python]==0.30.0
120
+ mkdocstrings[python]==1.0.0
121
121
  # via
122
122
  # mkdocstrings-python
123
123
  # render_engine (pyproject.toml)
124
- mkdocstrings-python==1.18.2
124
+ mkdocstrings-python==2.0.1
125
125
  # via mkdocstrings
126
126
  more-itertools==10.8.0
127
127
  # via render_engine (pyproject.toml)
128
- mypy==1.17.1
128
+ mypy==1.19.1
129
129
  # via render_engine (pyproject.toml)
130
130
  mypy-extensions==1.1.0
131
131
  # via mypy
132
- nodeenv==1.9.1
132
+ nodeenv==1.10.0
133
133
  # via pre-commit
134
134
  packaging==25.0
135
135
  # via
@@ -139,7 +139,7 @@ paginate==0.5.7
139
139
  # via mkdocs-material
140
140
  pathspec==0.12.1
141
141
  # via mkdocs
142
- platformdirs==4.4.0
142
+ platformdirs==4.5.1
143
143
  # via
144
144
  # mkdocs-get-deps
145
145
  # virtualenv
@@ -147,25 +147,25 @@ pluggy==1.6.0
147
147
  # via
148
148
  # pytest
149
149
  # render_engine (pyproject.toml)
150
- pre-commit==4.3.0
150
+ pre-commit==4.5.1
151
151
  # via render_engine (pyproject.toml)
152
152
  pygments==2.19.2
153
153
  # via
154
154
  # mkdocs-material
155
155
  # rich
156
- pymdown-extensions==10.16.1
156
+ pymdown-extensions==10.20
157
157
  # via
158
158
  # mkdocs-material
159
159
  # mkdocstrings
160
160
  # render_engine (pyproject.toml)
161
- pytest==8.4.2
161
+ pytest==9.0.2
162
162
  # via
163
163
  # pytest-cov
164
164
  # pytest-mock
165
165
  # render_engine (pyproject.toml)
166
- pytest-cov==6.2.1
166
+ pytest-cov==7.0.0
167
167
  # via render_engine (pyproject.toml)
168
- pytest-mock==3.14.1
168
+ pytest-mock==3.15.1
169
169
  # via render_engine (pyproject.toml)
170
170
  python-dateutil==2.9.0.post0
171
171
  # via
@@ -180,7 +180,7 @@ python-slugify==8.0.4
180
180
  # via
181
181
  # cookiecutter
182
182
  # render_engine (pyproject.toml)
183
- pyyaml==6.0.2
183
+ pyyaml==6.0.3
184
184
  # via
185
185
  # cookiecutter
186
186
  # mkdocs
@@ -201,12 +201,12 @@ requests==2.32.5
201
201
  # via
202
202
  # cookiecutter
203
203
  # mkdocs-material
204
- rich==14.1.0
204
+ rich==14.2.0
205
205
  # via
206
206
  # cookiecutter
207
207
  # render_engine (pyproject.toml)
208
208
  # typer
209
- ruff==0.12.11
209
+ ruff==0.14.10
210
210
  # via render_engine (pyproject.toml)
211
211
  shellingham==1.5.4
212
212
  # via typer
@@ -219,22 +219,22 @@ sniffio==1.3.1
219
219
  text-unidecode==1.3
220
220
  # via python-slugify
221
221
  toml==0.10.2
222
- typer==0.17.3
222
+ typer==0.21.0
223
223
  # via render_engine (pyproject.toml)
224
- types-python-dateutil==2.9.0.20250822
224
+ types-python-dateutil==2.9.0.20251115
225
225
  # via arrow
226
226
  typing-extensions==4.15.0
227
227
  # via
228
228
  # anyio
229
229
  # mypy
230
230
  # typer
231
- tzdata==2025.2
231
+ tzdata==2025.3
232
232
  # via render_engine (pyproject.toml)
233
- urllib3==2.5.0
233
+ urllib3==2.6.3
234
234
  # via requests
235
- virtualenv==20.34.0
235
+ virtualenv==20.35.4
236
236
  # via pre-commit
237
237
  watchdog==6.0.0
238
238
  # via mkdocs
239
- watchfiles==1.1.0
239
+ watchfiles==1.1.1
240
240
  # via render_engine (pyproject.toml)
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2025.11.1a1'
32
- __version_tuple__ = version_tuple = (2025, 11, 1, 'a1')
31
+ __version__ = version = '2026.1.1a2'
32
+ __version_tuple__ = version_tuple = (2026, 1, 1, 'a2')
33
33
 
34
- __commit_id__ = commit_id = 'g5ed5705d6'
34
+ __commit_id__ = commit_id = 'g6c6c29a4d'
@@ -27,6 +27,7 @@ class BaseObject:
27
27
  plugins: list[Callable] | None
28
28
  plugin_settings: dict = {"plugins": defaultdict(dict)}
29
29
  skip_site_map: bool = False
30
+ metadata: dict = dict()
30
31
 
31
32
  @property
32
33
  def _title(self) -> str:
@@ -1,7 +1,9 @@
1
1
  import copy
2
2
  import datetime
3
3
  import logging
4
+ import os
4
5
  from collections.abc import Callable, Generator
6
+ from multiprocessing.pool import ThreadPool
5
7
  from pathlib import Path
6
8
  from typing import Any
7
9
 
@@ -10,10 +12,9 @@ from more_itertools import batched
10
12
  from render_engine_parser import BasePageParser
11
13
  from slugify import slugify
12
14
 
13
- from render_engine.content_managers import ContentManager, FileContentManager
14
-
15
15
  from ._base_object import BaseObject
16
16
  from .archive import Archive
17
+ from .content_managers import ContentManager, FileContentManager
17
18
  from .feeds import RSSFeed
18
19
  from .page import Page
19
20
  from .plugins import PluginManager
@@ -245,6 +246,28 @@ class Collection(BaseObject):
245
246
  def __iter__(self):
246
247
  yield from self.content_manager
247
248
 
249
+ def __len__(self):
250
+ return len(self.content_manager)
251
+
252
+ @property
253
+ def all_content(self) -> Generator:
254
+ """All of the content that is associated with the Collection including Pages, Feed, and Archives"""
255
+ yield from self
256
+
257
+ if getattr(self, "has_archive", False):
258
+ for archive in self.archives:
259
+ yield archive
260
+ if archive.is_index:
261
+ # In order to avoid collision with parallel processing we need to do a copy.
262
+ # A deepcopy is not necessary because we only care about not overwriting the
263
+ # slug on the original.
264
+ index = copy.copy(archive)
265
+ index.slug = "index"
266
+ yield index
267
+
268
+ if feed := getattr(self, "feed", None):
269
+ yield feed
270
+
248
271
  def _run_collection_plugins(self, site, hook_type: str):
249
272
  """
250
273
  Run plugins for a collection
@@ -261,32 +284,28 @@ class Collection(BaseObject):
261
284
  return
262
285
  method(collection=self, site=site, settings=self.plugin_manager.plugin_settings)
263
286
 
264
- def render(self) -> None:
265
- """Iterate through Pages and Check for Collections and Feeds"""
287
+ def _render(self, entry):
288
+ """
289
+ Renders 1 entry in the Collection
266
290
 
267
- for entry in self:
291
+ :param entry: The entry to process
292
+ """
293
+ if not isinstance(entry, RSSFeed) and not isinstance(entry, Archive):
268
294
  entry.plugin_manager = copy.deepcopy(self.plugin_manager)
269
295
 
270
- for route in entry.routes:
271
- entry.site = self.site
272
- entry.render(route, self.site.theme_manager)
296
+ entry.site = self.site
297
+ entry.render(self.site.theme_manager)
273
298
 
274
- if getattr(self, "has_archive", False):
275
- for archive in self.archives:
276
- archive.site = self.site
277
- logging.debug("Adding Archive: %s", archive.__class__.__name__)
278
-
279
- for _ in self.routes:
280
- archive.render(self.routes[0], self.site.theme_manager)
281
-
282
- if archive.is_index:
283
- archive.slug = "index"
284
- archive.render(self.routes[0], self.site.theme_manager)
285
- feed: RSSFeed
286
- if hasattr(self, "Feed"):
287
- feed = self.feed
288
- feed.site = self.site
289
- feed.render(route="./", theme_manager=self.site.theme_manager)
299
+ def render(self) -> None:
300
+ """Iterate through Pages and Check for Archives and Feeds"""
301
+
302
+ # Use a ThreadPool to process all of the entries in the collection in parallel.
303
+ # This is limited to the number of CPUs available. The easiest way to implement
304
+ # this parallelization for a single task is to use the imap_unordered method of
305
+ # the ThreadPool. This is, effectively, a generator so we need to loop over it
306
+ # for them to run. Since there is no actual return value to look at we just `pass`.
307
+ for entry in ThreadPool(processes=os.cpu_count()).imap_unordered(self._render, self.all_content):
308
+ pass
290
309
 
291
310
  def create_entry(
292
311
  self, filepath: Path = None, editor: str = None, content: str = None, metadata: dict = None
@@ -19,4 +19,21 @@ class ContentManager(ABC):
19
19
  @abstractmethod
20
20
  def create_entry(self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None):
21
21
  """Create a new entry"""
22
- ...
22
+ pass
23
+
24
+ def find_entry(self, **kwargs):
25
+ """
26
+ Find an entry
27
+
28
+ :param kwargs: List of attributes to search by
29
+ :return: Page if it was found otherwise None
30
+ """
31
+ for page in self:
32
+ if all(getattr(page, attr, None) == value for attr, value in kwargs.items()):
33
+ return page
34
+ return None
35
+
36
+ @abstractmethod
37
+ def update_entry(self, *, page, **kwargs):
38
+ """Update an entry"""
39
+ pass
@@ -36,8 +36,16 @@ class FileContentManager(ContentManager):
36
36
  def pages(self, value: Iterable):
37
37
  self._pages = value
38
38
 
39
+ def __len__(self):
40
+ return len(list(self.pages))
41
+
39
42
  def create_entry(
40
- self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None
43
+ self,
44
+ filepath: Path = None,
45
+ editor: str = None,
46
+ metadata: dict = None,
47
+ content: str = None,
48
+ update: bool = False,
41
49
  ) -> str:
42
50
  """
43
51
  Create a new entry for the Collection
@@ -46,12 +54,33 @@ class FileContentManager(ContentManager):
46
54
  :param editor: Editor to open to edit the entry.
47
55
  :param content: The content for the entry
48
56
  :param metadata: Metadata for the new entry
57
+ :param update: Allow overwriting the existing file
49
58
  """
50
59
  if not filepath:
51
60
  raise ValueError("filepath needs to be specified.")
52
61
 
62
+ if not update and filepath.exists():
63
+ raise RuntimeError(f"File at {filepath} exists and update is disabled.")
64
+
53
65
  parsed_content = self.collection.Parser.create_entry(content=content, **metadata)
54
66
  filepath.write_text(parsed_content)
55
67
  if editor:
56
68
  subprocess.run([editor, filepath])
57
69
  return f"New entry created at {filepath} ."
70
+
71
+ def update_entry(self, page, *, content: str = None, **kwargs) -> str:
72
+ """
73
+ Update an entry
74
+
75
+ :param page: Page object to update
76
+ :param content: Content for the updated page
77
+ :param kwargs: Attributes to be included in the updated page
78
+ :return: String indicating that the page was updated.
79
+ """
80
+ self.create_entry(filepath=page.content_path, metadata=kwargs, content=content, update=True)
81
+ if self._pages:
82
+ self._pages = [
83
+ existing_page for existing_page in self._pages if page.content_path != existing_page.content_path
84
+ ]
85
+ self._pages.append(self.collection.get_page(page.content_path))
86
+ return f"Entry at {page.content_path} updated."