TIHIDI: Yes, Steve, You Can Have Prod Content Locally
Yesterday, I introduced Mockingbird - the small Linux container that pretends to be Sitecore Edge over your repo's serialized YAML. That post was the what. This one's the how.
This is a TIHIDI post - This Is How I Do It. Not a best practice. Just what I do.
Two layers, one repo, no shared CM
Two serialization layers in one repo. Two sitecore.json files. Different .gitignore postures. Different lifecycles.
One is what I call the Platform layer - templates, renderings, layouts, settings. Committed in git. The other is the Content layer that can pull from prod, never committed. Mockingbird reads both and serves them as one tree. The rendering host sees Edge.
There's a third bucket: structural anchors
The cut isn't perfectly clean. Some items have to exist in both layers, with different push semantics.
/sitecore/content/<tenant>/<site>/Home, for example, has to exist for the site to function - other items reference it as an ancestor. But Home's layout (__Final Renderings, presentation details) is editor territory. So Home is an ID stub in the platform layer, and the real Home - layout, fields, and all its descendants - lives in the content layer.
/Data and /<site>/Settings/Redirects are simpler: pure containers. Platform stubs the folder; content fills the descendants.
The pattern that lets you have it both ways is CreateOnly push semantics. In your platform module's includes:
{
"name": "site-anchors",
"path": "/sitecore/content/<tenant>/<site>/Home",
"scope": "SingleItem",
"allowedPushOperations": "CreateOnly"
},
{
"name": "site-redirects-root",
"path": "/sitecore/content/<tenant>/<site>/Settings/Redirects",
"scope": "SingleItem",
"allowedPushOperations": "CreateOnly"
}
SingleItem + CreateOnly means "create this item if missing; never overwrite if present." First deploy lays the stub. Every deploy after leaves it alone.
In your content module, the same Home path reappears at ItemAndDescendants - the item itself is editor-authored, so it lives here too:
{
"name": "site-home",
"path": "/sitecore/content/<tenant>/<site>/Home",
"scope": "ItemAndDescendants"
}
Same path, two layers, two intents. The Platform layer guarantees Home exists. The Content layer owns its fields, layout, and descendants. (For containers like /Data, content uses DescendantsOnly instead.)
| Bucket | Examples | Lives in | Push semantics |
|---|---|---|---|
| Platform | Templates, renderings, layouts, settings, role items | Platform layer (repo root), source-controlled | Create + Update + Delete |
| Structural anchors | /Home, /Data, /<site>/Settings/Redirects | Platform layer (repo root), source-controlled | CreateOnly |
| Content | Pages, news, datasource items, redirects, media | Content layer (content/), gitignored | Snapshot, pulled on demand |
What that looks like in the repo
my-project/
├── sitecore.json ← authoring layer root
├── config.mockingbird ← Mockingbird project registry (commit it)
├── src/
├── authoring/
│ └── items/ ← authoring layer items
├── content/
│ ├── sitecore.json ← content layer root
│ ├── <project>-content.module.json
│ ├── <project>-media.module.json
│ └── items/ ← gitignored
└── .gitignore ← contains: content/items/
The repo-root sitecore.json references your platform modules. Standard SCS.
The content/sitecore.json is its own animal. Two narrow modules. Content scopes to /Home (ItemAndDescendants), /Data and /Settings/Redirects (both DescendantsOnly). Media covers /sitecore/media library/Project/<tenant>/<site> and .../<tenant>/shared, both DescendantsOnly. Nothing else gets pulled.
content/items/ is gitignored - transient by design.
A working example of all of the above: LonghornTaco/mockingbird-example - North Star Outfitters, a fictional outdoor-gear site wired up with this two-layer pattern.
Pulling content into the gitignored layer
I use migrate-content.ps1 - a PowerShell wrapper around dotnet sitecore ser pull that handles auth, scoping, etc... I blogged about this the other day. Here's the invocation:
.\migrate-content.ps1 -Source prod -SkipPush
-SkipPush pulls prod content into content/items/ and stops.
Where Mockingbird closes the loop
Two layers on disk; one Edge endpoint to the rendering host. The compose file mounts your workspace; the Get Started wizard allows you to configure your scs layers:
volumes:
- ${MOCKINGBIRD_WORKSPACE}:/workspaces
Open http://localhost:3333. The Get Started wizard walks the workspace, surfaces sitecore.json files, and allows you to configure them as separate layers of one project. The selection saves to config.mockingbird at the workspace root - commit it so the next dev gets the setup for free.
Mockingbird indexes both layers and serves the union as a single GraphQL Layout Service. The rendering host doesn't know there are two layers; it just sees Edge.
The win: I can swap the content snapshot independently of the platform. Today's prod pull, last Tuesday's prod pull, a teammate's UAT pull - all different states of content/items/. The watcher picks up changes; the rendering host re-renders; the platform layer never moves.
Why this matters
The shared-cloud-CM model means every dev edits the same database. Steve breaks your page; you break Steve's. Coordinating around it is exhausting.
Two layers with different .gitignore postures fix it. Platform flows through git - branched, reviewed, deployed like code. Content snapshots flow through migrate-content.ps1 on each dev's own cadence, never committed, never shared.
Yes, Steve. You can have prod content locally. Without taking the rest of us with you.
Happy Sitecore trails, my friend!