Patterns & Practices

Guides & Tools

SHARING IS CARING

Patterns & Practices

Collection of tools and guidance meant to help you extend Microsoft 365 to your needs following best practices.

Don’t reinvent the wheel — focus on what truly matters for your organization.

Learn. Reuse. Share.

SEE INITIATIVES

White abstract geometric artwork from Dresden, Germany

Consider when Migrating SharePoint Site

Copy-PnPFile | PnP PowerShell

https:/pnp.github.io/powershell/cmdlets/Copy-PnPFile.html

Move-PnPFolder | PnP PowerShell

https:/pnp.github.io/powershell/cmdlets/Move-PnPFolder.html

#Create a form to collect the site name, subsite name, sub-subsite name, and document library name

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Download the image
if($null -eq $imagePath){
$imageUrl = "https://cspowerapplicationsstor.blob.core.windows.net/repository/cleanLogo.png"
$imagePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "cleanLogo.png")
Invoke-WebRequest -Uri $imageUrl -OutFile $imagePath}

# Create the form
$form = New-Object System.Windows.Forms.Form
$form.Text = "Copy Site Data to sites/Archive"
$form.Size = New-Object System.Drawing.Size(300, 500)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
$form.MaximizeBox = $false
$form.MinimizeBox = $false

# Create a label for site name
$labelForm = New-Object System.Windows.Forms.Label
$labelForm.Text = "Fill out approprate fields"
$labelForm.Size = New-Object System.Drawing.Size(280, 20)
$labelForm.Location = New-Object System.Drawing.Point(10, 60)
$labelForm.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelForm.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelForm)

# Create a label for site name
$labelForm2 = New-Object System.Windows.Forms.Label
$labelForm2.Text = "Library Will Remain Until Renamed"
$labelForm2.Size = New-Object System.Drawing.Size(280, 20)
$labelForm2.Location = New-Object System.Drawing.Point(10, 90)
$labelForm2.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelForm2.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelForm2)

# Load and add the image
$image = [System.Drawing.Image]::FromFile($imagePath)
$pictureBox = New-Object System.Windows.Forms.PictureBox
$pictureBox.Image = $image
$pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
$pictureBox.Size = New-Object System.Drawing.Size(260, 40)
$pictureBox.Location = New-Object System.Drawing.Point(10, 10)
$form.Controls.Add($pictureBox)

# Create a label for site name
$labelSite = New-Object System.Windows.Forms.Label
$labelSite.Text = "Site Name:"
$labelSite.Size = New-Object System.Drawing.Size(280, 20)
$labelSite.Location = New-Object System.Drawing.Point(10, 120)
$labelSite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSite)

# Create a text box for site name
$textBoxSite = New-Object System.Windows.Forms.TextBox
$textBoxSite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSite.Location = New-Object System.Drawing.Point(10, 150)
$form.Controls.Add($textBoxSite)

# Create a label for subsite name
$labelSubsite = New-Object System.Windows.Forms.Label
$labelSubsite.Text = "Subsite Name (optional):"
$labelSubsite.Size = New-Object System.Drawing.Size(280, 20)
$labelSubsite.Location = New-Object System.Drawing.Point(10, 180)
$labelSubsite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSubsite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSubsite)

# Create a text box for subsite name
$textBoxSubsite = New-Object System.Windows.Forms.TextBox
$textBoxSubsite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSubsite.Location = New-Object System.Drawing.Point(10, 210)
$form.Controls.Add($textBoxSubsite)

# Create a label for sub-subsite name
$labelSubSubsite = New-Object System.Windows.Forms.Label
$labelSubSubsite.Text = "Sub-Subsite Name (optional):"
$labelSubSubsite.Size = New-Object System.Drawing.Size(280, 20)
$labelSubSubsite.Location = New-Object System.Drawing.Point(10, 240)
$labelSubSubsite.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labelSubSubsite.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labelSubSubsite)

