simple-module-pagebuilder 0.0.6__tar.gz → 0.0.7__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.
- simple_module_pagebuilder-0.0.7/PKG-INFO +315 -0
- simple_module_pagebuilder-0.0.7/README.md +292 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/package.json +1 -1
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/board.py +12 -1
- simple_module_pagebuilder-0.0.7/pagebuilder/boot.py +140 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/NewPageDialog.tsx +67 -12
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/PageBoard.tsx +4 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/PageBoardCard.tsx +18 -3
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/PageListFilters.tsx +41 -5
- simple_module_pagebuilder-0.0.7/pagebuilder/components/PageListRow.tsx +121 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/components/TranslatePageDialog.tsx +187 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/PageEditorToolbar.tsx +7 -1
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/PageInspectorDrawer.tsx +61 -27
- simple_module_pagebuilder-0.0.7/pagebuilder/components/editor/TranslationsPanel.tsx +139 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/components/snapshots/ImportPlanSummary.tsx +139 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/components/snapshots/SnapshotList.tsx +112 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/components/snapshots/UploadBundleDialog.tsx +88 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/components/snapshots/planSummary.test.ts +133 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/components/snapshots/planSummary.ts +115 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/contracts/__init__.py +6 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/contracts/schemas/__init__.py +20 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/contracts/schemas/_pages.py +89 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/contracts/schemas/_snapshots.py +59 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/deps.py +8 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/diff.py +15 -10
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/__init__.py +2 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/pages.py +53 -9
- simple_module_pagebuilder-0.0.7/pagebuilder/endpoints/api/snapshots.py +198 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/endpoints/content_views.py +69 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/endpoints/public_views.py +269 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/seo.py +72 -15
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/views.py +47 -7
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/useEditorForm.ts +13 -1
- simple_module_pagebuilder-0.0.7/pagebuilder/hooks/useSnapshots.ts +85 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/locales.py +101 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/media_files.py +1 -1
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/models/__init__.py +12 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/models/_page.py +68 -2
- simple_module_pagebuilder-0.0.7/pagebuilder/models/_redirect.py +48 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/models/_snapshot.py +131 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/module.py +239 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/pages/ContentImportReview.tsx +107 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/pages/ContentSnapshots.tsx +79 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/PageEditor.tsx +13 -1
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/PageList.tsx +41 -77
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/PendingReview.tsx +16 -2
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/PublicPage.tsx +25 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/public_claims.py +15 -7
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/redirects.py +27 -5
- simple_module_pagebuilder-0.0.7/pagebuilder/scheduler.py +92 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/security.py +52 -28
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/service/__init__.py +64 -14
- simple_module_pagebuilder-0.0.7/pagebuilder/service/_translations.py +215 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/settings.py +279 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/__init__.py +13 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/apply.py +233 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/archive.py +202 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/assets.py +89 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/blobs.py +115 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/capture.py +280 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/format.py +56 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/media_match.py +116 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/pages.py +150 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/plan.py +260 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/service.py +300 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/snapshots/validate.py +194 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/api.ts +1 -0
- simple_module_pagebuilder-0.0.7/pagebuilder/utils/locale.ts +41 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/pagesApi.ts +31 -5
- simple_module_pagebuilder-0.0.7/pagebuilder/utils/snapshotsApi.ts +125 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/types.ts +40 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pyproject.toml +11 -4
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/conftest.py +10 -22
- simple_module_pagebuilder-0.0.7/tests/locale_fixture.py +48 -0
- simple_module_pagebuilder-0.0.7/tests/page_helpers.py +33 -0
- simple_module_pagebuilder-0.0.7/tests/snapshot_fixture.py +90 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_admin_list_view.py +31 -0
- simple_module_pagebuilder-0.0.7/tests/test_hreflang_and_sitemap.py +163 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_module.py +2 -2
- simple_module_pagebuilder-0.0.7/tests/test_multilingual.py +186 -0
- simple_module_pagebuilder-0.0.7/tests/test_page_translations.py +222 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_public_claims.py +68 -10
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_public_routes.py +21 -4
- simple_module_pagebuilder-0.0.7/tests/test_settings_source.py +126 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_site_layout.py +2 -2
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_apply.py +246 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_apply_media.py +107 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_archive.py +299 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_assets.py +70 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_blobs.py +64 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_capture.py +252 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_media_match.py +96 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_models.py +30 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_multilingual.py +238 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_page_payload.py +65 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_plan.py +288 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_validate.py +138 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshot_views.py +73 -0
- simple_module_pagebuilder-0.0.7/tests/test_snapshots_api.py +184 -0
- simple_module_pagebuilder-0.0.6/PKG-INFO +0 -184
- simple_module_pagebuilder-0.0.6/README.md +0 -162
- simple_module_pagebuilder-0.0.6/pagebuilder/endpoints/public_views.py +0 -162
- simple_module_pagebuilder-0.0.6/pagebuilder/models/_redirect.py +0 -28
- simple_module_pagebuilder-0.0.6/pagebuilder/module.py +0 -290
- simple_module_pagebuilder-0.0.6/pagebuilder/settings.py +0 -164
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/.gitignore +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/ConfirmDialog.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/DiffSummary.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/NoteDialog.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/ScheduledBadge.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/StatusBadge.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blockRegistry.test.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blockRegistry.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Button.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Columns.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Heading.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Image.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Image.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/MediaPicker.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Spacer.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/blocks/Text.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/BlockOutline.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/NarrowCanvas.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/PageActions.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/PageSettingsPanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/RevisionHistoryPanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/SchedulePanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/SeoPreview.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/editor/SeoSettingsPanel.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/layoutPuckConfig.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/lazyBlock.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/MediaFilters.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/MediaFolderSidebar.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/MediaGrid.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/MediaHeader.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/MediaUploadQueue.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/MediaUsageList.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/media/types.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/migrateContent.test.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/migrateContent.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/puckConfig.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/puckConfig.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgetCatalog.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgetCatalog.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_internal/accent-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_internal/disclosure-chevron.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_internal/eyebrow-split-section.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_internal/link-arrow.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_internal/rich-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_internal/solid-icons.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/background-media.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/content-media.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/cta-button.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/empty-placeholder.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/eyebrow-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/heading.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/image-srcset.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/index.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/mask.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/section.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/social-icon.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/_shared/text-link.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/accordion-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/alert-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/app-store-badges-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/article-cards-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/article-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/call-to-action-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/call-to-action-styles.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/call-to-action-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/carousel-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/centered-hero-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/colored-action-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/column-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/contact-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/contact-form-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/contact-form-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/container-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/definition-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/divider-widget.test.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/divider-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/eyebrow-section-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/faq-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/faq-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/feature-card-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/feature-cards-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/feature-cards-styles.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/feature-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/feature-grid-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/feature-showcase-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/gallery-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/grid-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/hero-widget-props.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/hero-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/html-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/iframe-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/leaderboard-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/logo-cloud-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/markdown-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/media-object-layout.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/media-object-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/media-object-text.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/media-object-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/newsletter-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/page-header-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/prose-section-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/quote-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/row-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/section-intro-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/signup-banner-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/simple-action-list-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/site-footer-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/site-header-nav.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/site-header-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/social-banner-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/stats-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/steps-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/story-cards-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/table-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/tabs-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/tags-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/testimonial-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/timeline-layout.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/timeline-render.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/timeline-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/under-construction-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/video-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/components/widgets/welcome-widget.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/contracts/events.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/contracts/schemas/_layout.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/contracts/schemas/_media.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/conversion/blocks-to-html.test.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/conversion/blocks-to-html.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/conversion/index.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/conversion/types.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/__init__.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/_deps.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/layout.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/media_detail.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/revisions.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/uploads.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/endpoints/api/workflow.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/checkbox-field.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/file-picker/file-gallery.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/file-picker/image-picker-field.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/file-picker/index.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/file-picker/types.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/index.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/fields/media-adapter.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/useAutosave.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/useIsNarrow.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/useMediaDetail.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/useMediaUploads.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/usePageRevisions.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/usePageSchedule.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/usePageWorkflow.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/hooks/usePendingDialog.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/layout_service.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/media_images.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/media_queries.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/media_service.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/media_usage.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/models/_base.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/models/_layout.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/models/_media.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/LayoutEditor.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/MediaDetail.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/MediaLibrary.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/pages/Trash.tsx +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/permissions.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/py.typed +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/service/_common.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/service/_revisions.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/service/_trash.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/service/_workflow.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/services.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/static/widgets-base.css +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/translation/apply.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/translation/extract.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/translation/index.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/translation/translatable-fields.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/translation/translation.test.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/blockOutline.test.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/blockOutline.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/datetime.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/decorative-image.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/editorSnapshot.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/markdown.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/measure.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/mediaApi.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/mediaFormat.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/parse-list.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/request.test.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/request.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/slugify.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/pagebuilder/utils/widgetUtils.ts +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/db_fixture.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_approval_workflow.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_auth_gating.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_csrf.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_draft_preview.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_image_processing.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_media_detail.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_media_files.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_media_security.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_media_usage_escaping.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_page_templates.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_pages_api.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_pages_search.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_public_view_headers.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_public_viewer.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_redirects.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_revision_notes_and_diff.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_revisions_api.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_scheduled_publish.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_seo_meta.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_sitemap_robots.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_trash.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_trash_preconditions.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tests/test_uploads_api.py +0 -0
- {simple_module_pagebuilder-0.0.6 → simple_module_pagebuilder-0.0.7}/tsconfig.json +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: simple_module_pagebuilder
|
|
3
|
+
Version: 0.0.7
|
|
4
|
+
Summary: Drag-and-drop visual page builder for simple_module apps
|
|
5
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python_modules
|
|
6
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: cms,pagebuilder,puck,simple-module
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: pillow>=10.0
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
12
|
+
Requires-Dist: simple-module-core<0.1,>=0.0.25
|
|
13
|
+
Requires-Dist: simple-module-db<0.1,>=0.0.25
|
|
14
|
+
Requires-Dist: simple-module-hosting<0.1,>=0.0.25
|
|
15
|
+
Requires-Dist: simple-module-settings<0.1,>=0.0.25
|
|
16
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
17
|
+
Requires-Dist: sqlmodel>=0.0.21
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: simple-module-test<0.1,>=0.0.25; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# simple_module_pagebuilder
|
|
25
|
+
|
|
26
|
+
Drag-and-drop visual page builder for [simple_module](https://github.com/antosubash/simple_module_python)
|
|
27
|
+
apps. Pages are composed from a reusable block library using
|
|
28
|
+
[Puck](https://puckeditor.com) and stored as JSON; published pages are served
|
|
29
|
+
at `/p/{slug}`.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Visual editor** — Puck-based drag-and-drop with a block library (heading,
|
|
34
|
+
text, image, button, columns, spacer) and a media picker.
|
|
35
|
+
- **Site layout** — a shared header/footer layout, edited the same way, wrapped
|
|
36
|
+
around every public page.
|
|
37
|
+
- **Media library** — uploads with magic-byte content sniffing, folder
|
|
38
|
+
organisation, and server-generated WebP thumbnails.
|
|
39
|
+
- **Revisions** — every status transition snapshots the page, with a diff view
|
|
40
|
+
and restore-as-draft.
|
|
41
|
+
- **Approval workflow** — edit → submit for review → approve / reject, gated by
|
|
42
|
+
three separate permissions.
|
|
43
|
+
- **Scheduled publishing** — set `publish_at` / `unpublish_at`; a background
|
|
44
|
+
poller flips status at the due time.
|
|
45
|
+
- **SEO** — per-page meta description, canonical URL, OG image, JSON-LD, plus
|
|
46
|
+
`/sitemap.xml` and `/robots.txt`.
|
|
47
|
+
- **Multilingual content** — a page can exist in several languages, each with
|
|
48
|
+
its own slug, draft and approval state. See below.
|
|
49
|
+
- **Content snapshots** — capture the whole site, download or upload it as a
|
|
50
|
+
`.zip`, and restore behind an approval gate. See below.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add simple_module_pagebuilder
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then add it to your host's `pyproject.toml` dependencies. The module is
|
|
59
|
+
discovered at boot through the `simple_module` entry point — no registration
|
|
60
|
+
code required.
|
|
61
|
+
|
|
62
|
+
Its frontend ships inside the wheel. Pull its JS dependencies into your host's
|
|
63
|
+
client app and regenerate the page manifest with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
smpy host sync-js-deps --host-client-app=host/client_app
|
|
67
|
+
smpy host gen-pages --host-dir=host/client_app
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
Once installed and migrated, sign in and open `/pagebuilder`:
|
|
73
|
+
|
|
74
|
+
1. **Create a page** — `/pagebuilder/new`. Give it a title; the slug is derived
|
|
75
|
+
from it and stays editable.
|
|
76
|
+
2. **Compose it** — drag blocks from the Puck sidebar. Images come from the
|
|
77
|
+
media library, which uploads and thumbnails them for you.
|
|
78
|
+
3. **Publish it** — publish directly with `pagebuilder.publish`, or submit for
|
|
79
|
+
review if your role only carries `pagebuilder.edit`. Reviewers work through
|
|
80
|
+
`/pagebuilder/pending`.
|
|
81
|
+
4. **Schedule it** — set `publish_at` / `unpublish_at` instead of publishing
|
|
82
|
+
now, and the poller flips the status at the due time.
|
|
83
|
+
5. **View it** — the page is live at `/p/{slug}`, wrapped in the site layout
|
|
84
|
+
from `/pagebuilder/layout`.
|
|
85
|
+
|
|
86
|
+
Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
|
|
87
|
+
two revisions and restore either one as a draft.
|
|
88
|
+
|
|
89
|
+
## Contributing blocks from another module
|
|
90
|
+
|
|
91
|
+
A module ships Puck blocks by calling `registerPuckBlocks` from its
|
|
92
|
+
`puck-blocks.ts`; the host imports every module's registration eagerly at app
|
|
93
|
+
start, so the registry is populated before the first render.
|
|
94
|
+
|
|
95
|
+
Because registration is eager, a block that statically imports a heavy render
|
|
96
|
+
component puts that component's whole dependency graph in the entry chunk —
|
|
97
|
+
on every page of the site, for every visitor. Measured on a consuming site, a
|
|
98
|
+
map block's static import put maplibre-gl (1 MB minified, 62% of the bundle)
|
|
99
|
+
in front of visitors who never opened the map. Register heavy blocks with
|
|
100
|
+
`lazyBlock` instead, which keeps fields and defaults eager but loads the
|
|
101
|
+
component the first time a page actually renders the block:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { lazyBlock } from '@simple-module-py/pagebuilder/pagebuilder/components/lazyBlock';
|
|
105
|
+
import { registerPuckBlocks } from '@simple-module-py/pagebuilder/pagebuilder/components/blockRegistry';
|
|
106
|
+
|
|
107
|
+
registerPuckBlocks({
|
|
108
|
+
blocks: {
|
|
109
|
+
AtlasMap: lazyBlock(
|
|
110
|
+
() => import('./components/AtlasMap').then((m) => m.AtlasMapEmbed),
|
|
111
|
+
{ label: 'Atlas map', fields: { /* … */ }, defaultProps: { /* … */ } },
|
|
112
|
+
(props) => <div className={heightClass[props.height]} aria-busy="true" />,
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Light blocks (text, cards, lists) can keep their static imports — the split
|
|
119
|
+
only pays for itself when the component drags in something big.
|
|
120
|
+
|
|
121
|
+
## Migrations
|
|
122
|
+
|
|
123
|
+
This module ships **no** migrations — that is the framework convention. Its
|
|
124
|
+
SQLModel tables are picked up by your host's `build_module_metadata()`, so
|
|
125
|
+
after installing it run:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
make migration msg="add pagebuilder"
|
|
129
|
+
make migrate
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Add `branch_labels = ("pagebuilder",)` to that first generated revision so the
|
|
133
|
+
module can later be removed on its own with
|
|
134
|
+
`alembic downgrade pagebuilder@base`.
|
|
135
|
+
|
|
136
|
+
## Permissions
|
|
137
|
+
|
|
138
|
+
| Permission | Grants |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `pagebuilder.edit` | Draft, save, submit for review, restore a revision |
|
|
141
|
+
| `pagebuilder.publish` | Publish and unpublish directly |
|
|
142
|
+
| `pagebuilder.approve` | Approve or reject a submitted page |
|
|
143
|
+
|
|
144
|
+
`DEFAULT_ROLE_MAP` in `pagebuilder.permissions` suggests three roles —
|
|
145
|
+
`pagebuilder_editor`, `pagebuilder_publisher`, `pagebuilder_approver` —
|
|
146
|
+
cumulatively mapped to those permissions.
|
|
147
|
+
|
|
148
|
+
## Settings
|
|
149
|
+
|
|
150
|
+
Stored in the database and edited under **Settings → PageBuilder**. There is
|
|
151
|
+
no `SM_PAGEBUILDER_*` environment variable: the settings class drops every env
|
|
152
|
+
source, so a value can only come from the store or from the default below.
|
|
153
|
+
Headless deployments write them with `scripts/set_setting.py`.
|
|
154
|
+
|
|
155
|
+
Fields marked ● are read once while the app boots — they decide which routes
|
|
156
|
+
are mounted and what the auth layer exempts — so changing one needs a restart.
|
|
157
|
+
The Settings screen says so next to the input.
|
|
158
|
+
|
|
159
|
+
| Setting | Default | Purpose |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `public_route_prefix` ● | `/p` | Where published pages are served |
|
|
162
|
+
| `content_locales` ● | `["en"]` | Languages a page may be authored in |
|
|
163
|
+
| `default_content_locale` ● | `en` | The language served at the unprefixed URL |
|
|
164
|
+
| `requires_auth` | `true` | Gate the admin surface behind authentication |
|
|
165
|
+
| `csrf_protect` | `true` | Require a CSRF token on mutating admin requests |
|
|
166
|
+
| `media_root` ● | `var/pagebuilder/media` | Upload storage directory |
|
|
167
|
+
| `media_url_prefix` ● | `/media/pagebuilder` | Public URL prefix for uploads |
|
|
168
|
+
| `media_max_bytes` | `10485760` | Per-upload size ceiling |
|
|
169
|
+
| `media_thumbnail_widths` | `320,640,1280,1920` | Widths generated as WebP |
|
|
170
|
+
| `media_webp_quality` | `82` | WebP encoder quality |
|
|
171
|
+
| `snapshot_root` | `var/pagebuilder/snapshots` | Snapshot + blob storage directory |
|
|
172
|
+
| `snapshot_max_upload_bytes` | `209715200` | Ceiling on an uploaded bundle (200 MB) |
|
|
173
|
+
| `snapshot_max_extracted_bytes` | `1073741824` | Ceiling on what a bundle may expand to (1 GB) |
|
|
174
|
+
| `public_csp` | see `settings.py` | CSP header on public pages |
|
|
175
|
+
| `public_cache_max_age` | `60` | `max-age` on public pages |
|
|
176
|
+
| `public_base_url` | `None` | Absolute base for canonical URLs and the sitemap |
|
|
177
|
+
| `site_name` | `None` | OpenGraph site name |
|
|
178
|
+
| `sitemap_enabled` ● | `true` | Serve `/sitemap.xml` |
|
|
179
|
+
| `robots_enabled` ● | `true` | Serve `/robots.txt` |
|
|
180
|
+
| `scheduler_enabled` ● | `true` | Run the scheduled publish/unpublish poller |
|
|
181
|
+
| `scheduler_interval_seconds` ● | `30` | Poll interval |
|
|
182
|
+
|
|
183
|
+
## Routes
|
|
184
|
+
|
|
185
|
+
**Admin views** (under `/pagebuilder`): `/`, `/new`, `/{page_id}/edit`,
|
|
186
|
+
`/pending`, `/layout`, `/media`, `/trash`, `/content`, `/content/review`.
|
|
187
|
+
|
|
188
|
+
**Admin API** (under `/api/pagebuilder`): pages CRUD, workflow transitions,
|
|
189
|
+
revisions and diffs, layout and its revisions, uploads, snapshots and imports.
|
|
190
|
+
|
|
191
|
+
**Public**: `/p/{slug}`, `/{locale}/p/{slug}` (one mount per non-default
|
|
192
|
+
content locale), `/sitemap.xml`, `/robots.txt`.
|
|
193
|
+
|
|
194
|
+
## Multilingual content
|
|
195
|
+
|
|
196
|
+
Off by default: with one content locale nothing changes, no locale-prefixed
|
|
197
|
+
route is mounted, and every existing URL is exactly what it was.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
python scripts/set_setting.py pagebuilder \
|
|
201
|
+
content_locales '["en","de","fr"]' default_content_locale en
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Or on the Settings screen, which is the same store. Either way it takes effect
|
|
205
|
+
at the next boot: the languages decide how many public routers are mounted.
|
|
206
|
+
|
|
207
|
+
These are *content* languages, deliberately separate from the host's
|
|
208
|
+
`SM_I18N_SUPPORTED_LOCALES`, which decides what language the admin console
|
|
209
|
+
speaks. A site can translate its content without translating its console, or
|
|
210
|
+
the reverse.
|
|
211
|
+
|
|
212
|
+
**A translation is an ordinary page.** It has its own row, slug, draft,
|
|
213
|
+
revisions, schedule and approval state; what makes two pages translations of
|
|
214
|
+
each other is a shared `translation_group`. Publishing one never publishes
|
|
215
|
+
another, and there is no "master" — deleting the English page leaves the German
|
|
216
|
+
one intact.
|
|
217
|
+
|
|
218
|
+
**Addresses.** The default locale keeps the bare prefix (`/p/about`) so adding
|
|
219
|
+
a language strands no link that already exists; every other locale is prefixed
|
|
220
|
+
(`/de/p/about`). `/{default}/p/{slug}` permanently redirects to the bare form,
|
|
221
|
+
so one document never answers at two URLs. Slugs are unique *per language*, so
|
|
222
|
+
`/p/about` and `/de/p/about` can both be "about".
|
|
223
|
+
|
|
224
|
+
**Discovery.** A published page emits `hreflang` links for every published
|
|
225
|
+
counterpart plus `x-default`, and the sitemap lists each language at its own
|
|
226
|
+
address with `xhtml:link` alternates — so a crawler finds a translation nothing
|
|
227
|
+
links to yet.
|
|
228
|
+
|
|
229
|
+
**Authoring.** Two ways in, because "we need this in German" is a thought an
|
|
230
|
+
author has while scanning the page list as often as while editing the page:
|
|
231
|
+
|
|
232
|
+
* the page list's **Translate** row action duplicates the page into a language
|
|
233
|
+
it does not have yet — it asks the server which languages are still free
|
|
234
|
+
(the list is paged, so a counterpart may be on a page the browser has not
|
|
235
|
+
loaded), offers a *Copy this page's content* choice, and opens the editor on
|
|
236
|
+
the result;
|
|
237
|
+
* the editor's **Languages** tab lists the site's locales, shows which ones the
|
|
238
|
+
page exists in and where each serves, and starts the ones it does not.
|
|
239
|
+
|
|
240
|
+
Both call `POST /api/pagebuilder/pages/{id}/translations`. A new translation inherits
|
|
241
|
+
the layout, nav membership and the source's title (untranslated, so what still
|
|
242
|
+
needs doing is obvious), starts as a draft, and never inherits `canonical_url`.
|
|
243
|
+
Its breadcrumb parent is the parent's own counterpart, so a trail never crosses
|
|
244
|
+
languages. A page's own language is fixed for its lifetime — moving one would
|
|
245
|
+
strand its slug and orphan the redirect pointing at it.
|
|
246
|
+
|
|
247
|
+
## Content snapshots
|
|
248
|
+
|
|
249
|
+
**Content › Import / Export** captures the site as a restore point. Every entry
|
|
250
|
+
in that list is a snapshot; they differ only in where they came from —
|
|
251
|
+
`manual` (you took it), `upload` (a bundle from another host), or `pre_restore`
|
|
252
|
+
(taken automatically before a restore).
|
|
253
|
+
|
|
254
|
+
**What a snapshot holds:** every non-trashed page including templates, page
|
|
255
|
+
redirects, the site layout, and the media library's files.
|
|
256
|
+
|
|
257
|
+
**What it deliberately does not hold:**
|
|
258
|
+
|
|
259
|
+
- *Branding.* `pagebuilder` has no Python dependency on the branding module,
|
|
260
|
+
and capture runs in-process. Bundling three fields would force every
|
|
261
|
+
pagebuilder host to install branding for a feature unrelated to it. Re-pick
|
|
262
|
+
the colour and design pack in Settings → Branding after restoring onto a
|
|
263
|
+
fresh host.
|
|
264
|
+
- *Another module's content*, news articles included, for the same reason.
|
|
265
|
+
- *Trashed pages, revision history and audit trails.* A snapshot is the site as
|
|
266
|
+
it stands, not its past; restoring history from another host would fabricate
|
|
267
|
+
an audit trail that never happened here.
|
|
268
|
+
|
|
269
|
+
A restore also does **not** write a `PageRevision` per page, unlike every other
|
|
270
|
+
mutation path. The `pre_restore` snapshot already holds the previous state of
|
|
271
|
+
every page, once, with media deduplicated by digest — a revision row per page
|
|
272
|
+
would duplicate that whole before-state and recover nothing extra. The restore
|
|
273
|
+
itself stays audited: who approved it, when, from which snapshot, and the plan
|
|
274
|
+
they were shown.
|
|
275
|
+
|
|
276
|
+
**Restoring is staged, never immediate.** Choosing Restore computes a plan —
|
|
277
|
+
what is created, what is overwritten, what is unchanged — and parks it for
|
|
278
|
+
approval. Taking a snapshot, uploading and staging need `pagebuilder.publish`;
|
|
279
|
+
only `pagebuilder.approve` can apply one. One import is pending at a time,
|
|
280
|
+
because a plan computed against content that has since changed no longer
|
|
281
|
+
describes what applying would do.
|
|
282
|
+
|
|
283
|
+
**Restoring is reversible and never deletes.** Applying takes a `pre_restore`
|
|
284
|
+
snapshot first, so the previous state is one restore away. A page live on the
|
|
285
|
+
site but absent from the bundle is left alone and reported in the plan.
|
|
286
|
+
|
|
287
|
+
**Portability.** Media URLs embed a host-local filename, so bundled content
|
|
288
|
+
stores `asset://<name>` sentinels instead and they resolve to whatever URL each
|
|
289
|
+
host assigns. `Page.parent_id` and `PageRedirect.page_id` travel as slugs and
|
|
290
|
+
are resolved in a second pass, since a parent may sort after its child and a
|
|
291
|
+
redirect may target a page the same restore creates.
|
|
292
|
+
|
|
293
|
+
**Languages.** A bundle identifies a page by `(locale, slug)`, not by slug
|
|
294
|
+
alone — the same pair the database is unique on — so a site publishing `about`
|
|
295
|
+
in two languages captures and restores as two pages rather than one silently
|
|
296
|
+
overwriting the other. Each document carries its `locale` and its
|
|
297
|
+
`translation_group`, so counterparts stay linked as one document after a
|
|
298
|
+
restore. A parent and a redirect both resolve within their own language, so a
|
|
299
|
+
breadcrumb or a retired address never crosses into another one.
|
|
300
|
+
|
|
301
|
+
Pages in a language the target host does not publish are restored and kept, not
|
|
302
|
+
discarded: nothing routes `/fr/p/…` until `fr` is added to `content_locales`,
|
|
303
|
+
at which point they simply start serving. Bundles written before languages
|
|
304
|
+
existed (`format_version` 1) still restore — every page in one is read as the
|
|
305
|
+
default content locale, which is what the schema guaranteed at the time.
|
|
306
|
+
|
|
307
|
+
## Contracts
|
|
308
|
+
|
|
309
|
+
DTOs other modules may depend on are exported from `pagebuilder.contracts`.
|
|
310
|
+
Everything else in the package is internal and may change without a major
|
|
311
|
+
version bump.
|
|
312
|
+
|
|
313
|
+
## Licence
|
|
314
|
+
|
|
315
|
+
MIT
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# simple_module_pagebuilder
|
|
2
|
+
|
|
3
|
+
Drag-and-drop visual page builder for [simple_module](https://github.com/antosubash/simple_module_python)
|
|
4
|
+
apps. Pages are composed from a reusable block library using
|
|
5
|
+
[Puck](https://puckeditor.com) and stored as JSON; published pages are served
|
|
6
|
+
at `/p/{slug}`.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Visual editor** — Puck-based drag-and-drop with a block library (heading,
|
|
11
|
+
text, image, button, columns, spacer) and a media picker.
|
|
12
|
+
- **Site layout** — a shared header/footer layout, edited the same way, wrapped
|
|
13
|
+
around every public page.
|
|
14
|
+
- **Media library** — uploads with magic-byte content sniffing, folder
|
|
15
|
+
organisation, and server-generated WebP thumbnails.
|
|
16
|
+
- **Revisions** — every status transition snapshots the page, with a diff view
|
|
17
|
+
and restore-as-draft.
|
|
18
|
+
- **Approval workflow** — edit → submit for review → approve / reject, gated by
|
|
19
|
+
three separate permissions.
|
|
20
|
+
- **Scheduled publishing** — set `publish_at` / `unpublish_at`; a background
|
|
21
|
+
poller flips status at the due time.
|
|
22
|
+
- **SEO** — per-page meta description, canonical URL, OG image, JSON-LD, plus
|
|
23
|
+
`/sitemap.xml` and `/robots.txt`.
|
|
24
|
+
- **Multilingual content** — a page can exist in several languages, each with
|
|
25
|
+
its own slug, draft and approval state. See below.
|
|
26
|
+
- **Content snapshots** — capture the whole site, download or upload it as a
|
|
27
|
+
`.zip`, and restore behind an approval gate. See below.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add simple_module_pagebuilder
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then add it to your host's `pyproject.toml` dependencies. The module is
|
|
36
|
+
discovered at boot through the `simple_module` entry point — no registration
|
|
37
|
+
code required.
|
|
38
|
+
|
|
39
|
+
Its frontend ships inside the wheel. Pull its JS dependencies into your host's
|
|
40
|
+
client app and regenerate the page manifest with:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
smpy host sync-js-deps --host-client-app=host/client_app
|
|
44
|
+
smpy host gen-pages --host-dir=host/client_app
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
Once installed and migrated, sign in and open `/pagebuilder`:
|
|
50
|
+
|
|
51
|
+
1. **Create a page** — `/pagebuilder/new`. Give it a title; the slug is derived
|
|
52
|
+
from it and stays editable.
|
|
53
|
+
2. **Compose it** — drag blocks from the Puck sidebar. Images come from the
|
|
54
|
+
media library, which uploads and thumbnails them for you.
|
|
55
|
+
3. **Publish it** — publish directly with `pagebuilder.publish`, or submit for
|
|
56
|
+
review if your role only carries `pagebuilder.edit`. Reviewers work through
|
|
57
|
+
`/pagebuilder/pending`.
|
|
58
|
+
4. **Schedule it** — set `publish_at` / `unpublish_at` instead of publishing
|
|
59
|
+
now, and the poller flips the status at the due time.
|
|
60
|
+
5. **View it** — the page is live at `/p/{slug}`, wrapped in the site layout
|
|
61
|
+
from `/pagebuilder/layout`.
|
|
62
|
+
|
|
63
|
+
Every transition writes a revision, so `/pagebuilder/{id}/edit` can compare any
|
|
64
|
+
two revisions and restore either one as a draft.
|
|
65
|
+
|
|
66
|
+
## Contributing blocks from another module
|
|
67
|
+
|
|
68
|
+
A module ships Puck blocks by calling `registerPuckBlocks` from its
|
|
69
|
+
`puck-blocks.ts`; the host imports every module's registration eagerly at app
|
|
70
|
+
start, so the registry is populated before the first render.
|
|
71
|
+
|
|
72
|
+
Because registration is eager, a block that statically imports a heavy render
|
|
73
|
+
component puts that component's whole dependency graph in the entry chunk —
|
|
74
|
+
on every page of the site, for every visitor. Measured on a consuming site, a
|
|
75
|
+
map block's static import put maplibre-gl (1 MB minified, 62% of the bundle)
|
|
76
|
+
in front of visitors who never opened the map. Register heavy blocks with
|
|
77
|
+
`lazyBlock` instead, which keeps fields and defaults eager but loads the
|
|
78
|
+
component the first time a page actually renders the block:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { lazyBlock } from '@simple-module-py/pagebuilder/pagebuilder/components/lazyBlock';
|
|
82
|
+
import { registerPuckBlocks } from '@simple-module-py/pagebuilder/pagebuilder/components/blockRegistry';
|
|
83
|
+
|
|
84
|
+
registerPuckBlocks({
|
|
85
|
+
blocks: {
|
|
86
|
+
AtlasMap: lazyBlock(
|
|
87
|
+
() => import('./components/AtlasMap').then((m) => m.AtlasMapEmbed),
|
|
88
|
+
{ label: 'Atlas map', fields: { /* … */ }, defaultProps: { /* … */ } },
|
|
89
|
+
(props) => <div className={heightClass[props.height]} aria-busy="true" />,
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Light blocks (text, cards, lists) can keep their static imports — the split
|
|
96
|
+
only pays for itself when the component drags in something big.
|
|
97
|
+
|
|
98
|
+
## Migrations
|
|
99
|
+
|
|
100
|
+
This module ships **no** migrations — that is the framework convention. Its
|
|
101
|
+
SQLModel tables are picked up by your host's `build_module_metadata()`, so
|
|
102
|
+
after installing it run:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
make migration msg="add pagebuilder"
|
|
106
|
+
make migrate
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Add `branch_labels = ("pagebuilder",)` to that first generated revision so the
|
|
110
|
+
module can later be removed on its own with
|
|
111
|
+
`alembic downgrade pagebuilder@base`.
|
|
112
|
+
|
|
113
|
+
## Permissions
|
|
114
|
+
|
|
115
|
+
| Permission | Grants |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `pagebuilder.edit` | Draft, save, submit for review, restore a revision |
|
|
118
|
+
| `pagebuilder.publish` | Publish and unpublish directly |
|
|
119
|
+
| `pagebuilder.approve` | Approve or reject a submitted page |
|
|
120
|
+
|
|
121
|
+
`DEFAULT_ROLE_MAP` in `pagebuilder.permissions` suggests three roles —
|
|
122
|
+
`pagebuilder_editor`, `pagebuilder_publisher`, `pagebuilder_approver` —
|
|
123
|
+
cumulatively mapped to those permissions.
|
|
124
|
+
|
|
125
|
+
## Settings
|
|
126
|
+
|
|
127
|
+
Stored in the database and edited under **Settings → PageBuilder**. There is
|
|
128
|
+
no `SM_PAGEBUILDER_*` environment variable: the settings class drops every env
|
|
129
|
+
source, so a value can only come from the store or from the default below.
|
|
130
|
+
Headless deployments write them with `scripts/set_setting.py`.
|
|
131
|
+
|
|
132
|
+
Fields marked ● are read once while the app boots — they decide which routes
|
|
133
|
+
are mounted and what the auth layer exempts — so changing one needs a restart.
|
|
134
|
+
The Settings screen says so next to the input.
|
|
135
|
+
|
|
136
|
+
| Setting | Default | Purpose |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `public_route_prefix` ● | `/p` | Where published pages are served |
|
|
139
|
+
| `content_locales` ● | `["en"]` | Languages a page may be authored in |
|
|
140
|
+
| `default_content_locale` ● | `en` | The language served at the unprefixed URL |
|
|
141
|
+
| `requires_auth` | `true` | Gate the admin surface behind authentication |
|
|
142
|
+
| `csrf_protect` | `true` | Require a CSRF token on mutating admin requests |
|
|
143
|
+
| `media_root` ● | `var/pagebuilder/media` | Upload storage directory |
|
|
144
|
+
| `media_url_prefix` ● | `/media/pagebuilder` | Public URL prefix for uploads |
|
|
145
|
+
| `media_max_bytes` | `10485760` | Per-upload size ceiling |
|
|
146
|
+
| `media_thumbnail_widths` | `320,640,1280,1920` | Widths generated as WebP |
|
|
147
|
+
| `media_webp_quality` | `82` | WebP encoder quality |
|
|
148
|
+
| `snapshot_root` | `var/pagebuilder/snapshots` | Snapshot + blob storage directory |
|
|
149
|
+
| `snapshot_max_upload_bytes` | `209715200` | Ceiling on an uploaded bundle (200 MB) |
|
|
150
|
+
| `snapshot_max_extracted_bytes` | `1073741824` | Ceiling on what a bundle may expand to (1 GB) |
|
|
151
|
+
| `public_csp` | see `settings.py` | CSP header on public pages |
|
|
152
|
+
| `public_cache_max_age` | `60` | `max-age` on public pages |
|
|
153
|
+
| `public_base_url` | `None` | Absolute base for canonical URLs and the sitemap |
|
|
154
|
+
| `site_name` | `None` | OpenGraph site name |
|
|
155
|
+
| `sitemap_enabled` ● | `true` | Serve `/sitemap.xml` |
|
|
156
|
+
| `robots_enabled` ● | `true` | Serve `/robots.txt` |
|
|
157
|
+
| `scheduler_enabled` ● | `true` | Run the scheduled publish/unpublish poller |
|
|
158
|
+
| `scheduler_interval_seconds` ● | `30` | Poll interval |
|
|
159
|
+
|
|
160
|
+
## Routes
|
|
161
|
+
|
|
162
|
+
**Admin views** (under `/pagebuilder`): `/`, `/new`, `/{page_id}/edit`,
|
|
163
|
+
`/pending`, `/layout`, `/media`, `/trash`, `/content`, `/content/review`.
|
|
164
|
+
|
|
165
|
+
**Admin API** (under `/api/pagebuilder`): pages CRUD, workflow transitions,
|
|
166
|
+
revisions and diffs, layout and its revisions, uploads, snapshots and imports.
|
|
167
|
+
|
|
168
|
+
**Public**: `/p/{slug}`, `/{locale}/p/{slug}` (one mount per non-default
|
|
169
|
+
content locale), `/sitemap.xml`, `/robots.txt`.
|
|
170
|
+
|
|
171
|
+
## Multilingual content
|
|
172
|
+
|
|
173
|
+
Off by default: with one content locale nothing changes, no locale-prefixed
|
|
174
|
+
route is mounted, and every existing URL is exactly what it was.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python scripts/set_setting.py pagebuilder \
|
|
178
|
+
content_locales '["en","de","fr"]' default_content_locale en
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Or on the Settings screen, which is the same store. Either way it takes effect
|
|
182
|
+
at the next boot: the languages decide how many public routers are mounted.
|
|
183
|
+
|
|
184
|
+
These are *content* languages, deliberately separate from the host's
|
|
185
|
+
`SM_I18N_SUPPORTED_LOCALES`, which decides what language the admin console
|
|
186
|
+
speaks. A site can translate its content without translating its console, or
|
|
187
|
+
the reverse.
|
|
188
|
+
|
|
189
|
+
**A translation is an ordinary page.** It has its own row, slug, draft,
|
|
190
|
+
revisions, schedule and approval state; what makes two pages translations of
|
|
191
|
+
each other is a shared `translation_group`. Publishing one never publishes
|
|
192
|
+
another, and there is no "master" — deleting the English page leaves the German
|
|
193
|
+
one intact.
|
|
194
|
+
|
|
195
|
+
**Addresses.** The default locale keeps the bare prefix (`/p/about`) so adding
|
|
196
|
+
a language strands no link that already exists; every other locale is prefixed
|
|
197
|
+
(`/de/p/about`). `/{default}/p/{slug}` permanently redirects to the bare form,
|
|
198
|
+
so one document never answers at two URLs. Slugs are unique *per language*, so
|
|
199
|
+
`/p/about` and `/de/p/about` can both be "about".
|
|
200
|
+
|
|
201
|
+
**Discovery.** A published page emits `hreflang` links for every published
|
|
202
|
+
counterpart plus `x-default`, and the sitemap lists each language at its own
|
|
203
|
+
address with `xhtml:link` alternates — so a crawler finds a translation nothing
|
|
204
|
+
links to yet.
|
|
205
|
+
|
|
206
|
+
**Authoring.** Two ways in, because "we need this in German" is a thought an
|
|
207
|
+
author has while scanning the page list as often as while editing the page:
|
|
208
|
+
|
|
209
|
+
* the page list's **Translate** row action duplicates the page into a language
|
|
210
|
+
it does not have yet — it asks the server which languages are still free
|
|
211
|
+
(the list is paged, so a counterpart may be on a page the browser has not
|
|
212
|
+
loaded), offers a *Copy this page's content* choice, and opens the editor on
|
|
213
|
+
the result;
|
|
214
|
+
* the editor's **Languages** tab lists the site's locales, shows which ones the
|
|
215
|
+
page exists in and where each serves, and starts the ones it does not.
|
|
216
|
+
|
|
217
|
+
Both call `POST /api/pagebuilder/pages/{id}/translations`. A new translation inherits
|
|
218
|
+
the layout, nav membership and the source's title (untranslated, so what still
|
|
219
|
+
needs doing is obvious), starts as a draft, and never inherits `canonical_url`.
|
|
220
|
+
Its breadcrumb parent is the parent's own counterpart, so a trail never crosses
|
|
221
|
+
languages. A page's own language is fixed for its lifetime — moving one would
|
|
222
|
+
strand its slug and orphan the redirect pointing at it.
|
|
223
|
+
|
|
224
|
+
## Content snapshots
|
|
225
|
+
|
|
226
|
+
**Content › Import / Export** captures the site as a restore point. Every entry
|
|
227
|
+
in that list is a snapshot; they differ only in where they came from —
|
|
228
|
+
`manual` (you took it), `upload` (a bundle from another host), or `pre_restore`
|
|
229
|
+
(taken automatically before a restore).
|
|
230
|
+
|
|
231
|
+
**What a snapshot holds:** every non-trashed page including templates, page
|
|
232
|
+
redirects, the site layout, and the media library's files.
|
|
233
|
+
|
|
234
|
+
**What it deliberately does not hold:**
|
|
235
|
+
|
|
236
|
+
- *Branding.* `pagebuilder` has no Python dependency on the branding module,
|
|
237
|
+
and capture runs in-process. Bundling three fields would force every
|
|
238
|
+
pagebuilder host to install branding for a feature unrelated to it. Re-pick
|
|
239
|
+
the colour and design pack in Settings → Branding after restoring onto a
|
|
240
|
+
fresh host.
|
|
241
|
+
- *Another module's content*, news articles included, for the same reason.
|
|
242
|
+
- *Trashed pages, revision history and audit trails.* A snapshot is the site as
|
|
243
|
+
it stands, not its past; restoring history from another host would fabricate
|
|
244
|
+
an audit trail that never happened here.
|
|
245
|
+
|
|
246
|
+
A restore also does **not** write a `PageRevision` per page, unlike every other
|
|
247
|
+
mutation path. The `pre_restore` snapshot already holds the previous state of
|
|
248
|
+
every page, once, with media deduplicated by digest — a revision row per page
|
|
249
|
+
would duplicate that whole before-state and recover nothing extra. The restore
|
|
250
|
+
itself stays audited: who approved it, when, from which snapshot, and the plan
|
|
251
|
+
they were shown.
|
|
252
|
+
|
|
253
|
+
**Restoring is staged, never immediate.** Choosing Restore computes a plan —
|
|
254
|
+
what is created, what is overwritten, what is unchanged — and parks it for
|
|
255
|
+
approval. Taking a snapshot, uploading and staging need `pagebuilder.publish`;
|
|
256
|
+
only `pagebuilder.approve` can apply one. One import is pending at a time,
|
|
257
|
+
because a plan computed against content that has since changed no longer
|
|
258
|
+
describes what applying would do.
|
|
259
|
+
|
|
260
|
+
**Restoring is reversible and never deletes.** Applying takes a `pre_restore`
|
|
261
|
+
snapshot first, so the previous state is one restore away. A page live on the
|
|
262
|
+
site but absent from the bundle is left alone and reported in the plan.
|
|
263
|
+
|
|
264
|
+
**Portability.** Media URLs embed a host-local filename, so bundled content
|
|
265
|
+
stores `asset://<name>` sentinels instead and they resolve to whatever URL each
|
|
266
|
+
host assigns. `Page.parent_id` and `PageRedirect.page_id` travel as slugs and
|
|
267
|
+
are resolved in a second pass, since a parent may sort after its child and a
|
|
268
|
+
redirect may target a page the same restore creates.
|
|
269
|
+
|
|
270
|
+
**Languages.** A bundle identifies a page by `(locale, slug)`, not by slug
|
|
271
|
+
alone — the same pair the database is unique on — so a site publishing `about`
|
|
272
|
+
in two languages captures and restores as two pages rather than one silently
|
|
273
|
+
overwriting the other. Each document carries its `locale` and its
|
|
274
|
+
`translation_group`, so counterparts stay linked as one document after a
|
|
275
|
+
restore. A parent and a redirect both resolve within their own language, so a
|
|
276
|
+
breadcrumb or a retired address never crosses into another one.
|
|
277
|
+
|
|
278
|
+
Pages in a language the target host does not publish are restored and kept, not
|
|
279
|
+
discarded: nothing routes `/fr/p/…` until `fr` is added to `content_locales`,
|
|
280
|
+
at which point they simply start serving. Bundles written before languages
|
|
281
|
+
existed (`format_version` 1) still restore — every page in one is read as the
|
|
282
|
+
default content locale, which is what the schema guaranteed at the time.
|
|
283
|
+
|
|
284
|
+
## Contracts
|
|
285
|
+
|
|
286
|
+
DTOs other modules may depend on are exported from `pagebuilder.contracts`.
|
|
287
|
+
Everything else in the package is internal and may change without a major
|
|
288
|
+
version bump.
|
|
289
|
+
|
|
290
|
+
## Licence
|
|
291
|
+
|
|
292
|
+
MIT
|
|
@@ -87,13 +87,22 @@ _ORDER = [
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
async def load(
|
|
90
|
-
db: AsyncSession,
|
|
90
|
+
db: AsyncSession,
|
|
91
|
+
*,
|
|
92
|
+
search: str | None = None,
|
|
93
|
+
locale: str | None = None,
|
|
94
|
+
per_stage: int = PER_STAGE,
|
|
91
95
|
) -> list[Stage]:
|
|
92
96
|
"""Every column, in pipeline order.
|
|
93
97
|
|
|
94
98
|
``In review`` is returned even when empty; the caller drops it, because
|
|
95
99
|
whether the approval workflow is in use is a question about the site rather
|
|
96
100
|
than about this query.
|
|
101
|
+
|
|
102
|
+
``locale`` narrows every column to one language, the same way ``search``
|
|
103
|
+
narrows them. It has to be applied here as well as in the table query: the
|
|
104
|
+
board is the default view, so a language filter the board ignored would be
|
|
105
|
+
a control that visibly does nothing.
|
|
97
106
|
"""
|
|
98
107
|
now = datetime.now(UTC)
|
|
99
108
|
search_clause = _search_filter(search)
|
|
@@ -103,6 +112,8 @@ async def load(
|
|
|
103
112
|
clauses = _conditions(key, now)
|
|
104
113
|
if search_clause is not None:
|
|
105
114
|
clauses = [*clauses, search_clause]
|
|
115
|
+
if locale:
|
|
116
|
+
clauses = [*clauses, Page.locale == locale]
|
|
106
117
|
|
|
107
118
|
total = await db.scalar(select(func.count()).select_from(Page).where(*clauses))
|
|
108
119
|
# Scheduled sorts by when it fires — the next one to go live is the one
|