This guide explains how to add a new publishing platform to MPP. A platform is not a single config entry. It is a cross-module contract: project selection, draft compilation, account connection, publishing execution, frontend display, and extension or remote-browser capability must all use the same platform key.
After a new platform is integrated:
- Users can select the platform on a project.
- Pre-publish sync can generate platform-specific
AdaptedContent. - The publishing flow can resolve the platform publisher, or clearly enter a manual or remote-browser flow.
- Account connection status is visible to both UI and backend.
- API contract, generated code, tests, and docs are updated together.
Choose the platform integration mode before writing code. This keeps platform rules in the right module.
| Question | Decision | Main integration points |
|---|---|---|
| Platform has a stable official publishing API | Prefer API publishing | Go PlatformPublisher, account credentials, official SDK or HTTP client |
| Platform has no stable API, but supports server-side browser login/publishing | Remote-browser publishing | RemoteBrowserPlatformAdapter, browser-worker, cookie storage |
| Platform is better controlled from user's local browser DOM | Extension publishing | WXT content script, handoff, extension callback |
| Platform only needs draft preview, not auto-publishing | Pre-publish/manual publishing | Content Pipeline, frontend preview, status copy |
Check platform.md for the current publishing-method classification. Platform capabilities change; re-check official docs, account qualification, review requirements, and rate limits before implementation.
- Use a stable lowercase key, such as
mastodonorbilibili. - Do not use display names, brand variants, or region labels as keys unless they represent different APIs or publishing semantics.
- The same key must be used across
contracts, Go, Rust, frontend, extension, and test fixtures. - After a key is persisted, renaming it requires migrating
project_platform_publications,platform_accounts,remote_browser_sessions, and related data.
| Module | Required work |
|---|---|
contracts |
Update PublishPlatform enum; add account/browser/publish schemas and paths if needed; regenerate code |
content-pipeline-service |
Register draft profile; implement platform draft compilation; register media profile; add snapshot and version docs |
backend |
Add platform allowlist entry; register publisher; connect credentials or cookies; wire publishing service, routes, tests |
browser-worker |
If remote-browser script is needed, add internal publish route, request schema, DOM automation |
frontend |
Add platform tab, icon, i18n, account card, pre-publish preview, publish-button behavior |
extension |
If extension publishing is needed, add capability, adapter key, content script, host permission, UI config |
deploy/docs |
Add OAuth/env/secrets, runtime params, platform docs, operations notes |
Update contract first, then regenerate typed code for all affected services.
Required:
- Add the new key to
PublishPlatformincontracts/components/content.yaml. - If the platform has account configuration APIs, add account response, save request, and test response schemas in
contracts/components/account.yaml. - If the platform needs a
browser-workerscript, add request schema and internal path incontracts/components/browser-worker.yamlandcontracts/paths/browser-worker.yaml. - If adding user-facing APIs, expose them through the relevant
contracts/paths/*.yamlandcontracts/views/*.openapi.yaml.
Generate:
sh contracts/generate.shDo not edit generated files manually:
backend/internal/contracts/openapi.gen.gofrontend/src/lib/dashboard/api/generated.tsbrowser-worker/internal/contracts/openapi.gen.goai-service/contract_schemas.py
See openAPI-code-generation-guide.md.
Draft profiles are the compatibility boundary for platform content formats. The frontend editor should not understand platform-specific formats directly.
Drafts:
- Register
<platform>@v1incontent-pipeline-service/crates/content-pipeline-core/src/drafts/profiles.rs. - Add the compile branch in
content-pipeline-service/crates/content-pipeline-core/src/drafts.rs, returningAdaptedContentJSON. - Use an existing format:
html,markdown, ortext, unlessDraftFormatis extended first incontracts/components/content.yaml. - Add a snapshot fixture:
content-pipeline-service/crates/content-pipeline-core/tests/fixtures/draft_snapshots/<platform>.json. - Update Current Draft Profiles and Changelog in
content-pipeline-service/PROFILE_VERSIONS.md.
Media:
- Register media profile in
content-pipeline-service/crates/content-pipeline-core/src/media/profiles.rs. - Define
max_bytes, whether output should compress to the limit, and allowed output MIME types. - When a publisher uploads images, use
media.DownloadAndProcessForPlatform(source, "<platform>", "<usage>")instead of hard-coded generic compression rules. - Update Current Media Profiles and Changelog in
PROFILE_VERSIONS.md.
Verify:
cd content-pipeline-service
cargo test -p content-pipeline-coreBackend allowlist controls whether a project can select a platform.
Required:
- Add the key to
allowedProjectPlatformsinbackend/internal/services/project/service.go. - Confirm
NormalizeProjectPlatformsstill handles duplicates, empty values, and invalid keys correctly. - Add or update tests under
backend/internal/services/projectfor create, update, and save-platform flows.
If only frontend tabs are updated, but the allowlist is not, backend returns ErrInvalidProject.
Every auto-publishing platform must implement PlatformPublisher:
type PlatformPublisher interface {
ValidateConfig(config []byte) error
Publish(ctx context.Context, pub *models.ProjectPlatformPublication, account *models.PlatformAccount) (string, string, error)
}Recommended:
- Add
backend/internal/publisher/platforms/<platform>/. - Parse
pub.AdaptedContentin the publisher, accepting only fields emitted by Content PipelineAdaptedContent. - Keep
ValidateConfiglimited to platform publish config validation; it should not call external services. - Make
Publishreturn remote content ID and accessible URL; failure errors should include platform name and actionable cause. - Register in
backend/internal/publisher/factory.go:Factory.Register("<platform>", &<Platform>Publisher{}). - Test parsing, validation, and publishing error branches.
Publishing service resolves publishers through publisher.Factory.GetPublisher(platform) in backend/internal/services/publish/service.go. Missing registration returns no publisher registered for platform: <platform>.
Choose the smallest account implementation that matches the platform auth mode.
Official API/OAuth:
- Add platform account service file under
backend/internal/services/platform_account/. - Define credentials JSON with only fields needed for publishing.
- Provide
GetWorkspace<Platform>Account,UpsertWorkspace<Platform>Account,TestWorkspace<Platform>Account, or OAuth start/callback methods. - Merge saved credentials into publication config in
ApplySavedCredentialsToPublication, or let the publisher readaccount.Credentialsdirectly. - Add routes in
backend/internal/handlers/user-dashboard/accounts.goandbackend/cmd/api/server.go. - Update DTOs, contract, and frontend account API.
Cookie/QR-code login:
- Implement
backend/internal/publisher/browser.RemoteBrowserPlatformAdapter. - Define
LoginURL,AllowedDomains,RequiredCookies,DetectLogin, andExtractAccount. - Register adapter in
backend/internal/services/browser_session/service.go. - If publishing must load stored cookies, add the key to
usesStoredBrowserCookiesinbackend/internal/services/publish/service.go. - Test required cookies, domain suffixes, and account-extraction fallback.
Only do this when the platform needs server-side browser form filling.
Backend:
- Add or generalize
Start<Platform>PublishSession; usebackend/internal/services/publish/browser_session.goas reference. - Build the
browser-workerrequest body fromProjectPlatformPublication.AdaptedContent. - Prepare media with platform media profile.
- Add user route in
backend/internal/handlers/user-dashboard/publications.goandbackend/cmd/api/server.go.
Browser Worker:
- Add platform script under
browser-worker/internal/publish/. - Register internal route in
browser-worker/internal/server/server.go. - Declare request schema in
contracts/paths/browser-worker.yamlandcontracts/components/browser-worker.yaml. - Do not duplicate business validation in worker; worker should only perform DOM actions inside current browser session.
Platform selection and display should be metadata-driven.
Required:
- Add
PLATFORM_TABSentry infrontend/src/lib/content/platforms.ts. - Add icon at
frontend/public/icons/platforms/<platform>.svg. - Update i18n:
frontend/public/locales/zh/common.jsonandfrontend/public/locales/en/common.json. - If account connection UI is needed, add or extend
frontend/src/app/[locale]/dashboard/auth/_components/*anduse-auth-page-controller.ts. - If platform cannot auto-publish, check
AUTO_PUBLISH_PLATFORM_TABSand special branches inuse-content-publish-workflow.ts. - If platform needs custom preview, update
platform-preview.tsxand related tests.
Verify:
cd frontend
pnpm test
pnpm type-check
pnpm lintOnly do this when local browser extension publishing is needed.
Required:
- Extend
PlatformKeyandAdapterKeyinextension/src/types/platform.ts. - Add
PLATFORM_CAPABILITIESandADAPTER_SCRIPT_FILESentries inextension/src/platforms/capabilities.ts. - Add platform host permission in
extension/wxt.config.ts. - Add
extension/entrypoints/<platform>.content.tsand callregisterAdapterRunner. - Add
extension/src/adapters/<platform>.ts; keep it limited to target-page DOM fill/click/status reporting. - Update
extension/src/publish/platform-ui.tsto decide whether handoff is enabled. - Update tests for capability, tab injection, and adapter event callback.
Verify:
cd extension
pnpm test:run
pnpm compile
pnpm lintPlatform integrations often need new env vars or secrets.
- Put OAuth/API keys in
contracts/env.schema.yaml, then regenerate example env files. - If Docker/Kubernetes needs new values, update
deploy/docker/*.example,deploy/kubernetes/**/app-config.yaml, or secret materializer. - For new external domains used by remote browser, prefer platform adapter
AllowedDomains; do not broaden global network policy. - Reuse existing
platformmetrics label; avoid adding high-cardinality labels.