# Create a text box for sub-subsite name
$textBoxSubSubsite = New-Object System.Windows.Forms.TextBox
$textBoxSubSubsite.Size = New-Object System.Drawing.Size(260, 20)
$textBoxSubSubsite.Location = New-Object System.Drawing.Point(10, 270)
$form.Controls.Add($textBoxSubSubsite)

# Create a label for document Library name
$labeldocLibrary = New-Object System.Windows.Forms.Label
$labeldocLibrary.Text = "Document Library: $documentLibrary"
$labeldocLibrary.Size = New-Object System.Drawing.Size(280, 20)
$labeldocLibrary.Location = New-Object System.Drawing.Point(10, 300)
$labeldocLibrary.Font = New-Object System.Drawing.Font("Arial", 10, [System.Drawing.FontStyle]::Bold)
$labeldocLibrary.ForeColor = [System.Drawing.Color]::Teal
$form.Controls.Add($labeldoclibrary)

# Create a text box for document Library name
$textdocLibrary = New-Object System.Windows.Forms.TextBox
$textdocLibrary.Size = New-Object System.Drawing.Size(260, 20)
$textdocLibrary.Location = New-Object System.Drawing.Point(10, 330)
$textdocLibrary.Text = $documentLibrary  # Set default value
$form.Controls.Add($textdocLibrary)

# Create a button
$button = New-Object System.Windows.Forms.Button
$button.Text = "OK"
$button.Size = New-Object System.Drawing.Size(75, 23)
$button.Location = New-Object System.Drawing.Point(100, 370)
$form.Controls.Add($button)

# Add event handler for text box text changed event
$textdocLibrary.Add_TextChanged({
    if ([string]::IsNullOrWhiteSpace($textdocLibrary.Text)) {
        $button.Enabled = $false  # Enable the button if text box is not empty
    } else {
        $button.Enabled = $true  # Disable the button if text box is empty
    }
})

if ($null -eq $documentLibrary) {
$button.Enabled = $false  # Disable the button initially
}

# Define the button click event
$button.Add_Click({
    $form.Tag = @{
        SubSitesiteName = $textBoxSubSubsite.Text
        SiteName = $textBoxSite.Text
        SubsiteName = $textBoxSubsite.Text
        DocLibrary = $textdocLibrary.Text
    }
    $form.Close()
})

# Show the form
$form.ShowDialog()

# Output the collected data
$form.Tag

# Get the site and subsite names from the form

$siteName = $form.Tag.SiteName -replace '\s', ''
# Output the site name
Write-Host "Site Name entered: $siteName"

$subsiteName = $form.Tag.SubsiteName -replace '\s', ''
if ($form.Tag.SubsiteName) {
Write-Host "Site Name entered: $subsiteName"
}
$subSubsiteName = $form.Tag.SubSitesiteName -replace '\s', ''
if ($form.Tag.SubSitesiteName) {
Write-Host "Site Name entered: $subSubsiteName"
}

if ($form.Tag.DocLibrary) {
    $documentLibrary = $form.Tag.DocLibrary -replace '\s', ''
    } 
    else {
    $documentLibrary = $documentLibrary
    }
Write-Host "Document Library: $documentLibrary"

#Set Variables
$contentType = ""
$listsFolder = ""
$clinetId = "ac16ec23-e5e2-4fbb-8eda-108e5f760593"
$tenant = "CleanSlateCenter.onmicrosoft.com"
$thumbprint = "C53191C3422366FF6C5D38AB999ECDA3E549DF82"
$siteBaseUrl = "https://cleanslatecenter.sharepoint.com/"
$siteDestinationURL = "sites/Archive"
$siteMigrateUrl = $siteBaseUrl + $siteDestinationURL

$siteSourceBaseUrl = if ($subsiteName) {
    if ($subSubsiteName) {
        $siteName + "/" + $subsiteName + "/" + $subSubsiteName
    } else {
        $siteName + "/" + $subsiteName
    }
} else {
    $siteName
}

$subsite = $siteSourceBaseUrl -replace "/", "_"

