Sitecore Development Azure VM Setup – A Scripted Approach

In this episode, we will be using Azure PowerShell, Chocolatey, the Sitecore Installation Framework to set up a virtual machine. This post is not an end-to-end tutorial but points out some of the main concepts needed to get there. A full implementation can be found in my Azure Virtual Machine for Sitecore Development repository on GitHub.

Getting Started

Make sure you have the following software installed on your local development environment:

If you are unaware of why you need Visual Studio Code instead of just using the PowerShell ISE, read this article about PowerShell Core 6.0.

Make sure you have installed everything correctly by connecting to azure in the Visual Studio Code’s built-in terminal using Connect-AzAccount.

Connect-AzAccount

Create the Virtual Machine

You can manually create this virtual machine in the Azure portal, or you can read my previous post on how to Create an Azure VM with ports actually working via Azure PowerShell. Once created, you should see these resources in your resource group:

Resources created after VM creation.

Installing Software

Now that we have the VM setup, we will use Chocolatey to install the software. A full list of packages can be found on the Chocolatey Packages page.

Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

$ChocoPackages = "googlechrome", "visualstudio2017community", "git", "notepadplusplus.install" , "scala"

ForEach ($PackageName in $ChocoPackages) {
    choco install $PackageName -y
}

Now you might be wondering, “Won’t executing this install the software on my local machine?” The answer to that is YES. To get the choco install to run on the Azure VM, we will leverage Set-AzVMCustomScriptExtension.

#Make this object as you will use it A LOT
$Vm = Get-AzVM -Name $VmName -ResourceGroupName $ResourceGroup

