Site Structure & Pages as Code: What Git Can (and Can't) Own
Lesson 9: Site Structure & Pages as Code โ What Git Can (and Can't) Own
Lesson 8 shipped your components. This lesson is about shipping the site itself: lists, navigation, and the modern pages people land on. The good news: site structure is beautifully scriptable from a repo. The honest news, up front: full page content round-tripping to git is not a real thing โ and the community consensus is that it shouldn't be.
.aspx files, are edited constantly by content editors, and break on git round-trip. The bridge between the two is scripted page provisioning: you keep page recipes in the repo, a pipeline builds the pages, and editors take over from there.
Why page content won't round-trip
Every modern page lives in the SitePages library as an .aspx file, but the actual content (text, web part instances, layout) is embedded as a serialized client-side data model โ not human-editable markup. SharePoint has no export API for it as portable content, and the schema shifts as Microsoft updates web parts. Export โ edit โ import reliably corrupts or loses things. So nobody sane stores page content in git; they store the instructions to create it.
Mechanism 1: PnP Provisioning Templates (structure)
A PnP template is a portable XML/JSON description of a site that the provisioning engine can apply anywhere. Capture it from a well-built "golden" site, commit it, and replay it on every new site โ in a pipeline, at scale.
| Captured by templates | Not captured |
|---|---|
| Lists & libraries (schema, views, content types), fields with validation, navigation, files bundled as assets, site settings, branding, page metadata (name, layout, publish state) | Modern page content โ web part configuration and text. Those must be scripted separately (Mechanism 3). |
# Extract a template from a golden site (PnP PowerShell)
Get-PnPSiteTemplate -Out "./provisioning/team-site.xml" `
-Handlers Lists,Fields,ContentTypes,Navigation,Files,SiteSettings,Branding
# Apply it to a new site
Invoke-PnPSiteTemplate -Path "./provisioning/team-site.xml" -ClearNavigation
site template command (older blog posts claiming one are stale). Run template apply steps with pwsh + PnP.PowerShell in your pipeline, e.g. through the PnP PowerShell OIDC action from Lesson 8.
Mechanism 2: Site scripts + site designs (lightweight, Microsoft-native)
Site scripts are JSON "verb recipes" (create a list, add a field, set the theme, add navigation) registered with the tenant and bundled into a site design that appears in the SharePoint "Create site" UI. They're perfect for lightweight, repeatable provisioning with zero PowerShell, and they can't touch page content either โ configuration only.
// site-scripts/project-site.json
{
"$schema": "schema.json",
"actions": [
{ "verb": "createSPList", "listName": "Projects", "templateType": 100 },
{ "verb": "addSPField", "listName": "Projects", "fieldType": "Text", "displayName": "Owner" },
{ "verb": "setSiteTheme", "themeName": "Contoso" }
]
}
# Register and wire it up with CLI for Microsoft 365
m365 spo sitescript add --title "Project site" --content "./site-scripts/project-site.json"
m365 spo sitedesign add --title "Project design" --webTemplate 64 `
--siteScripts "<script-guid-from-previous-command>"
Choosing: site scripts win when the goal is "anyone can create a consistent site from the UI" and the scope is small. PnP templates win when you need real depth (views, files, complex content types, repeat application in CI/CD). Many shops use both: a site design for creation, then a template apply for the full structure.
Mechanism 3: Page recipes โ scripted page provisioning
For pages that must exist with a known structure (home page, department landing pages, documentation hubs), the accepted pattern is a page recipe checked into the repo: a small script that creates the page, adds text and web parts into defined sections, and publishes it.
# pages/home.ps1 โ a "recipe" your pipeline runs
$page = Add-PnPPage -Name "Home" -LayoutType Article
Add-PnPPageTextPart -Page $page -Section 1 -Column 1 `
-Text "<h1>Welcome to Engineering</h1><p>Start here for team resources.</p>"
Add-PnPPageWebPart -Page $page -Section 1 -Column 2 `
-ComponentId "490d5c76-18ea-4d9d-812a-254b51748115" ` # text web part
-Properties @{ Title = "Announcements" }
Set-PnPPage -Identity $page -Published $true
The CLI equivalent covers the same ground: m365 spo page add --name Home.aspx --layoutType Article, then m365 spo page text add, m365 spo page clientsidewebpart add, and m365 spo page publish --id โฆ (plus page list, page set, page copy for management).
Get-PnPClientSidePage -Identity Home / m365 spo page list), skip or update deliberately, and never let a pipeline blindly recreate a page that editors may have customized since provisioning. A common rule: recipe owns the page until the first human edit โ then SharePoint owns it.
๐ง Knowledge Check
1. What does a PnP Provisioning Template reliably capture from a site?
2. Why isn't modern page content stored in git as a rule?
3. What is a "page recipe" in the scripted-provisioning pattern?