#$siteSourceBaseUrl = $form.Tag.SiteName 
$siteDestinationFolder = "$documentLibrary/$subsite"
$listsFolder = $siteDestinationFolder + "/Lists"
$doclistName = $siteDestinationFolder.Split('/')[0]
$siteDestinationRoot = $siteDestinationURL + "/" + $siteDestinationFolder -replace "/$subsite", ''
$siteDestinationFullPath = $siteDestinationURL + "/" + $siteDestinationFolder 
$siteUrl = $siteBaseUrl + $siteSourceBaseUrl
$contentType = $subsite + "ContentTypeTemplate.xml"

     # Connect to the destination site
     Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint

     # Check if the document library exists in the destination site
     $libraryExists = Get-PnPList -Identity $documentLibrary -ErrorAction SilentlyContinue
 
         if (-not $libraryExists) {
             # Create the document library in the destination site
             New-PnPList -Title $documentLibrary -Template DocumentLibrary
         }
 
     # Create the field Creators and Last Update by if it doesn't exist
     # Check if the field exists
     $field = Get-PnPField -List $doclistName -Identity "LastUpdateBy" -ErrorAction SilentlyContinue
         
         if ($null -eq $field) {
             Add-PnPField -List $doclistName -DisplayName "LastUpdateBy" -InternalName "LastUpdateBy" -Type Text
             Write-Host "Field 'LastUpdateBy' created."
         } else {
             Write-Host "Field 'LastUpdateBy' already exists."
         }
 
     $field = Get-PnPField -List $doclistName -Identity "Creator" -ErrorAction SilentlyContinue
 
         if ($null -eq $field) {
             Add-PnPField -List $doclistName -DisplayName "Creator" -InternalName "Creator" -Type Text
             Write-Host "Field 'Creator' created."
         } else {
             Write-Host "Field 'Creator' already exists."
         }
 
     # Check if the folder exists in the destination site
     $folderExists = Get-PnPFolder -Url $siteDestinationFolder -ErrorAction SilentlyContinue
 
         if (-not $folderExists) {
             # Create the folder in the destination site
             Add-PnPFolder -Name $subsite -Folder "/$siteDestinationRoot"
             Add-PnPFolder -Name "Lists" -Folder "$siteDestinationFolder"
         }
 
         # Collect the folders and files from the source site    
         try {
         
     # Connect to the source site
     Connect-PnPOnline -Url $siteUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
 
             # Retrieve all lists including the properties you'll need for filtering.
             $lists = Get-PnPList -Includes Title, BaseTemplate, Hidden, RootFolder.ServerRelativeUrl
 
             $documentLibraries = $lists | Where-Object {
                 $_.BaseTemplate -eq 101 -or 
                 (-not $_.Hidden)  -and ($_.Title -notmatch 'Health & Safety Documents') -and 
                 ($_.Title -notmatch 'Forms') -and ($_.Title -notmatch 'Workflow Tasks') -and 
                 ($_.RootFolder.ServerRelativeUrl -notmatch 'Lists')
             }            
 
             # Initialize an array to store all folders
 
             $folders = @()
             $allfolders = @()
             $docLibraries = @()
             $folderfiles = @()
             $topDocumentFolders = @()
             $listsSections = @()
             $files1stLevel = @()    
             $rootLists = @()                    
 
                 # Loop through each document library and get folders
                 foreach ($library in $documentLibraries) { #$library = $documentLibraries[1]
                     $libraryRootFolder = Get-PnPFolder -Url $library.RootFolder.ServerRelativeUrl 
                     $RootCellar = $libraryRootFolder.Name #$library.Title
                     $docLibraries += $libraryRootFolder 
             
                     # Get all top level folders in the document library
                     $docsRootDir = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType Folder 
                     $docsRootDir = $docsRootDir | Where-Object { $_.Name -ne "Forms" }
                     $topDocumentFolders += $docsRootDir 
                     
                     # Get all top level files in the document library
                     $libraryItems = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType File -Recursive
                     $libraryLevelFiles = Get-PnPFolderItem -FolderSiteRelativeUrl $RootCellar -ItemType File 
                     $siteFiles = $libraryItems | Where-Object {$libraryLevelFiles -notcontains $_}
 
                     # Acquire the properties of the files
                     foreach ($item in $libraryLevelFiles) {
                         $listItem = Get-PnPProperty -ClientObject $item -Property ListItemAllFields
                     
                         if ($null -ne $listItem.Id){ #($listItem -and $null -ne $listItem.Id) {
                             $folderfiles += $listItem    
                             $files1stLevel += $listItem
                         }
                     }
 
                     foreach ($item in $siteFiles) {
                     $fileItem = Get-PnPProperty -ClientObject $item -Property ListItemAllFields
             
                         if ($null -ne $fileItem.Id) {
                             $folderfiles += $fileItem    
                         }
                     }
                 }
                 
                 # Retrieve all lists and libraries
                 $lists = Get-PnPList
 
                 # Filter lists that are in the root directory
                 $rootLists = $lists | Where-Object {-not $_.Hidden -and $_.Title -notmatch "Tasks" -and $_.Title -notmatch "Documents"} 
                 $listFolders = $rootLists | Where-Object {$_.RootFolder.ServerRelativeUrl -notmatch "Lists"} 
                 $listsSections = $rootLists | Where-Object {$listFolders -notcontains $_}
 
             # Extract the content types and lists from the source site
             Get-PnPSiteTemplate -Out $contentType -Handlers ContentTypes -Force
             
             }
             catch {
                 # Handle the error
                 Write-Host "Error details: $_"
                 exit
             }
 
     # Connect to the destination site
     Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint 
 
         # Apply the template to the destination site
         # Invoke-PnPSiteTemplate -Path $contentType -Verbose
         
         # Create the document librarry folders in the destination site
         foreach ($library in $docLibraries) {
             $RootCellar = $library.Name 
             $libraryLanding = $siteDestinationFolder + "/" + $RootCellar   
 
             # Check if the folder exists in the destination site
             $folderExists = Get-PnPFolder -Url $libraryLanding -ErrorAction SilentlyContinue
             if (-not $folderExists) {
                 # Create the folder in the destination site
                 Add-PnPFolder -Name $RootCellar -Folder $siteDestinationFolder 
             }
             $allfolders += $library    
         }
 
         # Copy each file to the root directories in destination site
         foreach ($file in $files1stLevel) { 
             $fieldValues = $file.FieldValues
             $title = $fieldValues["Title"]
             #$pageName = $fieldValues["FileLeafRef"] 
             $SourcePageURL = $fieldValues["FileRef"] 
             $SourceUrl = $fieldValues["FileDirRef"] 
             $pageLanding = $SourceUrl -replace "^/$siteSourceBaseUrl", $siteDestinationFolder 
             $pagetoCopy = $SourcePageURL -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder" 
             
             # Check if the file exists in the destination site
             $fileExists = Get-PnPFile -Url $pagetoCopy -ErrorAction SilentlyContinue
             if ($fileExists) {
                 Write-Host "File already exists in the destination site." -ForegroundColor Yellow
                 continue
             }
             else {
             Copy-PnPFile -SourceUrl $SourcePageURL -TargetUrl $pageLanding -Force
             Write-Host "File copied successfully." 
             }
         }
 
         # Add the folder depth field values to the files
         Function InitializeAndAddField {
             param (
                 [array]$folders,
                 [string]$fieldName
             )
             $folders | ForEach-Object {
                 if (-not $_.PSObject.Properties[$fieldName]) {
                     $_ | Add-Member -MemberType NoteProperty -Name $fieldName -Value ($_.ServerRelativeUrl -split '/').Count
                 } else {
                     $_.$fieldName = ($_.ServerRelativeUrl -split '/').Count
                 }
             }
             return $folders
         }
         
         $topDocumentFolders = InitializeAndAddField -folders $topDocumentFolders -fieldName "FolderDepth"
         $topDocumentFolders = $topDocumentFolders  | Where-Object { $_.Name -ne "Forms" }
         foreach($top in $topDocumentFolders){
             Write-Host "Folder: $($top.ServerRelativeUrl) Depth: $($top.FolderDepth)"
         }
 
         # Function to recursively process folders and their subfolders
     function Copy-FoldersRecursively {
         param (
             [array]$folders
         )
     
         foreach ($folder in $folders) { 
             # Process the current folder
             $folderUrl = $folder.ServerRelativeUrl -replace "^/$siteDestinationFullPath", "/$siteSourceBaseUrl" 
             $lastSlashIndex = $folderUrl.LastIndexOf('/')
             $folderPath = $folderUrl.Substring(0, $lastSlashIndex)
             $itemCount = $folder.ItemCount
             Write-Output "Processing folder: $folderUrl"
             
             # Replace the source URL with the destination URL
             $destinationUrl = $folderPath -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
             $nextLevelFolder = $folderUrl -replace "^/$siteSourceBaseUrl", "$siteDestinationFolder"
             
             try {
                 if($itemCount -ne 0){
             
                     Copy-PnPFolder -SourceUrl $folderUrl -TargetUrl $destinationUrl -Force -Overwrite
                     Write-Host "Copied folder from $folderUrl to $destinationUrl" -ForegroundColor Green
                 } else {
                     # Create the empty folder at the destination
                     Add-PnPFolder -Name (Split-Path $folderUrl -Leaf) -Folder $destinationUrl -ErrorAction SilentlyContinue
                     Write-Host "Created empty folder at $destinationUrl" -ForegroundColor Yellow
                 }
                 } catch {
                     Write-Host "Error copying or creating folder from $folderUrl to $destinationUrl" -ForegroundColor Blue
                     Write-Host "Error details: $_" -ForegroundColor Magenta
                 }
     
             Start-Sleep -Seconds 5
             # Get subfolders of the current folder
             $subfolders = Get-PnPFolderItem -FolderSiteRelativeUrl $nextLevelFolder -ItemType Folder -ErrorAction SilentlyContinue
             if ($subfolders) {
                 # Log retrieved subfolders for debugging
                 Write-Output "Retrieved subfolders for $($folderUrl): $($subfolders | ForEach-Object { $_.ServerRelativeUrl })"
                 
                 # Filter out subfolders named "Forms"
                 $filteredSubfolders = $subfolders | Where-Object { $_.Name -ne "Forms" }
                 
                 # Recursively process subfolders
                 Copy-FoldersRecursively -folders $filteredSubfolders
             } else {
                 Write-Output "No subfolders found for $folderUrl"
             }
         }
     }
 
     # Measure the execution time of the Copy-FoldersRecursively function
     $executionTime = Measure-Command {
         Copy-FoldersRecursively -folders $topDocumentFolders
     }
 
     # Display the elapsed time
     Write-Host "The Copy-FoldersRecursively function took $($executionTime.TotalSeconds) seconds to complete."
 
     # If needed, add a sleep based on the measured time
     if ($executionTime.TotalSeconds -lt 5) {
         Write-Host "Adding a sleep of 5 seconds to ensure all files are available for update."
         Start-Sleep -Seconds 5
     }
 
     foreach ($item in $folderfiles) { #$item = $folderfiles[10]
                 $fieldValues = $null
                 $fieldValues = $item.FieldValues
                 $title = $fieldValues["Title"]
                 $itemId = $fieldValues["ID"]
             if($null -ne $itemId) {
                 $userEditor = ""
                 $userCreator = ""
 
                 # Access metadata
                 $created = $fieldValues["Created"]
                 $modified = $fieldValues["Modified"]
                 $author = $fieldValues["Author"]
                 $editor = $fieldValues["Editor"]
                 $userCreated = $author.Email
                 $userEdited = $editor.Email
                 $fileName = $fieldValues["FileLeafRef"]   
                 $fileUrl = $fieldValues["FileDirRef"]
                 
                 $destinationUrlFile = $fileUrl -replace "^/$siteSourceBaseUrl", "/$siteDestinationFullPath"
                 #$archiveFolder = $siteDestinationFullPath -replace "/$subsite", "" 
                 $DestinationFileName = $destinationUrlFile + "/" + $fileName 
                 $NewFile = Get-PnPFile -Url $DestinationFileName -AsListItem 
                 if ($null -ne $NewFile.Id) {
                 $values = @()           
                     # Prepare the values to update
                     $values = @{
                         "Creator" = $userCreated
                         "Created" = $created
                         "LastUpdateBy" = $userEdited
                         "Modified" = $modified
                     }
 
                 $pnpUser = "i:0#.f|membership|" + $userEdited
                 $userEditor = Get-PnPUser -Identity $pnpUser -ErrorAction SilentlyContinue
                 if ($userEditor) {
                     $values["Editor"] = $userEdited
                 }
                 $pnpCreator = "i:0#.f|membership|" + $userCreated
                 $userCreator = Get-PnPUser -Identity $pnpCreator -ErrorAction SilentlyContinue
                 if ($userCreator) {
                     $values["Author"] = $userCreated
                 }
 
                 # Update the metadata on the new file with the modified and modified by source values
                
                     Write-Host "Updating metadata for file: $DestinationFileName with ID: $($NewFile.Id)"
 
                     Set-PnPListItem -List $doclistName -Identity $NewFile.Id -Values $values
                 }
             }
         }
 
 
     # Copy the lists and libraries to the destination site
     # Connect to the source site
     Connect-PnPOnline -Url $siteUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint
 
     foreach($root in $listsSections | Where-Object { $_.Title -notmatch "QuickLinks" -and $_.Title -notmatch "Page Content" -and $_.Title -notmatch "Microfeed"}){
     # Define the SharePoint list name
         $listName = ""
         $listXml = ""    
         $listName = $root.Title
         $listXml = $subsite + $listName + ".xml"
         #Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint 
             
     # Extract the content types and lists from the source site
     Get-PnPSiteTemplate -Out  $listXml -ListsToExtract $listName -Handlers Lists -Force # $listName -Handlers Lists
     Add-PnPDataRowsToSiteTemplate -Path $listXml -List $listName 
     }       
 
     # Connect to the destination site
     Connect-PnPOnline -Url $siteMigrateUrl -ClientId $clinetId -Tenant $tenant -Thumbprint $thumbprint        
 
     foreach($root in $listsSections | Where-Object { $_.Title -notmatch "QuickLinks" -and $_.Title -notmatch "Page Content" -and $_.Title -notmatch "Microfeed"}){
         # Define the SharePoint list name
             $listName = ""
             $listCreating = ""
             $listUrl = ""
             $listXml = ""    
             $listName = $root.Title
             $listCreating = $subsite + "_" + $listName
             $listUrl = $siteUrl + "/Lists/" + $listName 
             $listXml = $subsite + $listName + ".xml"
 
     # Upload the template XML to the destination site
 
     if($listXml -ne "") {     
     Add-PnPFile -Path $listXml -Folder $listsFolder 
     <#try {
         Set-PnPList -Identity $listName -Title $listCreating
         Write-Output "Successfully renamed list: $listName to $listCreating"
     } catch {
         Write-Output "Failed to rename list: $listName"
         Write-Output "Error: $($_.Exception.Message)"
     }#>
     Remove-Item -Path $listXml -Force
     }
 }
 Add-PnPFile -Path $contentType -Folder $listsFolder 
 Remove-Item -Path $contentType -Force

Guiding your business through the project

Experience the fusion of imagination and expertise with Études—the catalyst for architectural transformations that enrich the world around us.

Meet our team

Our comprehensive suite of professionals caters to a diverse team, ranging from seasoned architects to renowned engineers.

Francesca Piovani

Founder, CEO & Architect

Rhye Moore

Engineering Manager

Helga Steiner

Architect

Ivan Lawrence

Project Manager

We’ve worked with some of the best companies.

Enhance your architectural journey with the Études Architect app.

  • Collaborate with fellow architects.
  • Showcase your projects.
  • Experience the world of architecture.
White abstract geometric artwork from Dresden, Germany