Set-AzVMCustomScriptExtension  `
        -ResourceGroupName $Vm.ResourceGroupName `
        -VmName $Vm.Name `
        -Location $Vm.Location `
        -FileUri "$BaseFileUri/InstallSoftware.ps1" `
        -Run "InstallSoftware.ps1" `
        -Name "VmInstallSoftware" `
        -Argument "-VmDownloadFolder $VmDownloadFolder"

The Set-AzVMCustomScriptExtension will download the PowerShell scripts from an accessible URL, such as a public blob storage. You will likely want to pass any secure variables via the Argument parameter.

It is important to note that AzVMCustomScriptExtension creates a string of the PowerShell script it will execute remotely. You may want to encode/decode the variables you are passing.

Enable PowerShell Remoting and Using the Custom Script Extension

Before we can install Sitecore, we will need to get PowerShell remoting enabled so that we can copy the license.xml file to the server. To do this, you can call Set-AzCustomScriptExtension again. Note that you need to pass the same script extension Name parameter as you can only have one.

Set-AzVMCustomScriptExtension  `
     -ResourceGroupName $Vm.ResourceGroupName `
     -VmName $Vm.Name `
     -Location $Vm.Location `
     -FileUri "$BaseFileUri/InstallPsRemoting.ps1" `
     -Run "InstallPsRemoting.ps1" `
     -Name "VmInstallSoftware"

The contents of InstallPsRemoting.ps1 would Enable-PSRemoting and open the Windows firewall. Again, this file would be stored in an accessible location such as a public Azure container. I am also taking this execution opportunity to install IIS.

Enable-PSRemoting -Force
netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5985 protocol=TCP action=allow

if ((Get-WindowsFeature Web-Server).InstallState -ne "Installed") {
    Install-WindowsFeature -Name "Web-Server" -IncludeAllSubFeature -IncludeManagementTools
}

While this opens up connectivity on the virtual machine, you’ll also need to set this up on your local instance with the public IP of the virtual machine.

#Get the public IP of your VM
$VmPublicIpAddress = Get-AzPublicIpAddress -Name $Vm.Name

#Update your trustedhosts
Set-Item WSMan:localhost\client\trustedhosts -value $VmPublicIpAddress.IpAddress -Force

#Update your local firewall for WinRM"
netsh advfirewall firewall show rule name="WinRM-HTTP" | netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5985 protocol=TCP action=allow

#Enable-PSRemoting on your local
Enable-PSRemoting -SkipNetworkProfileCheck -Force

Once this is configured, you can simply use PSSession to copy the file from your local to the VM.

$Cred = New-Object System.Management.Automation.PSCredential ($VmUsername, $VmUserPassword)

$Session = New-PSSession -ComputerName $VmPublicIpAddress.IpAddress -Port 5985 -Credential $Cred

Copy-Item -Path "license.xml" -Destination $SCInstallRoot -ToSession $Session

While you can use Invoke-Command with the $Session to run local scripts remotely, consider the reasons to use AzVMCustomScriptExtension instead.

For my purposes, this script potentially could be run by a number of developers globally. It made more sense for my implementation that these remotely executed PowerShell scripts remained consistent. Using AzVMCustomScriptExtension, most of the execution functions will live in a managed public storage account reducing the risk of them being changed as well as providing less complexity upfront to the users executing the scripts.

Installing Prerequisites and Sitecore

For this specific implementation, I am installing Sitecore 9.1 Update-1, which requires SQL 2016 SP2/2017, and Solr 7.2.1.

This means we will also need to install the Sitecore Install Framework and install the prerequisites on the virtual machine:

$SitecorePSRepository = "https://sitecore.myget.org/F/sc-powershell/api/v2"

$SifModule = Get-InstalledModule SitecoreInstallFramework -AllVersions -ErrorAction SilentlyContinue
if([string]::IsNullOrEmpty($SifModule)) {
    Register-PSRepository –Name SitecoreRepo –SourceLocation $SitecorePSRepository -InstallationPolicy Trusted

    Install-Module SitecoreInstallFramework -Force
}

Import-Module SitecoreInstallFramework

Install-SitecoreConfiguration 'C:\VmSoftware\Sitecore\prerequisites.json' -Verbose

For Solr, let’s make life easy and use Jeremy Davis‘ Low Effort Solr Install.
For SQL, we will use Brad Christie’s SQL SIF Installer.

Installing SQL may take a while this way as it downloads it from Microsoft. I think it might be worthwhile to create a private repository in Azure, place the download there, and provide access to the VM to increase the speed. However, I haven’t had the opportunity to validate the improvement.

$SqlInstallParams = @{
    SqlExpressDownload = "https://download.microsoft.com/download/4/1/A/41AD6EDE-9794-44E3-B3D5-A1AF62CD7A6F/sql16_sp2_dlc/en-us/SQLEXPR_x64_ENU.exe"
    SqlAdminPassword = $SqlAdminPassword
    TempLocation = $VmDownloadPath
    Path = "sqlexpress.json"
}
try {
    Install-SitecoreConfiguration @SqlInstallParams
} catch [System.Data.SqlClient.SqlException] {
#The SQL installer produces a "needs restart" error which stops the script.  We want to continue
    if( $_.Exception.Number -eq 3021) {
        continue
    }
}

Sitecore can be installed as usual with the XP0-SingleDeveloper.ps1. I have modified mine to decode all the variables. As a reminder, AzVMCustomScriptExtension creates a string of the PowerShell script it will execute remotely. I found it easiest to go ahead and encode all my variables passed to this specific file and decode them before execution.

function Get-Decoded {
    [CmdletBinding()]
    param(
        [String]$Str
    )
    return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Str))
}


$DeSCInstallRoot = Get-Decoded -Str $SCInstallRoot

# Install XP0 via combined partials file.
$singleDeveloperParams = @{
    Path = "$DeSCInstallRoot\XP0-SingleDeveloper.json"
    SqlServer = Get-Decoded -Str $SqlServer
    SqlAdminUser = Get-Decoded -Str $SqlAdminUser
    SqlAdminPassword = Get-Decoded -Str $SqlAdminPassword
    SitecoreAdminPassword = Get-Decoded -Str $SitecoreAdminPassword
    SolrUrl = Get-Decoded -Str $SolrUrl
    SolrRoot = Get-Decoded -Str $SolrRoot
    SolrService = Get-Decoded -Str $SolrService
    Prefix = Get-Decoded -Str $Prefix
    XConnectCertificateName = Get-Decoded -Str $XConnectSiteName
    IdentityServerCertificateName = Get-Decoded -Str $IdentityServerSiteName
    IdentityServerSiteName = Get-Decoded -Str $IdentityServerSiteName
    LicenseFile = Get-Decoded -Str $LicenseFile
    XConnectPackage = Get-Decoded -Str $XConnectPackage
    SitecorePackage = Get-Decoded -Str $SitecorePackage
    IdentityServerPackage = Get-Decoded -Str $IdentityServerPackage
    XConnectSiteName = Get-Decoded -Str $XConnectSiteName
    SitecoreSitename = Get-Decoded -Str $SitecoreSiteName
    PasswordRecoveryUrl = Get-Decoded -Str $PasswordRecoveryUrl
    SitecoreIdentityAuthority = Get-Decoded -Str $SitecoreIdentityAuthority
    XConnectCollectionService = Get-Decoded -Str $XConnectCollectionService
    ClientSecret = Get-Decoded -Str $ClientSecret
    AllowedCorsOrigins = Get-Decoded -Str $AllowedCorsOrigins
}

Push-Location $DeSCInstallRoot

Install-SitecoreConfiguration @singleDeveloperParams *>&1 | Tee-Object XP0-SingleDeveloper.log

Pop-Location

Restarting the Virtual Machine

There are at least two times I needed to restart the machine during the install. One is after installing the Software via Chocolatey because of the .NET installs required for Visual Studio. The other is after installing Solr and MSSQL as the Solr script updates the Windows Environment Variables and MSSQL’s dependencies for a restart. To do this in your local script, you can simply execute the following:

Restart-AzVM -Name $Vm.Name -ResourceGroupName $Vm.ResourceGroupName

Final Thoughts

Again, this has not shown a full implementation, but a sampling of things you might need to do. A full implementation can be found at Azure Virtual Machine for Sitecore Development on GitHub.

You can absolutely do most, if not all of this, with an ARM template and Azure Automation. In fact, those might be a better option for you and your organization.

If you decide to put things on a public container, please do not put your license file or any other sensitive information on the public cloud.

Create an Azure VM with ports actually working via Azure PowerShell

Ever spun up an Azure virtual machine just to realize it doesn’t have the right ports open? Then, after 30 minutes of rolling your face on the keyboard, updating, restarting, and punching your monitor, it still doesn’t work? Just do it with Azure PowerShell.

$ResourceGroup = "YourResourceGroupName"
$VmName = "YourVirtualMachineName"
$Location = "eastus"
$ImageName = "Win2019Datacenter"
$VmSize = "Standard_B4ms"

$VmUsername = "icanconnectuser"
$VmUserPassword = ConvertTo-SecureString "SuperP@ssW0rd" -AsPlainText -Force

$credential = New-Object System.Management.Automation.PSCredential ($VmUsername, $VmUserPassword)

$NewAzureVmParams = @{
            ResourceGroupName = $ResourceGroup
            Name = $VmName
            Location = $Location
            ImageName = $ImageName
            Size = $VmSize
            Credential = $credential
            #OpenPorts = 80, 443, 3389
        }

#This creates your VM!
New-AzVM @NewAzureVmParams

Now at this point, you’ve just created the VM and you still can’t connect. Notice I’ve commented out the OpenPorts parameter. I’ve done this because, well… it doesn’t mean it updates the network security group and still won’t work. Updating the network security group will create and update the necessary pieces in Azure for your connections to work.

#Create a VM object to make life easier
$Vm = Get-AzVM -Name $VmName -ResourceGroupName $ResourceGroup

#Get the network security group
$NSG = Get-AzNetworkSecurityGroup -Name $Vm.Name

#Create the rules you want
$RuleRdp = New-AzNetworkSecurityRuleConfig -Name AllowRdp -Description "Allow RDP" `
-Access Allow -Protocol Tcp -Direction Inbound -Priority 100 -SourceAddressPrefix `
Internet -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 3389

#Add it to the network security group
$NSG.SecurityRules.Add($RuleRdp)

$RuleHttp = New-AzNetworkSecurityRuleConfig -Name AllowHttp -Description "Allow HTTP" `
-Access Allow -Protocol Tcp -Direction Inbound -Priority 101 -SourceAddressPrefix `
Internet -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 80

$NSG.SecurityRules.Add($RuleHttp)

$RuleHttps = New-AzNetworkSecurityRuleConfig -Name AllowHttps -Description "Allow HTTPS" `
-Access Allow -Protocol Tcp -Direction Inbound -Priority 102 -SourceAddressPrefix `
Internet -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 443

$NSG.SecurityRules.Add($RuleHttps)

#Lastly, update the Network security group
Set-AzNetworkSecurityGroup -NetworkSecurityGroup $NSG

Yes, I could have put this into a pretty function, and you should! If you check your VM’s network section, you should now see the ports. Now when you go to your VM and press “Connect” and download the RDP file, it will actually work right away…. hassle free!

Get a list of Azure Resources and their Sizes

You are probably here because you have painstakingly clicked on every single Azure resource in the portal to view it’s size. Using Azure PowerShell, your pain is finally over! To pull a list of all the VM’s and their respective sizes:

$ResourceGroup = "YourResourceGroupName"
$VmSizes = @{}
$Vms = Get-AzResource -ResourceGroupName $ResourceGroup -ResourceType "Microsoft.Compute/virtualMachines"
ForEach($Vm in $Vms) {
    $CurVm = Get-AzVm -Name $Vm.Name -ResourceGroupName $Vm.ResourceGroupName 
    $VmName = $CurVm.Name
    $VmSize = $CurVm.HardwareProfile.VmSize
        $VmSizes.Add($VmName, $VmSize)
}

$VmSizes

Similarly, you can pull App Services via the app service plan like so:

$ResourceGroup = "YourResourceGroupName"
$AppServicePlans = Get-AzAppServicePlan -ResourceGroupName $ResourceGroup

foreach ($AppServicePlan in $AppServicePlans ) {
    $webApps = Get-AzWebApp -AppServicePlan $AppServicePlan
    $webApps | ft @{Label="App Service Plan"; Expression={ $AppServicePlan.Name}}, 
        @{Label="Service Plan Size"; Expression={ $AppServicePlan.Sku | select-object -ExpandProperty Size }}, 
        @{Label="App Service Name"; Expression={ $_.RepositorySiteName}}
}

Enjoy!

A Week in Peru

Scratch one off the bucket list! I’ve always been entranced by those majestic photos of Machu Picchu, so I decided to plan a trip and take my own. Read on to learn about the adventure as well as some tips from a first-hand experience.

Travel Clinic

Tetanus and Hepatitis B aren’t usually the first things you think of when planning a trip to Machu Picchu, but after weeks of a nagging mother, I caved in and visited our local travel clinic. The nurse confirmed what my internet research had uncovered, informing me avoid drinking the tap water. She provided medication for both altitude sickness and an antibiotic in case of stomach problems. Diamox, the altitude sickness meds, are an absolute must. Without them, the trip would have been far less enjoyable!

Day 1 – Boston to Lima

My tallness loves the space JetBlue provides as well as the on-board entertainment and included WiFi. Much to my surprise, JetBlue has a flight from Boston to Lima with a stop in Ft. Lauderdale. Unfortunately, the WiFi only lasts while you are in US airspace and in-flight movie on the second leg of the trip was only in available Spanish. I tried to enjoy it, but I can’t take Aquaman (Jason Mamoa) seriously when he sounds like Antonio Banderas. The flight left Boston at 1 pm EST and arrived in Lima at 11pm local time.

In Lima, we stayed at the Wyndham across the street from the airport. They are connected by a sky bridge, which is allows you to avoid the sea of taxi drivers. After a long day of travel, it was nice to quickly get somewhere to rest up. While this was a convenient hotel, it was both the most expensive and the least comfortable of the trip. The atmosphere was odd with mall-like jewelry kiosk next to the check-in, but a decent restaurant. I would not say it bad, it just wasn’t great.

Day 2 – Cusco

Boarding on both ends of the plane – how efficient!

The flight from Lima to Cusco is just over an hour long, with almost hourly departures on regional jets. In that short amount of time, you’re going from sea level to about 11,000 feet! The small airport is surrounded by mountains, so the flights only arrive and depart with clear weather conditions. In the baggage claim area, you are surrounded by tourist companies offering various tour packages. We went ahead and chose an afternoon city tour visiting five archaeological locations. A shuttle would pick us up from our hotel, so we quickly grabbed a taxi to drop off our bags.

Iglesia De Santo Domingo

We learn about European colonization in grade school, but I forgot how brutal some of it was. The Spanish destroyed Incan temples and used the rubble to build churches where they once stood. Iglesia De Santo Domingo is one such place. The Spanish decided it wasn’t feasible to waste all the explosives on completely level it, so you end up with this mix of the Incan’s perfectly carved stones and Spanish mortar work.

Incan wall holding up better than the “superior Spanish engineering”

Sacsayhuaman

This location was an Incan fortress. The scale at which these stones were carved and placed is completely mind blowing. If you notice in the photo, the people in the foreground are shorter than the stones themselves.

This is also the location we realized that altitude sickness is real! The approximately thirty steps up to the top where the photo was taken was incredibly difficult. We didn’t make it to the top without taking a break and my girlfriend noted that my lips had gone purple!

Much of the Peruvian archaeological areas have been rebuilt, and not necessarily with he same skill that was required originally. Even so, it is hard not to admire the craftsmanship and how tightly locked they remain after hundreds of years.

A breathtaking view of Cusco from Sacsayhuaman

Q’enqo, Tambomachay, and Puka Pukara

Puka Pukara aka the Red Fortress

The tour had an additional three archaeological stops including an underground sacrificial chamber, a smaller fort, and what likely was an Incan spa. The water at Tambomachay still runs to this day!

The lodging in Cusco was located at Tambo del Arriero Hotel Boutique. It was very reasonably priced (92 USD) and was located close to the historic center. It had enjoyable atmosphere, comfortable room, and friendly staff.

Day 3 – Pisac and the Aranwa

Greg with his “awesome” hat

From Cusco, we traveled to Pisac, which was about a 45 minute taxi ride. Pisac is known for its market and boy did we load up on things to take home, such as that awesome hat. There are some ruins on the mountain to see, but we skipped this in the interest of time.

Problems

I thought I was being smart when I only brought my debit card and two credit cards. Having just done some serious shopping, we barely had enough cash to get us to our next destination, so I attempted to withdraw from the ATM. Declined! So I tried to do a cash withdrawal using my credit cards, which also didn’t work. I called my bank, USAA, who basically confirmed that they weren’t getting any requests for withdrawals. When asking what to do, I felt like I could hear her shrug. Seemed odd considering all of their members are military and are often deployed overseas. Fortunately, my girlfriend’s debit card from a regional New Hampshire bank worked. For the rest of our trip, I was unable to use my card at any ATM we came across, although I could use it at restaurants and such.

Another funny mishap here in Pisac was trying out the bus. We literally hopped on the bus clearly labelled “Cusco-Urubamba.” I ended up having to stand on the bus that was obviously designed for seated passengers only as I was hunched over. I was thinking “this is going to be an uncomfortable 45 minute ride.” When we crossed a bridge, I decided it was a good idea to confirm we were actually going to Urubamba in my broken Spanish and all of the passengers yelled at the driver to stop for us. I laugh thinking about our reaction if we had stepped off the bus in Cusco. We decided it was worth just getting a taxi after that. The 45 minute taxi ride was 60 Soles, which is about 20 USD where as the bus was going to be 10 Soles for the both of us.

Arawna Sacred Valley Hotel

After arriving in Urubamba and successfully getting some much needed dinero, I look up the address for our hotel. We drove passed it a couple miles back on the way here. Tired from walking all day, we spend little time looking for a proper taxi and hopped in mototaxi, which is basically a loosely enclosed 3-wheeled motorcycle. I think we hit top speed on the main road of 20mph. The windows were so low for me I could only see the road from my window.

After the longest 15 minutes, the driver pulls off the pavement onto a very unkept dirt road. We knew we were going to be mugged and left in the field. I pull up our location on Google maps to make sure we were on track. Ahead of us, we see a sign spray painted on a piece of plywood that says something like “Aranwa Hotel 700 meters ahead.” There’s some peace-of-mind that we’re going to the right place, but also some fear that our hotel is going to be a ditch covered by corrugated metal. My girlfriend says, “Greg, I’m not sure we’re going to stay wherever he’s taking us.”

I feel the mototaxi slow and with a quick turn and stop, he announces we have arrived. I unfold out onto the new-found pavement. We are standing in front of doors reminiscent of the Jurassic Park gate, but much fancier. We both literally sigh with relief and at the same time are taken aback by the unexpected grandeur. A man in a suit directs us to the front doors.

We walk in, and we see the most beautiful stained glass window in the lobby. We learn that the hotel has a spa, museum, several shops, a movie theater, and a fantastic restaurant. There were freaking llamas in the courtyard! Girlfriend, “Greg, you did good.” Greg, “Thanks,” not realizing I had booked something this fancy.

The Aranwa Sacred Valley was by far the best hotel we stayed at while in Peru and oddly, not the most expensive. We enjoyed this place so much, it gets its own section in this post!

A picture with our friendly waitress, Madison.

Day 4 – Train to Aguas Calientes

There are only two ways to get to Machu Picchu, a long bus ride, or the train. Even with my extensive planning, I managed to forget to purchase the train tickets. Depending on your departure station, there are basically three options: cheap, expensive, and baller. The cheap ones were of course sold out, so I went for expensive. My forgetfulness ended up being an unforgettable experience!

We boarded the Perurail Sacred Valley train. The three cars that we had access to can hold 35 passengers and includes the dining car, the bar car, and the observation car. We were two of the eight passengers who boarded!

A four course meal, snacks, wine, need I say more? The friendly train staff were extremely attentive and knowledgeable about the area, bringing attention to many points-of-interest as we passed. Most notably, the one and only Smick (pictured below), was so friendly and very patient with our plethora of questions. The ride takes approximately three hours.

Casa Del Sol Machupicchu was my second favorite hotel. There wasn’t anything to dislike about this place. The greeter first takes your bags, hands you a refreshing welcome drink from the bar, and then takes us to our air conditioned room. While you take in the view, he ran over to to the bus station to get our tickets for the following morning.

The small town of Aguas Calientes where many start their Machu Picchu day. It is packed with hotels, restaurants, food, and entertainment.

Day 5 – Machu Picchu

The buses from Aguas Calientes to Machu Picchu run constantly from open to close of the archaeological site. They curb rushes by having entry and exit times on your ticket. I did see the folks at the entry turn people away for not arriving at their given time, however, it is near impossible to enforce making you leave on time.

We got onto the bus at 6 am in the dark and zigzagged up the mountain. Every turn was like the almighty was turning up the lights just a little bit more. It was breathtakingly beautiful. I was nervous that the bus might clip one of the many people who chose to walk up the mountain.

When purchasing tickets for Machu Picchu, there are three options: Machu Picchu ruins only, Machu Picchu ruins and Machu Picchu mountain, and Machu Picchu ruins and Huayna Picchu mountain. According to the internet Machu Picchu hike is supposed to take longer but is significantly easier than Huayna Picchu. Having first consulted out-dated information, I didn’t realize you needed tickets at all. We purchased the tickets that included Huayna Picchu hike even though it was rated as moderate to challenging as the Machu Picchu mountain was sold out. There are people who do the multiple day treks to Machu Picchu and then do these peaks. All I can say is, good for them! Funny enough, you can smell some of the folks who did this.

Line waiting for entry to Machu Picchu

At the entrance, there are guides speaking many languages. You can join a group or request a private tour. We however had the early mountain hike at 7 am, so we made our way over to Huayna Picchu which is on the opposite side of the ruins.

Once we made it through the entry, we followed the clearly marked path which lead small entry hut. The gate opened promptly at 7 am and we signed in as visitors 5 and 6. He informed us that we had 45 minutes to ascend and 45 minutes to return.

It starts off with some manageable ups and downs and then you reach Huyana Picchu… As we began go ascend, my girlfriend asked me multiple times “why we were doing this,” followed by, “I hate you right now.” It was painfully difficult for two individuals that have desk jobs. We stopped frequently watching our more in-shape fellow travelers pass us, some of whom were wrinkled and grey.

Some of my “favorite” stairs

There are few pictures of the actual hike up, as we were too focused on “enjoying the experience” of getting our asses kicked. It winds back and forth with several wobbly stairs and missing cable hand rails. The trail was a mix of steep rocky paths and broken stairs. I’m glad we were one of the first people to head up, as it is the same way down. It would be worse needing to step aside near some of the questionable drops.

Conquering smiles with the ant-sized ruins in the background

Once you make it to the first ruin at the top, your reward it waiting for you just around the corner… more stairs! Also, you are surrounded by picture perfect… well perfection.

At the summit, there is a line to take a picture sitting on a large jetted rock looking down at the ruins. Ours didn’t turn out well as our chosen photographer was too short to get the ruins and us.

A young woman in front of us asked us what happens if we don’t make it back down in time. I looked at my phone and laughed. Our 45 minutes window was missed by a whole hour. My response was such a dad joke. “Well, you have to keep doing it until you can do it in 45 minutes. Fortunately, there’s a quick way down.”

We did make it down quickly, only taking the 45 minutes. At the bottom, we heard one of the tour guides said he’d made it up in 25 minutes. Crazy!

Back at the main archaeological site, the path through the ruins is a large loop. Since we walked to the opposite side for the hike, we were half way through without any context of what we were looking at. There are “park” workers stationed throughout, so we asked one of them about a tour guide. He was able to radio to the front and have someone come meet us. This was great, as there is no re-entry and being with a guide gives you access have a “do over.”

My own magazine worthy photo of the ruins. Look closely for the tourists to understand the scale.

The guide is a must to provide context for all the things you are viewing. Our personal guide was only 120 Soles, which is about 40 dollars for the 3 hours he was with us.

Our guide was also a creative photographer

Machu Picchu is such a popular tourist location that they have implemented a lot of things to stop erosion due to visitors. They have this hard plastic grates on all the walking trails within the ruins, and the three main areas are rotated for time. We were able to see the temple of the condor, but unable to see the inside of the sun temple.

After an extremely tiring day, we collected our bags at our hotel, had a beer, and waited for our evening train to Cusco.

The evening train was the Vistadome, the least expensive option(woo hoo!). The experience was excruciating as we were tired and another train with mechanical problems caused us to be delayed an additional three hours. This train serves a snack, has assigned seating, and you cannot roam around. It is more like a bus. This ride has a entertaining fashion show where the staff model the latest alpaca trends.

Day 6 – Cusco

Museo y Catacumbas Del Convento San Francisco de Asis Del Cusco

The Museum of Saint Francis was filled with paintings, a library with really old books, and a small viewing area of the catacombs. Unfortunately, there wasn’t anything here in English and you were only allowed to take photos of the courtyard. Most everything was in Spanish or the original Latin. There were some students who volunteered there that provided some context and did mention they were working on translations for the future.

Chocolate Museum

I honestly was not excited about the chocolate museum, but it ended up being deliciously impressive! First off, it is free. That’s right, free! They provide a 10-15 minute sampling where they explain the different chocolates. Unless you hate chocolate, you’re not leaving here without a full bag wondering how you could possibly fit this in your suitcase.

There is a small cafe where you can order a quick snack and, if you plan ahead, they offer chocolate making classes.

Limo and Precolumbian Art Museum

I somehow made it out of this restaurant without a picture of the food. This meal ties for first place with our meal at the Aranwa hotel. Limo is located on the gorgeous Plaza de Armas. We ate early, so we were able to sit on the enclosed balcony and look out at the plaza. The waiter said that after 7, when they do reservations, there isn’t a free spot.

We did souvenir shopping and headed to the Precolumbian Art Museum, which is open later than the others we had considered visiting. If you like decorative pottery, carvings, and gold jewelry, this has a large collection. For a small up charge, you can get the handset to use while you walk through he museum.

This time in Cusco was the only time we stayed in a hotel for more than one night. The Esplendor Hotel (now called the Union Boutique Hotel), was clean and comfortable. The staff was helpful, even helping us order pizza due to the language barrier. This was the only hotel that didn’t offer complimentary bottled water, as the tap water is not safe to drink.

Day 7 – Back to Lima

The morning was a flight to Lima. Upon arriving, we took the Airport Express bus, which was only 8 dollars a ticket and makes frequent trips from the airport to the Miraflores district. The trip took about an hour. I didn’t realize how large Lima was and was even more surprised by how modern and westernized everything seemed to become as we got closer to our hotel. Feeling completely ignorant, I did a quick search to realize the population is over 10 million… which is slightly more than New York City.

View from above the Larcomar mall

From our hotel, we walked half a block to a mall built into the cliff, Larcomar. It had many restaurants stores and restaurants, but surprisingly included many familiar places, such as Chilis, Friday’s, Timberland, GAP, and Banana Republic. After grabbing dinner, we headed back to research the next day’s activities and to relax.

Our last night in Peru, we stayed at the Best Western Plus Urban Larco Hotel. It was squeaky clean with a very sleek and simplistic design. There are definitely much nicer hotels to stay at, but this was an extremely convenient location at a reasonable price.

Day 8 – Lima

Pre-Incan temple against the modern-day hospital in Miraflores

Our last day we spent walking. First, we walked to an Incan market to purchase some last minute gifts for friends and family, not having realized we weren’t really in tourist land anymore. The we continued for another 30 minutes through he bustling city to the Huaca Pucllana ruins. The only way to view the ruins are to join a 45 minute guided tour. The pr-Incan clay buildings were still being excavated and they had recently found the ancient remains of a sacrificed mother and child the week before. They do all this with funds donations and the ticket sales as they do not receive and aid from he government. The guide also mentioned that the ruins extended much further, but had been demolished to make way for the ever-growing city.

We made our way back to the water and took a stroll through Lover’s Park. Even though Lima is usually cloud-covered, the colorful landscaping and mosaic decorative walls did not disappoint. We sat for awhile and enjoyed watching the parasailers strategically fly between the ocean, the cliff, and the tall city landscape.

The most “thrilling” portion of our journey was the trip from our hotel to the airport. As we flew down the highway across multiple lanes, even lanes that were meant for the other direction. Our driver said that in Lima there is only one rule – don’t hit anyone. I have never in my life seen a more skilled driver, nor have I ever prayed so hard. Seriously, a two lane road turned into five lanes going a single direction.

I feel so fortunate to have been able to make this trip. I can’t wait for our next adventure!

Planning the Trip

The trip was planned around a single event – visiting Machu Picchu. There are a few guardrails for you trip you should think about:

  • Peru has a wet and a dry season, so you’ll likely want to visit when it is dry between May and October.
  • Spend at least two days at high altitude before doing any strenuous activity to allow your body to acclimate, especially if you are visiting from sea-level.
  • Phone Service – I have AT&T and it was relatively inexpensive to get a few gigs of data and cell service, however, all the hotels we stayed at had WiFi.
  • Tickets
    • A ticket is required for entry into Machu Picchu. I found it easiest to purchase Machu Picchu tickets directly from Peru’s Ministry of Culture site to avoid markup. They are limited!
    • An additional fee to the Machu Picchu ticket is required if you are to hike either of the two peaks. I recommend picking the early times to avoid the heat.
    • Sunday is free entry for locals.
    • If you are planning on taking the bus up, get your tickets ahead of time in Aguas Calientes.
    • Get your train tickets ahead of time from PeruRail.

I bought three different books at the bookstore to plan this trip. While it does not have as many pictures as the others, Frommer’s EasyGuide to Lima, Cusco, and Machu Picchu was my favorite. It gave sample itineraries and was easy to scan for information. Like with any printed text however, I would recommend validating important information such as operating hours online and such.

I’m all about release plans, sprint boards, and estimation… so my planning process definitely leverages skills I use at work! Sticky notes are the best because plans, like life, change.

An early view or our Peru planning wall


Quick Self-Signed Certs Sitecore 9.x

When you download and install Sitecore XP1, you need to install all of the certificates first. If you need to do this often like I do, this can be quite tedious.  Stealing some code from the Single-Developer.ps1, this simple foreach wrapper is a huge time saver.

This assumes you already have the Sitecore Installation Framework installed(SIF) and can execute PowerShell scripts. Please use real certificates in production.

Enjoy!

###############################
# CreateAllSelfSignedCerts

# Prefix for sites
$Prefix = "sitecore"
# Folder with the install files in it (specifically the createcert.json)
$SCInstallRoot = "C:\Sitecore\9.1.1\XP1\"

$ContentDeliverySiteName = "$prefix.cd"
$ContentManagementSiteName = "$prefix.cm"
$ReportingSiteName = "$prefix.rep"
$ProcessingSiteName = "$prefix.prc"
$ReferenceDateSiteName = "$prefix.refdata"
$IdentityServerSiteName = "$prefix.identityserver"
$XP1MarketingAutomationSiteName = "$prefix.ma"
$XP1MarketingAutomationReportingSiteName = "$prefix.mareporting"
$XP1ClientCertificateName = "$prefix.xconnect_client"
$XP1CollectionSitename = "$prefix.collection"
$XP1CollectionSearchSitename = "$prefix.search"
$XP1CortexProcessingSitename = "$prefix.processingEngine"
$XP1CortexReportingSitename = "$prefix.reporting"

$siteNames =
$ContentDeliverySiteName,
$ContentManagementSiteName,
$ReportingSiteName,
$ProcessingSiteName,
$ReferenceDateSiteName,
$IdentityServerSiteName,
$XP1MarketingAutomationSiteName,
$XP1MarketingAutomationReportingSiteName,
$XP1ClientCertificateName,
$XP1CollectionSitename,
$XP1CollectionSearchSitename,
$XP1CortexProcessingSitename,
$XP1CortexReportingSitename

function InstallCertificates {
Foreach ($site in $siteNames) {
$certParams = @{
Path = "$SCInstallRoot\createcert.json"
CertificateName = $site
}
Install-SitecoreConfiguration @certParams -Verbose *&>1 | Tee-Object ".\CertInstall.${site}.log"
}
}
InstallCertificates

Querying Sitecore

Often times we run into situations where we need to pull information from Sitecore.  Two of my favorite tools to do this are Sitecore Rocks and Sitecore PowerShell Extensions (SPE).

For those of you who are unfamiliar with these tools, Sitecore Rocks is a Visual Studio extension that makes developing and interacting with Sitecore easy.  SPE is an installed module into Sitecore that gives you a scripting environment and a command line interface.  We will simply be executing queries, but I highly recommend reading their respective documentation sites to learn about their full set of features.

I don’t claim to be a PowerShell guru, so what I’ve done is create simple queries you can run in both Sitecore Rocks and SPE.  You can write pure PowerShell queries, but I find the ones I’ve created to feel overly complex.  So for any query below, you can execute this in SPE with the following:

Get-Item -Path master: -Query "/sitecore/templates/Feature//*[@@templatename = 'Template']"

Or like this so you can easily define the columns into a table:

$items = Get-Item -Path master: -Query "/sitecore/templates/Feature//*[@@templatename = 'Template']"

$items | Format-Table Name, @{ Label = 'Path'; Expression={ $_.Paths.Path } },  @{ Label = 'Std Val'; Expression={ $_["__Standard values"] } }

You can get more information about Working with Items and getting item by XPath on the SPE doc site.  If you have built indexes and aren’t working off of restored databases, a performant way to do this would be finding items using the content search API.

The examples provided below are intended to be run in the Sitecore Rocks Query Analyzer.  I highly recommend reading 28 Days of Sitecore Rocks: Query Analyzer to get started.

Standard Values of Templates
Are there Standard Values on all the templates?

select @@Name as Name, @@Path as Path, @#__Standard values# as #Standard Values# from /sitecore/templates/Feature//*[@@templatename = 'Template']

Types of Fields
Are there fields of type Multilist and what is their source?

select @@Name as Name, @Type, @Source, @@Path as Path from /sitecore/templates/Feature//*[@Type = 'Multilist']

Are there fields of type Treelist and what is their source?

select @@Name as Name, @Type, @Source, @@Path as Path from /sitecore/templates/Feature//*[@Type = 'Treelist']

What are all the fields, their types, and their sources regardless of their template? Order them by Type and Name.

select @@Name as Name, @Type, @@Path as Path from /sitecore/templates/Project//*[@@templatename = 'Template field'] order by Type, Name

List of Templates
Just give me a list of all the templates.

select @@Name as Name, @@Path as Path from /sitecore/templates/Project//*[@@templatename = 'Template']

List of Content Items
Where are all my content items based on the Article template in the Draft workflow state?  The GUID in this example is of the Draft item in a the Sample Workflow.

select @@Name as Name, @@Path as Path from /sitecore/content//*[@@templatename = 'Article' and contains(@#__Workflow state#, "{190B1C84-F1BE-47ED-AA41-F42193D9C8FC}")] order by Path

Rich Text Editor fields with HTML editor profiles defined
What are all the rich-text fields and their sources? (originally from Sitecore John himself)

select @@path, @Source from /sitecore/templates//*[@@templatekey = 'template field' and @type = 'Rich Text'] order by Source

Search for Tokens in Fields
Where are all the single-line text fields with a $name token?

select @@Name as Name, @Type, @@Path as Path from /sitecore/templates//*[@type = 'Single-Line Text' and contains(@title, '$name')]

Explorer Sitecore Rocks Tools
Right next to the Execute Command in the ribbon, there are some really useful tools!Sitecore Rocks Query Analyzer Tools

Insert Fields gives you a list of every field available out of Sitecore.  As it is connected to your Sitecore instance, this means it will pull the fields off of the items specific to your environment!

Sitecore Rocks Query Analyzer Insert Fields

Exporting Data with Sitecore Rocks

One of my favorite features is the ability easily export the results.  Simply right click on the Results tab.  This is invaluable for providing an ad hoc report of items.  For example, we could export the results of the “Article” query we performed.

Sitecore Rocks Query Analyzer Export

For repeatable queries/reports you want Sitecore users to access, consider a different approach with Sitecore PowerShell Extensions Dynamic Reports.

The Big Gotcha

When using a Sitecore Query, it’s important to be cognizant of the configured Query.MaxItems value in the Sitecore configs.  Depending on your version of Sitecore, it may be set to 100 or 260 by default.  I typically set this to 9999 on my local, but I wouldn’t do this on a production environment.

Disclaimer

These queries are intended for developers to gather information.  They are not optimized for performance and should not be used in a production environment.

Sitecore Workflow – Multiple Users Approving an Item

Today we are going to create a workflow that allows multiple users to approve an item.

What we won’t cover in this post is how to setup security on fields and workflow commands.  For that, I recommend John’s post on Security Access Rights.

Based on the business requirements, there are a few things I need to do:

  • Checks to make sure that at least one user is required for the approval
  • Make it easy for editors to determine who needs to approve the content

Workflow

I’ve gone ahead and built out my workflow with the needed commands and actions and selected some amazing icons.

MultiApprove Workflow
MultiApprove Workflow

User List Field Type

First, we need to build a custom Field Type that allows us to select the users we want to require.  I’ve called mine User List.  John blogged about a users droplist to accomplish this, so we’re going to build it as a multilist using another blog showing how to create a custom multilist field.  I’ve created a txt of Lookup.cs as it’s a bit long.

Now, actually setup the Field Type in Sitecore.  In the core database, I’ve created mine here:
/sitecore/system/Field types/List Types/User List using the template Template Field Type

Set the Assembly and Class appropriately.  Mine were:
Assembly: sandbox.BusinessLogic
Class: sandbox.BusinessLogic.Common.UserLookup

Workflow Template

With the new user list created, we can create our template like so.  I’ve also removed editing permissions to Everyone on the Approved By and the Awaiting Approval From fields as we don’t want editors to be able to change these.

MultiApproveTemplate
MultiApprove Template

Once you’ve created this, make sure you inherit it to either your site’s Standard Template  or Page Template.  In my case, my News Page inherits it.

MultiApprove Template Inheritance
MultiApprove Template Inheritance

 

Users Assigned Check Action

In my case, we want to prevent items from progressing in the workflow if they have not selected any users to approve it.  So, we want to create an Action under the Draft’s Submit command.  I’ve named mine UsersAssignedCheck.

User Assigned Check Action
User Assigned Check Action

In the Type String field, fill in the classNamespace.Classname, AssemblyName. For this example, we’ll be using:
sandbox.BusinessLogic.Workflows.MultiApprove.UsersAssignedCheck, sandbox.BusinessLogic

UserAssignedCheck Code:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;
using System;
using sandbox.BusinessLogic.Helpers;
using Sitecore.Workflows.Simple;

namespace sandbox.BusinessLogic.Workflows.Multiapprove
{
    public class UsersAssignedCheck
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            ProcessorItem processorItem = args.ProcessorItem;
            if (processorItem == null)
                return;
            var item = args.DataItem;


            // if no users selected, throw warning
            if (String.IsNullOrEmpty(item.Fields[MultiApproveConstants.Sitecore_UsersFieldName].Value)))
            {
                SheerResponse.Alert("Assign at least 1 Sitecore User to the MultiApprove Sitecore Users.", false, "Error");

                // abort to prevent Next State
                args.AbortPipeline();
            }
        }
    }
}

Screenshot:

Assign One User Error
Assign One User Error

Approve Action

The approve action will:

  • Prevent the next state if all users have not approved
  • Update the Approved By field
  • Update the Awaiting Approval From field
  • Clear Approved By and Awaiting Approval From  fields if all users have approved

The String Type for this action is:
sandbox.BusinessLogic.Workflows.MultiApprove.Approve, sandbox.BusinessLogic

Approve Action Code:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using System;
using System.IO;
using sandbox.BusinessLogic.Helpers;
using Sitecore.Workflows.Simple;
using Sitecore.Security.Authentication;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using sandbox.BusinessLogic.Models.sitecore.templates.Common.Workflows;
using Sitecore.Web.UI.Sheer;

namespace sandbox.BusinessLogic.Workflows.Multiapprove
{
    public class Approve
    {
        public void Process(WorkflowPipelineArgs args)
        {
            Assert.ArgumentNotNull((object)args, "args");
            ProcessorItem processorItem = args.ProcessorItem;
            if (processorItem == null)
                return;

            // this is the context item
            var item = args.DataItem;

            // the active user
            string activeUserName = AuthenticationManager.GetActiveUser().Name.ToLower();

            // required Approver List
            List<string> requiredApprovers = new List<string>(item.Fields[IMultiApproveConstants.Sitecore_UsersFieldName].Value.ToLower().Split('|'));

            // update fields if this user is an approver
            if (requiredApprovers.Any(str => str.Contains(activeUserName)))
            {
                AddToApprovedBy(item, activeUserName);
                UpdateAwaitingApprovalFrom(item, requiredApprovers);
            }

            // if we're still waiting for approval, display a message telling the user they're not the last approver
            if (!String.IsNullOrEmpty(item.Fields[IMultiApproveConstants.Awaiting_Approval_FromFieldName].Value))
            {
                // not all users approved
                SheerResponse.Alert(
                    "You have approved this item, however, the following users still need to approve this content:\n"  
                    item.Fields[IMultiApproveConstants.Awaiting_Approval_FromFieldName].Value, false, "Approval");
                
                // abort the pipeline, because we don't want to reach the "Next State" execution
                args.AbortPipeline();
            }
            else
            {
                // clear Approved By and Awaiting Approval From fields
                // as we should clear it before the item is edited again
                ClearApprovals(item);
            }

        }

        private void AddToApprovedBy(Item item, string user)
        {
            if (ContainsUser(item.Fields[IMultiApproveConstants.Approved_ByFieldName].Value, user)) return;
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Approved_ByFieldName] = AddUserToMultiline(item[IMultiApproveConstants.Approved_ByFieldName], user);
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
        }

        private void UpdateAwaitingApprovalFrom(Item item, List<string> requiredApprovers)
        {
            StringBuilder awaitingApprovalUsers = new StringBuilder();
            List<string> approvedBy = new List<string>(item.Fields[ IMultiApproveConstants.Approved_ByFieldName].Value.Split('\n'));

            foreach (string approver in requiredApprovers)
            {
                if (!approvedBy.Any(str => str.Contains(approver)))
                {
                    awaitingApprovalUsers.AppendLine(approver);
                }
            }
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Awaiting_Approval_FromFieldName] = awaitingApprovalUsers.ToString();
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }
        }

        private string AddUserToMultiline(string multiline, string user)
        {
            StringBuilder sb = new StringBuilder(multiline);
            sb.AppendLine(user);
            return sb.ToString();
        }

        private Boolean ContainsUser(string multiline, string user)
        {
            using (StringReader reader = new StringReader(multiline))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    if (line == user)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        private void ClearApprovals(Item item)
        {
            item.Editing.BeginEdit();
            try
            {
                item[IMultiApproveConstants.Approved_ByFieldName] = "";
                item[IMultiApproveConstants.Awaiting_Approval_FromFieldName] = "";
                item.Editing.EndEdit();
            }
            catch (Exception)
            {
                item.Editing.CancelEdit();
            }

        }
    }

}

Screenshots:

This is the notification you will receive if you’re not the last required approver.

Approval Notification
Approval Notification

The item is updated, so all users can easily see what approvals are still required.  You can see that I have approved it.  Note: I am an admin during this test.  Normal content editors would not have the option to edit these fields at this point.

Test News Page
Test News Page

We’re done! Or are we?

This is a working proof of concept.  Ideally you won’t have hard-coded strings or display unnecessary users.  Maybe we want to hide the commands to those users who have already approved, that’s for another post!  Enjoy!

Built on Sitecore 8.0 Update 4

Sitecore Workflow – Suppressing Comments

When creating your own workflows, you may not always want to require your users to comment.  Sitecore provides an option to Suppress Comment on the actions you create, however checking this box didn’t seem to have any effect .

So out of curiosity, I opened up the Sitecore Kernel with trusty DotPeek.

Looking for the field “Suppress Comment”, I found that it was being checked in Sitecore.Workflows.BasicWorkflowCommandAppearanceEvaluator

Solution

Suppress Comment

Applying this Appearance Evaluator Type to the Command allows this check to take place.

  1. Make sure you’ve selected the Suppress Comment checkbox
  2. On you Command, locate the Field Appearance Evaluator Type
  3. Insert: Sitecore.Workflows.BasicWorkflowCommandAppearanceEvaluator, Sitecore.Kernel

Using the LinkedIn JavaScript API

I was recently charged with a project where I needed to get user information from LinkedIn and I decided to use their JavaScript API.  Unfortunately, most of their examples were not very useful for what I was trying to do.

What Am I doing?

The goal was to pre-fill a long registration form and then display willing registrants on an attendees page.  This was convenient since I could take the user’s LinkedIn photo URL.  In this post, I will only go over how to get the values from LinkedIn.

Create the Application

The first step is to create an application.  Head over to https://www.linkedin.com/secure/developer. (LinkedIn account required!)  I’m not going to walk you through the form process, but I’d like to point out a few important spots.

  • The Application Name will be displayed to the user, so name it accordingly
  • Make sure to check the appropriate scopes according to which data you will need. For this example, I have selected r_fullprofile, r_emailaddress, and r_contactinfo. They provide a well-documented section for the profile fields, which should help you define your scope.
  • JavaScript API Domains – make sure you include your development domain, e.g. localhost.

Once you hit save, you’ll be presented with an API Key.  Save that, you’ll need it later.  If you forget, it will be listed on the application page.

The Fun Stuff

Initialize the LinkedIn JavaScript API, and paste in your API key.  Since we defined the scope at the application level, you do not need to define it here, but you can if you want.

<script type="text/javascript" src="http://platform.linkedin.com/in.js">
api_key: yourApiKey
onLoad: onLinkedInLoad
authorize: true
</script>

Now within another script tag, add the authentication event:

<script type="text/javascript">
function onLinkedInLoad() {
IN.Event.on(IN, "auth", onLinkedInAuth);
}

Now we make the API call, listing the fields we’d like to pull back.  Again, refer to the profile fields to see all the available fields and which fields are collections.

function onLinkedInAuth() {
IN.API.Profile("me")
.fields(["firstName", "lastName", "positions:(title,company)", "pictureUrl", "publicProfileUrl", "emailAddress"])
.result(function (result) {
displayProfileData(result);
});
}

In this display function, I’m using jQuery to set the values I’ve pulled back.  You will notice I am setting the positions field to 0, as I only care about their current job. In my case, I do not care or need to iterate through their past positions and companies.

function displayProfileData(profile) {
var profile = profile.values[0]; $('#firstName').html(profile.firstName); $('#lastName').html(profile.lastName); $('#title').html(profile.positions.values[0].title); $('#company').html(profile.positions.values[0].company.name);
$('#email').html(profile.emailAddress); $('#pic').attr('href', profile.publicProfileUrl);
 $('#pic img').attr('src', profile.pictureUrl);
$('#pic img').attr('alt', profile.firstName + " " + profile.lastName);
}

Toss this in the body of your HTML.  This is the button you’ll use to initiate the LinkedIn popup authentication form.  I have seen people add their own CSS styles to it to customize it, but I personally will not do that.  On a LinkedIn forum post, one of the developers states that LinkedIn would like to have a consistent experience for their users across all sites.  My only other concern is if they decide to change their styles one day, you will have a broken button.

<script type="IN/Login"></script>

This is the HTML we are going to shove the data into.  In my own project, I put these values into the input fields, but for the purpose of this post, we are going to do it this way.

<div id="firstName"></div>
<div id="lastName"></div>
<div id="title"></div>
<div id="company"></div>
<div id="email"></div>
<a id="pic" href="#"><img src="" alt="" /></a>

It’s Alive!

And that’s it!  If you did everything correctly, you should see a login button.

button

When clicked, LinkedIn will notify the user to which data you’ve requested access.  So, make sure you only select the scope’s you will need.

login

Once you allow access, this should be the result: (obviously with your beautiful mug instead of mine).

2013-10-10_0005