

<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:wfw="http://wellformedweb.org/CommentAPI/"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
     xmlns:slash="http://purl.org/rss/1.0/modules/slash/">

    <channel>
        <title>Blog  -  Joe Glombek</title>
        <atom:link href="https://joe.gl/ombek/blog/rss" rel="self" type="application/rss+xml" />
        <link>https://joe.gl/ombek/blog/rss</link>
        <description>Technical blogging from Joe Glombek, Senior .NET Developer and Umbraco MVP.</description>
        <lastBuildDate>Wed, 04 Mar 2026 02:02:04 +0000</lastBuildDate>
        <language>en-GB</language>
        <sy:updatePeriod>daily</sy:updatePeriod>
        <sy:updateFrequency>1</sy:updateFrequency>
            <item>
                <title>The Umbracian&#39;s Guide to Bristol (2026)</title>
<link>https://joe.gl/ombek/blog/umbracian-bristol-guide/</link>                <pubDate>Tue, 03 Mar 2026 09:44:22 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/umbracian-bristol-guide/?rssid=#138c023e-b33a-4799-a08f-c3c02e3b9985</guid>
                <enclosure url="https://joe.gl/media/bbxpi3dl/img_3014-1.jpg" length="1321497" type="image/jpg" />
                <description>Are you heading to Umbraco Spark and hoping to explore Bristol while you&#39;re here? I live near Bristol and now consider myself a Spark veteran, so thought I&#39;d share my insights!</description>
                <content:encoded><![CDATA[
                    <p>Are you heading to Umbraco Spark and hoping to explore Bristol while you're here? I live near Bristol and now consider myself a Spark veteran, so thought I'd share my insights!</p>
<h2>Spark and related events</h2>

<p>Usually this is where I remind you to sign up to all the pre- and post-Spark events as well as what to expect on the day. But this year Spark has put together a fantastic <a href="https://umbracospark.com/2026-umbraco-spark/">practical guide to Spark!</a></p>

<h2>Map</h2>

<p>A map of the primary locations is included below. It also includes my recommendations for Bristol - read on for details!</p>

<iframe src="https://www.google.com/maps/d/u/0/embed?mid=1akl8-ORreqkPsAr3yVVf-sud9SM_G9U&ehbc=2E312F&noprof=1" style="width:100%" height="480"></iframe>

<p><a href="https://www.google.com/maps/d/u/0/edit?mid=1akl8-ORreqkPsAr3yVVf-sud9SM_G9U&amp;usp=sharing">Open map full screen.</a></p>

<h2>Recommendations</h2>

<p>Bristol is a lovely city - so lovely, in fact, I moved here in 2022 - so many people elect to stay in the area for the weekend afterwards. Here's some advice for those people:</p>

<h3>Getting around</h3>

<p>Although the city is quite sprawling, a lot of Bristol is accessible by foot (particularly if you're good with hills!)</p>

<p>As much as the locals complain, Bristol has a pretty good bus network (just don't compare it to London or any continental European city!) Fares on First Bus busses are £2.60 for a single with a cap at £6.80 for the day, if you stay within Bristol. Busses all support tap-on-tap-off which are automatically capped, so no cash is needed.</p>

<p>For <a href="https://ridedott.com">bike and electric scooter hire you'll need the Dott app</a> which you'll need your driving license (full or provisional) to sign up for, so be sure to do that ahead of time.</p>

<h3>Food and drink</h3>

<p>Bristol is home to fantastic independent cafes, pubs, restaurants and food stalls - as well as the birthplace of small chains like The Lounges and Boston Tea Party.</p>

<p>My favourite food stalls are in <a href="https://www.wappingwharf.co.uk/">Wapping Wharf (Spike Island, harbourside)</a> (St Nicholas Market is closed at the weekend!)</p>

<p>You can't go wrong with most cafes in Bristol, but some of my regulars are <a href="https://www.mud-dock.co.uk/cafe/">Mud Dock (harbourside)</a>, <a href="https://thebristolloaf.co.uk/beacon/">The Bristol Loaf ("The Centre")</a>, <a href="https://www.eastvillagecafe.co.uk/">East Village Cafe (Clifton Village)</a> and Santiago's (particularly if you're waiting for a bus at the bus station!)</p>

<p>Some of my favourite pubs include <a href="https://applecider.co.uk">The Apple (central)</a> (don't miss having a half - too strong to sell it in pints! - of Old Bristolian cider on this cider-barge), <a href="https://butcombe.com/the-cottage-inn-bristol/">The Cottage Inn (Spike Island, harbourside)</a> and pretty much anywhere on King Street (central).</p>

<h3>A touch of nature</h3>

<p>Leigh Woods is a fantastic woodland a short walk over Clifton Suspension Bridge. You'll want some boots but it's easy to follow signposted routes.</p>

<p>Ashton Court is a deer park with great views over Bristol and a lovely cafe. You can stick to tarmac and gravel paths here if you want clean shoes! Also a Parkrun location.</p>

<p>Brandon Hill is a lovely park right next to Park Street and is home of Cabot Tower - which is worth a climb (free) for some more fantastic views.</p>

<h3>Shopping</h3>

<p>Cabot Circus and Bristol Shopping Quarter are mostly (although not entirely!) big chain shops you'd find in most cities.</p>

<p>Clifton Village (further out than Clifton but within the city, unlike it's name implies!) is where you'll find the best of Bristol's local boutique-y gift shops and cafes. Also worth visiting the iconic suspension bridge while you're there (and the cafe at the observatory is worth a stop!)</p>

<p>A little closer to the centre of town is Park Street and Whiteladies Road as well as Gloucester Road, less classy than Clifton but some lovely smaller shops. (Gloucester Road is the charity-shopper's heaven!)</p>

<h3>Sightseeing</h3>

<p>Bristol is home to some great museums. M Shed is the historic venue for Spark but also a great little free museum. You can find <a href="https://www.bristolmuseums.org.uk/">more information about all the Bristol Museums on their website</a>.</p>

<p>This year's venue is also a museum - <a href="https://www.wethecurious.org">We The Curious</a> is an interactive science museum that's recently reopened after a large fire. I've not been since the reopening, but have fond memories of going as a child!</p>

<p>Next to We The Curious is the aquarium (central) which is expensive but very good. Bristol Zoo is no longer located in Clifton, and is now outside the city.</p>

<p>Cabot Tower in Brandon Hill park (central) is free to climb for some epic views.</p>

<p>Clifton Suspension Bridge (Clifton Village) is free to walk across and Observatory Hill is nearby for some good views and a nice coffee shop. Or head into Clifton Village for endless cafes and pubs.</p>

<p>Bristol University has several beautiful historic buildings. The closest to the city centre are at the top of Park Street along with the Bristol Art Gallery.</p>

<p>Personally, I like to explore new cities using virtual audio tours. <a href="https://voicemap.me/share/xzplzs">Claim a free one using my VoiceMap referral link.</a>  There are 5 highly-rated tours of Bristol to choose from.</p>

<p><img src="/media/ahclxydl/_5015bce3-afd6-46c0-94ad-0f1aa160e3cd.jpg" alt="A cartoon unicorn sat in a harbour, surrounded by boats. The Clifton Suspension Bridge is behind it and hot air balloons float in the sky above." /></p>

<h3>Nearby</h3>

<p>The cities of Bath and Cardiff, Wales are within an easy train ride (15 minutes or 1 hour respectively) if you want to spread your wings a little further.</p>

<h2>See you there!</h2>

<p>I'll be at the hackathon right the way through to the after party - so I'll see you there! Please come and say hi, and see my talk too if it appeals!</p>

<p>If you think I've missed something vital, <a href="/ombek/contact/">let me know on socials!</a></p>

<p><img src="/media/cvdp05yj/spark-unicorn.png" alt="A more realistic unicorn stood on a hill with Bristol and the suspension bridge behind it at sunset. AI generated." /></p>

<p><small>
<em>Thanks to <a href="https://www.linkedin.com/in/tristanjthompson/">Tristan Thompson</a> for his contributions to the 2024 post which have been brought forwards.</em>
</small></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Running GitHub Actions .NET and Azure workflows locally with Act</title>
<link>https://joe.gl/ombek/blog/act/</link>                <pubDate>Fri, 27 Feb 2026 11:29:14 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/act/?rssid=#0732f2d8-deff-4f61-a17f-3e813e7cecb8</guid>
                <enclosure url="https://joe.gl/media/5mch42t1/copilot_20260227_232531.png" length="2256197" type="image/png" />
                <description>Act is a fantastic tool for testing GitHub actions locally instead of pushing that 10th commit in a row called Testing GitHub Actions (again) but configuring it to work correctly can be a balancing act, so here are some tips for getting Act working with your .NET CI/CD flows.</description>
                <content:encoded><![CDATA[
                    <p>Act is a fantastic tool for testing GitHub actions locally instead of pushing that 10th commit in a row called <code>Testing GitHub Actions (again)</code> but configuring it to work correctly can be a balancing <em>act</em>, so here are some tips for getting Act working with your .NET CI/CD flows.</p>
<h2>Basic setup</h2>

<p>You'll need:</p>

<ul>
<li><a href="https://github.com/nektos/act">act</a></li>
<li>Docker (to run a Linux build) or run act from a Windows machine (to run a Windows build)</li>
<li><a href="https://cli.github.com/">GitHub CLI</a></li>
<li><a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli">Azure CLI</a></li>
<li>dotnet</li>
</ul>

<p>Ensure your GitHub and Azure CLIs are logged in, using <code>az login</code> and <code>gh auth login</code>.</p>

<h2>TL;DR</h2>

<p>This PowerShell command (running on a Windows machine) will run most .NET/Azure builds:</p>

<pre><code>act push -W '.github/workflows/my-workflow.yml' --var-file '.github/workflows/local/vars.env' --secret-file '.github/workflows/local/secrets.env' -s GITHUB_TOKEN="$(gh auth token)" --artifact-server-path 'C:\Windows\Temp\act-artifacts' -P windows-latest=-self-hosted -e '.github/workflows/local/payload.json'
</code></pre>

<p>I tend to save this PowerShell script in <code>.github/workflows/local/act.ps1</code> inside the repo.</p>

<p>You'll also need to create the files mentioned in here.</p>

<p><code>vars.env</code> and <code>secrets.env</code> are files in the <code>.env</code> format that must contain any variables or secrets your workflows need to run. Be sure to gitignore the secrets file!</p>

<p>The <code>payload.json</code> file should contain:</p>

<pre><code>{
  "head_commit": {
      "message": "Sample commit message"
  }
}
</code></pre>

<p>You will also need to disable any steps that use <code>actions/setup-dotnet</code> or <code>azure/login</code> locally. e.g.</p>

<pre><code>  - name: Setup .NET
    # Prevent this step from running if running locally with act
    if: ${{ !env.ACT }}
    uses: actions/setup-dotnet@v4
</code></pre>

<p>The rest of this article will break down why this is needed and how else you might need to modify act.</p>

<h2><code>GITHUB_TOKEN</code></h2>

<p>The <code>-s GITHUB_TOKEN="$(gh auth token)"</code> portion of our command is what provides the workflows with GitHub access. You can pass in a GitHub personal access token if you'd rather, or add the access token to your <code>secrets.env</code> file, but this requires less setup as the PowerShell script automatically inserts the token generated by the GitHub CLI.</p>

<h2>Artifact Server</h2>

<p>Providing the <code>--artifact-server-path</code> enables the act artifact server emulator. It's needed if you use the <code>actions/upload-artifact</code> and <code>actions/download-artifact</code> actions. This path can be viewed to help diagnose problems with artifacts.</p>

<h2>Self-hosted Runners</h2>

<p>Using the <code>-P windows-latest=-self-hosted</code> flag tells act to use the local machine as the build agent instead of a Docker container. This is not ideal, but saves complication! It's also the reason we can disable the <code>actions/setup-dotnet</code> or <code>azure/login</code> steps as we assume these are configured correctly locally. These actions are more complicated to configure.</p>

<p>The left hand side of the assignment should match the <code>runs-on:</code> declaration on your workflow and, of course, this should match your local machine. You probaly can't run a <code>windows-latest</code> build on a Mac.</p>

<h2>Payload</h2>

<p>The <code>-e '.github/workflows/local/payload.json'</code> parameter is a JSON file containing additional information to pass the workflow that act doesn't include by default. The <code>azure/webapps-deploy</code> action when using <code>azure/login</code> for authentication requires <code>head_commit.message</code>field to be set. Any null-reference errors in Actions (that, when the source code is viewed, use the GitHub context) can usually be fixed by addding values in this file.</p>

<h2>Disabling steps</h2>

<p>Act allows you to disable build steps using the <code>if: ${{ !env.ACT }}</code> syntax. This is helpful if you're able to use the self-hosted mode to remove configuration steps that are a pain to get working.</p>

<p>I'll keep this article updated as I work out better ways of working with act. Ideally a Docker container would be used for builds, but it does add an additional layer of complexity. This is the workflow I have working currently.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbraco Flavored Markdown: A ${template} for success</title>
<link>https://24days.in/umbraco-cms/2025/template-for-success/</link>                <pubDate>Sat, 13 Dec 2025 09:00:39 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://24days.in/umbraco-cms/2025/template-for-success/?rssid=#5f73e72a-af4f-4735-968c-aec24f2ea122</guid>
                <description>Editor experience isn’t just a nice-to-have — it’s what makes or breaks your build. With the arrival of the first LTS using the new backoffice, we&#39;re seeing fresh possibilities to shape more intuitive and delightful editing environments. But with great innovation (hello Bellissima!) comes the loss of old comforts (farewell AngularJS).</description>
                <content:encoded><![CDATA[
                    <p>Editor experience isn’t just a nice-to-have — it’s what makes or breaks your build. With the arrival of the first LTS using the new backoffice, we're seeing fresh possibilities to shape more intuitive and delightful editing environments. But with great innovation (hello Bellissima!) comes the loss of old comforts (farewell AngularJS).</p>
<p><a href="https://24days.in/umbraco-cms/2025/template-for-success/">Full article on 24 Days in Umbraco</a></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Bellissima Backoffice: Custom Entity Signs</title>
<link>https://joe.gl/ombek/blog/bellissima-backoffice-custom-entity-signs/</link>                <pubDate>Thu, 11 Dec 2025 11:28:54 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/bellissima-backoffice-custom-entity-signs/?rssid=#f98820dd-2a9e-4b29-9b8b-d32d17c1fa4a</guid>
                <enclosure url="https://joe.gl/media/bd3j4mx4/locked-signs.png" length="77269" type="image/png" />
                <description>You might recognise &quot;entity signs&quot; from the &quot;pending changes&quot; sign in older versions of Umbraco. But now, in Umbraco 17, we can add our own!</description>
                <content:encoded><![CDATA[
                    <p>You might recognise "entity signs" from the "pending changes" sign in older versions of Umbraco. But now, in Umbraco 17, we can add our own!</p>
<h2>Scenario</h2>

<p>You might have seen code like this, that blocks certain document types from being deleted.</p>

<pre><code>/// A common use case: prevent certain document types being deleted
public class LockedDocumentContentMovingToRecycleBinNotificationHandler : INotificationHandler&lt;ContentMovingToRecycleBinNotification&gt;
{
    public static string[] LOCKED_ALIASES = ["home", "error"];
    public static Guid[] LOCKED_IDS = [
      Guid.Parse("a95360e8-ff04-40b1-8f46-7aa4b5983096"),
      Guid.Parse("9db112c5-c2ea-441d-8bd4-6daf522aa2b6")
    ];
    public void Handle(ContentMovingToRecycleBinNotification notification)
    {
        foreach (var item in notification.MoveInfoCollection)
        {
            if (Array.Exists(LOCKED_ALIASES, alias =&gt; alias.Equals(item.Entity.ContentType.Alias, StringComparison.OrdinalIgnoreCase)))
            {
                notification.CancelOperation(new EventMessage(
                  $"{item.Entity.Name} cannot be trashed",
                  $"The content item '{item.Entity.Name}' is of type '{item.Entity.ContentType.Name}' which cannot be trashed.",
                  EventMessageType.Error));
            }
        }
    }
}
</code></pre>

<p>But wouldn't it be nice to show this in the backoffice before trying to delete an item?</p>

<h2>Flagging content items</h2>

<p>The custom signs can be configured to show for items that have a certain "flag". This is also a new feature of Umbraco 17. (The only use I'm aware of for flags at the moment is for custom entity signs, but perhaps this will change in the future!)</p>

<pre><code>using Umbraco.Cms.Core;
using Umbraco.Cms.Api.Management.Services.Flags;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document.Collection;
using Umbraco.Cms.Api.Management.ViewModels.Document.Item;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using My.UmbracoBackofficeExtensions.Notifications;

namespace My.UmbracoBackofficeExtensions
{
    // Created a C# class that implements `IFlagProvider`
    public class LockedDocumentFlagProvider : IFlagProvider
    {
        // We'll use this alias in the custom sign configuration
        private const string Alias = Constants.Conventions.Flags.Prefix + "My.Locked";

        // Indicate that this flag provider only provides flags for documents.
        public bool CanProvideFlags&lt;TItem&gt;()
            where TItem : IHasFlags =&gt;
            typeof(TItem) == typeof(DocumentTreeItemResponseModel) ||
            typeof(TItem) == typeof(DocumentCollectionResponseModel) ||
            typeof(TItem) == typeof(DocumentItemResponseModel);

        // Implemented the `PopulateFlags` method which is just looping through each item and checking it in the `ShouldAddFlag` method.
        public Task PopulateFlagsAsync&lt;TItem&gt;(IEnumerable&lt;TItem&gt; itemViewModels)
            where TItem : IHasFlags
        {
            foreach (TItem item in itemViewModels)
            {
                if (ShouldAddFlag(item))
                {
                    item.AddFlag(Alias);
                }
            }

            return Task.CompletedTask;
        }

        // We just get the ID of the document type and check it against our list of IDs we don't allow being deleted
        private bool ShouldAddFlag&lt;TItem&gt;(TItem item)
        {
            Guid id;
            switch (item)
            {
                case DocumentTreeItemResponseModel dti:
                    id = dti.DocumentType.Id;
                    break;
                case DocumentCollectionResponseModel dc:
                    id = dc.DocumentType.Id;
                    break;
                case DocumentItemResponseModel di:
                    id = di.DocumentType.Id;
                    break;
                default:
                    return false;
            }

            return LockedDocumentContentMovingToRecycleBinNotificationHandler.LOCKED_IDS.Contains(id);
        }
    }
}
</code></pre>

<p>This provider also needs registering in a composer.</p>

<pre><code>public class LockedDocumentComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.SignProviders()
            .Append&lt;LockedDocumentFlagProvider&gt;();

        // Our existing logic to disallow deleting the document deletion
        builder.AddNotificationHandler&lt;ContentMovingToRecycleBinNotification, LockedDocumentContentMovingToRecycleBinNotificationHandler&gt;();
    }
}
</code></pre>

<h2>Configuring the custom entity sign</h2>

<p>The entity sign is configured in a backoffice extension. If you're already extending the backoffice and have a manifests file already, please read on. Otherwise, I've written about extending the backoffice in my <a href="https://24days.in/umbraco-cms/2025/template-for-success">Template for Success</a> article on 24 Days in Umbraco.</p>

<p>Once the flag is in place, entity signs only require a manifest to configure them, no JavaScript required:</p>

<pre><code>import { UMB_DOCUMENT_ENTITY_TYPE } from '@umbraco-cms/backoffice/document';

export const manifests: Array&lt;UmbExtensionManifest&gt; = [
  // ...
  // Adding a new manifest of type `enitiySign` and kind `icon`
  {
    type: 'entitySign',
    kind: 'icon',
    alias: 'Umb.EntitySign.Document.My.Locked',
    name: 'Is Locked Document Entity Sign',
    // Specifying which enties can show this sign, documents
    forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
    // Specify what entities should be "flagged" with to make the sign show
    forEntityFlags: ['Umb.My.Locked'],
    // Can only show 2 icons at once, so the weighting matters. `-1000` means this one is really unimportant!
    weight: -1000,
    meta: {
      // Specifying what the sign looks like
      iconName: 'icon-lock',
      label: 'Locked',
      iconColorAlias: 'red',
    }
    // You'll notice we don't link to a TS file! Flagging is purely a C# concern
  }
];
</code></pre>

<h2>The result</h2>

<p>As you can see in the screenshot below, Home and Error are locked and have our new red padlock custom entity sign, while Features and Error have unpublished changes with the default pencil entity sign and Error has both, sorted by priority.</p>

<p><img src="/media/bd3j4mx4/locked-signs.png" alt="A screenshot of a content tree in Umbraco with a custom entity sign on the Home and Error nodes. The Features and Error nodes have the default unpublished changes entity sign." /></p>

<p>This is an example of what entity signs could be used for, but hopefully you can now imagine many more uses! And not just document types either!</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Bellissima Backoffice: Custom block views</title>
<link>https://joe.gl/ombek/blog/bellissima-backoffice-custom-block-views/</link>                <pubDate>Wed, 10 Dec 2025 05:20:34 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/bellissima-backoffice-custom-block-views/?rssid=#8b3a538f-47d7-4221-8426-1cf5cd7459d6</guid>
                <enclosure url="https://joe.gl/media/mqfervaj/block-custom-view-no-template.png" length="89061" type="image/png" />
                <description>Custom block views have changed with recent versions, so let&#39;s take a look at how we might do this.</description>
                <content:encoded><![CDATA[
                    <p>Custom block views have changed with recent versions, so let's take a look at how we might do this.</p>
<p>Custom backoffice block views are now configured using an Umbraco backoffice extension.</p>

<p>If you're already extending the backoffice and have a manifests file already, please read on. Otherwise, I've written about extending the backoffice in my <a href="https://24days.in/umbraco-cms/2025/template-for-success">Template for Success</a> article on 24 Days in Umbraco.</p>

<h2>The example</h2>

<p>In this post I'll be using the example of showing a little tag to indicate a block is hidden. I'm using the familiar pattern used by Clean Starter Kit of having a<code>hidden</code> setting on each block. Rather than rendering the text "[HIDDEN]" in each block template I'd like this to automatically appear for all blocks. Here's a mockup I made in Paint of what we're aiming for here.</p>

<p><img src="/media/nligk5hw/blocks-component-mockup.png" alt="A screenshot of a block list where some elements have a &quot;Hidden&quot; tag at the end of the text." /></p>

<h2>Registering the extension</h2>

<p>Add a manifest to your manifests file:</p>

<pre><code>export const manifests: Array&lt;UmbExtensionManifest&gt; = [
  // ...
  {
    // extension type is `blockEditorCustomView`
    type: 'blockEditorCustomView',
    alias: 'My.HiddenBlockEditorView',
    name: 'Hidden Block Editor View',
    // referencing a TS file for the element we want to use a the block view
    element: () =&gt; import('./hidden-block.element'),
    // specifying which block editor we're replacing the view for
    forBlockEditor: 'block-list'
  }
];
</code></pre>

<h2>Creating the custom element</h2>

<p>That referenced <code>hidden-block.element.ts</code> file looks like this:</p>

<pre><code>import { html, customElement, LitElement, property, css, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
import type { UmbBlockDataType } from '@umbraco-cms/backoffice/block';
import type { UmbBlockEditorCustomViewElement, UmbBlockEditorCustomViewConfiguration } from '@umbraco-cms/backoffice/block-custom-view';

@customElement('hidden-block-custom-view')
// Extending the `UmbElementMixin` and implementing `UmbBlockEditorCustomViewElement`
export class HiddenBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement {

    // UmbRefListBlockElement is not exposed to extend it, so we have to copy a lot of it in to replace it:

    @property({ type: String, reflect: false })
    label?: string;
   // ...
   // Other properties copied from UmbRefListBlockElement
   // ...

    // This render method is largely copied from UmbRefListBlockElement, with some customisations to show hidden state
    override render() {
        const blockValue = { ...this.content, $settings: this.settings, $index: this.index };
        return html`
            &lt;uui-ref-node standalone
        href=${(this.config?.showContentEdit ? this.config?.editContentPath : undefined) ?? ''}
        class="${this.settings?.hide ? 'hidden' : ''}"&gt;
            ${when(
                this.settings?.hide,
                () =&gt;
                    html`&lt;umb-icon slot="icon" name="icon-checkbox-dotted"&gt;&lt;/umb-icon&gt;`,
                () =&gt;
                    html`&lt;umb-icon slot="icon" .name=${this.icon}&gt;&lt;/umb-icon&gt;`
            )}
                &lt;umb-ufm-render slot="name" inline .markdown=${this.label} .value=${blockValue}&gt;&lt;/umb-ufm-render&gt;
                ${when(
                    this.unpublished,
                    () =&gt;
                        html`&lt;uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}
                                    &gt;&lt;umb-localize key="blockEditor_notExposedLabel"&gt;&lt;/umb-localize
                                &gt;&lt;/uui-tag&gt;`,
                )}
                ${when(
                    this.settings?.hide,
                    () =&gt;
                        html`&lt;uui-tag slot="name" look="secondary" title="Hidden"
                                &gt;Hidden&lt;/umb-localize
                            &gt;&lt;/uui-tag&gt;`
                )}
            &lt;/uui-ref-node&gt;
        `;
    }

    static override styles = [
        css`
            /* Copied styles from UmbRefListBlockElement and added custom styles /*
        `,
    ];
}
export default HiddenBlockCustomView;

declare global {
    interface HTMLElementTagNameMap {
        'hidden-block-custom-view': HiddenBlockCustomView;
    }
}
</code></pre>

<p><a href="https://github.com/slidejoe/template-for-success/blob/main/samples/My.UmbracoBackofficeExtensions/Client/src/hidden-block.element.ts">The full code sample is available on GitHub.</a></p>

<h2>The new view!</h2>

<p>With that in place, my block lists now look like this!</p>

<p><img src="/media/mqfervaj/block-custom-view-no-template.png" alt="A screenshot of a block list where some elements have a &quot;Hidden&quot; tag at the end of the text, are greyed out with a dashed-square icon" /></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Bellissima Backoffice: Blocks in the Rich Text Editor</title>
<link>https://joe.gl/ombek/blog/bellissima-backoffice-blocks-in-the-rte/</link>                <pubDate>Wed, 10 Dec 2025 11:32:32 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/bellissima-backoffice-blocks-in-the-rte/?rssid=#5d6aaef2-42d0-444d-9395-ed040b9627da</guid>
                <enclosure url="https://joe.gl/media/fuufngqs/copilot_20251210_123114.png" length="1617848" type="image/png" />
                <description>We&#39;ve been able to add blocks (as seen in Block List and Block Grid) to the Rich Text Editor (RTE) since Umbraco 13, but they&#39;re topical at the moment with v17 being released, the first LTS version since macros have been removed, more of us are looking to replace macros for good!

RTE Blocks are a new way of thinking about content (and a good replacement for macros!)</description>
                <content:encoded><![CDATA[
                    <p>We've been able to add blocks (as seen in Block List and Block Grid) to the Rich Text Editor (RTE) since Umbraco 13, but they're topical at the moment with v17 being released, the first LTS version since macros have been removed, more of us are looking to <a href="/ombek/blog/migrating-rte-macros/">replace macros</a> for good!</p>

<p>RTE Blocks are a new way of thinking about content (and a good replacement for macros!)</p>
<h2>Creating block types</h2>

<p>You can reuse your existing Block List and Block Grid blocks - they're exactly the same thing! But in most real use cases these will be different document types.</p>

<p>You simply need to create "element" type document types with all the properties you want on your new block, the difference comes in the data type you configure them in:</p>

<h2>Adding blocks to the RTE</h2>

<p>Instead of creating a Block List or Block Grid, we need to create (or modify an existing) Rich Text Editor data type.</p>

<p>We'll need to add the Block button to the RTE toolbar.</p>

<p><img src="/media/fwsadqij/rte-toolbar-settings.png" alt="A screenshot of the RTE toolbar settings with an arrow indicating dragging the Blocks button to toolbar." /></p>

<p>The RTE now has settings for configuring Blocks, just like in other Block editors:
<img src="/media/i00djapz/rte-blocks-settings.png" alt="A screenshot of the block configuration section of the RTE settings showing one configured for &quot;Phone Number RTE Block&quot;" /></p>

<p>Select your element types with optional settings and configure a label template, just as you would ordinarily as well as a setting for whether the block will render inline (think <code>span</code>) or as a block (think <code>div</code>).</p>

<h2>Rendering RTE Blocks</h2>

<p>This is probably where the RTE block differs most from the other block editors (and even that doesn't differ much!)</p>

<p>It simply needs a view matching the alias in the <code>Views\Partials\RichText\Components</code> folder.</p>

<p>Here's my example phone number view located at <code>TemplateForSuccess.Web\Views\Partials\RichText\Components\PhoneNumberRteBlock.cshtml</code></p>

<pre><code>@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage&lt;Umbraco.Cms.Core.Models.Blocks.RichTextBlockItem&lt;ContentModels.PhoneNumberRteBlock&gt;&gt;
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@using System.Text.RegularExpressions
@{
    var contactPage = Model.Content.ContactUsPage as ContentModels.Contact;

    if(contactPage is null || string.IsNullOrWhiteSpace(contactPage.PhoneNumber))
    {
        return;
    }
}

&lt;a href="tel:@contactPage.PhoneNumber.Replace(" ", "")"&gt;
    @Regex.Replace(contactPage.PhoneNumber, @"\+44\s*", "0")
&lt;/a&gt;
</code></pre>

<p>In this example I'm rendering a phone number from the selected contact page (a property on the Block), applying all the logic I want to format the phone number and adding the <code>tel:</code> link.</p>

<p>The journey for adding this block via the backoffice looks like this:</p>

<ul>
<li>Click the insert block button</li>
<li>Pick my phone number block</li>
<li>Complete the content for the block, a content picker for the contact page in my case</li>
<li>Save</li>
<li>And a placeholder appears inline in the RTE</li>
</ul>

<p><img src="/media/33vn5xeq/rte-block.apng" alt="A screen capture showing the steps above." /></p>

<p>Which, when viewed on the frontend renders my view:</p>

<p><img src="/media/0jsnhour/rte-blocks-rendered.png" alt="A screenshot of the RTE contents with the RTE Block replaced by the markup from my view." /></p>

<h2>Further reading</h2>

<p><a href="/ombek/blog/migrating-rte-macros/">Migrating Rich Text Editor Macros to Blocks using uSync Migrations</a></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Bellissima Backoffice: Property Descriptions</title>
<link>https://joe.gl/ombek/blog/bellissima-backoffice-property-descriptions/</link>                <pubDate>Mon, 24 Nov 2025 07:15:24 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/bellissima-backoffice-property-descriptions/?rssid=#0f86c27b-ea3a-4033-b759-60276313b663</guid>
                <enclosure url="https://joe.gl/media/f3gm4ln5/screenshot-2025-12-10-213356.png" length="80835" type="image/png" />
                <description>I&#39;m sure you&#39;re already aware of property descriptions! They’re useful to explain a property when a name doesn&#39;t give enough detail. The property descriptions have accepted markdown for a long time now (although allowed features have changed over time!). And because it’s markdown, that means we can use HTML too. And because its HTML it means we can also make use of web components, like UUI!</description>
                <content:encoded><![CDATA[
                    <p>I'm sure you're already aware of property descriptions! They’re useful to explain a property when a name doesn't give enough detail. The property descriptions have accepted markdown for a long time now (although allowed features have changed over time!). And because it’s markdown, that means we can use HTML too. And because its HTML it means we can also make use of web components, like UUI!</p>
<h3>Property Descriptions: Read more</h3>

<p>One of the features that's changed in recent versions is the "read more" functionality. In earlier versions of Umbraco, <code>---</code> rendered a read more link.</p>

<pre><code>Short description

---

Descriptions below a `---` were rendered behind a "Read More" link in v9-13.
</code></pre>

<p>This is no longer the case, <code>---</code> renders a horizontal rule element (as it should!) but we can recreate the read more functionality with modern HTML.</p>

<pre><code>Short description

---

Three dashes renders a horizontal rule.

&lt;details&gt;
&lt;summary&gt;Read more&lt;/summary&gt;

We have to use the native HTML `details` element for modern Umbraco.

&lt;/details&gt;
</code></pre>

<p>We can even take that a step further - in this example I’m using: markdown to render italic text, a code sample and a hyperlink; HTML to render a <code>&lt;details&gt;</code> element to recreate the read more functionality; and because UUI uses web components, which are HTML, we can bring UUI elements into the descriptions too, so I’ve got a UUI Umbraco help icon in there too to make the property description rich and intuitive without taking too much space.</p>

<pre><code>Should search engines and other crawlers index this page and serve them up as search results?
&lt;details&gt;
&lt;summary&gt;
  &lt;uui-icon name="icon-help-alt" label="More details"&gt;&lt;/uui-icon&gt; 
&lt;/summary&gt;

This sets the *index* aspect of the [`robots` meta tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/meta/name/robots)

&lt;/details&gt;
</code></pre>

<p><img src="/media/csahpboa/prop-description.png" alt="A screen capture of the markdown rendered in the backoffice with a user clicking on the question-mark help icon to expand further details." /></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Regularly Expressed Regular Expressions</title>
<link>https://joe.gl/ombek/blog/eg-regex/</link>                <pubDate>Thu, 06 Nov 2025 01:42:38 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/eg-regex/?rssid=#f7616033-d01d-4014-a6c9-60c27a0b76a8</guid>
                <enclosure url="https://joe.gl/media/ow4bhpsj/regex.jpg" length="199915" type="image/jpg" />
                <description>Examples of common or useful regular expressions I&#39;ve created over the years.</description>
                <content:encoded><![CDATA[
                    <p>Examples of common or useful regular expressions I've created over the years.</p>
<h2>Useful expressions</h2>

<p>I'll keep coming back to this blog post as I create more, so it might be worth a bookmark!</p>

<p>The regular expressions that start with <code>^</code> (start of string/line) and end with <code>$</code> (end of string/line) will ensure the whole string matches. This is useful for validating a string is in the format you expect.
To find a string in the format you expect inside another string, these can be omitted. </p>

<p>Most of these regular expressions can be changed to the other method simply by adding/removing the <code>^</code>(start of string/line) and <code>$</code> (end of string/line) characters at the start and end respectively.</p>

<h3>GUID</h3>

<h4>Validate a string is a GUID</h4>

<p><code>^(?:\((?=.*\))|{(?=.*}))?[a-fA-F0-9]{8}(?:-(?=(?:.*-){3}))?(?:[a-fA-F0-9]{4}-?){3}[a-fA-F0-9]{12}(?:\)(?&lt;=\(.*)|}(?&lt;={.*))?$</code></p>

<p><a href="https://regexr.com/8hv1f">Open in RegExr</a></p>

<p>Matches guids in most formats that .NET's <code>Guid.Parse</code> can handle (except <code>0x</code> format), is case insensitive, dashes and brackets are optional, but brackets must match.</p>

<h4>Replace a GUID inside a longer string</h4>

<p>As above without leading <code>^</code> and trailing <code>$</code>.</p>

<h4>Find inside a longer string</h4>

<p><code>[a-fA-F0-9]{8}(?:-(?=(?:[a-fA-F0-9]+-[a-fA-F0-9]+){3}))?(?:[a-fA-F0-9]{4}-?){3}[a-fA-F0-9]{12}(?=[$\s)}])</code></p>

<p><a href="https://regexr.com/8hv1l">Open in RegExr</a></p>

<p>This one is simpler as it doesn't need to worry about the brackets - we're looking for the bare minimum that constitutes a GUID.</p>

<h3>YouTube URLs</h3>

<p><code>https?:\/\/(?:www.)?(?:(?:youtube.com\/(?:v\/|embed\/|watch\?v=|\&amp;v=))|(?:youtu.be\/))([^#\&amp;\?\s]+)\S*</code></p>

<p><a href="https://regexr.com/8hup7">Open in RegExr</a></p>

<p>Will match many forms of YouTube URL (default, short links, embed). The first group in the match will be the video ID which can be used to construct an embed URL.</p>

<h3>Vimeo URLs</h3>

<p><code>https?:\/\/(?:(?:www\.)|(?:player\.))?(?:vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/(?:\w+\/)?videos\/|album\/(?:\d+\/)?video\/|video\/|external\/)?(\d+))\S*</code></p>

<p><a href="https://regexr.com/8hupd">Open in RegExr</a></p>

<p>With thanks to <a href="https://huskey.uk/">Mike Masey</a> for the starting point of this one.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbraco property storage cheat sheet</title>
<link>https://joe.gl/ombek/blog/umbraco-property-storage/</link>                <pubDate>Thu, 11 Sep 2025 09:17:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/umbraco-property-storage/?rssid=#106d369f-c7b9-423e-a3ea-a3d078c56c4b</guid>
                <enclosure url="https://joe.gl/media/xxud5u1z/copilot_20251021_145610.png" length="2167748" type="image/png" />
                <description>Was it a comma separated list or a JSON array? When using the Content Service, it can be really useful to know what format to store data in.</description>
                <content:encoded><![CDATA[
                    <p>Was it a comma separated list or a JSON array? When using the Content Service, it can be really useful to know what format to store data in.</p>
<h2><code>Umbraco.MultiUrlPicker</code></h2>

<p>Still an array, even in single mode.</p>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>[
  {
    "name": "Link title",
    "udi": "umb://document/bb6ca0ea21334bc6b345afd7c703b965"
  },
  {
    "name": "Link title",
    "url": "https://example.com",
    "queryString": "#with-anchor",
    "target": "_blank"
  }
]
</code></pre>

<p><strong>The above is prettified for legibility. The value in Umbraco will have no spaces or new lines.</strong></p>

<p><del>
<a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/multi-url-picker#add-values-programmatically">How to programmatically update a Multi Url Picker</a>
</del> This does not work.</p>

<h2><code>Umbraco.MultiNodeTreePicker</code></h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>umb://document/bb6ca0ea21334bc6b345afd7c703b965,umb://document/e3d3e3b32d484424815286d4042a80a4
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/content-picker#add-values-programmatically">How to programmatically update a "Content Picker"/Multi-node tree picker</a></p>

<h2><code>Umbraco.ContentPicker</code></h2>

<p>Stored in SQL <code>varcharValue</code> column.</p>

<pre><code>umb://document/bb6ca0ea21334bc6b345afd7c703b965
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/document-picker#add-values-programmatically">How to programmatically update a "Document Picker"/<code>Umbraco.ContentPicker</code></a></p>

<h2><code>Umbraco.RadioButtonList</code></h2>

<p>Stored in SQL <code>varcharValue</code> column.</p>

<pre><code>One
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/radiobutton-list#add-values-programmatically">How to programmatically update a Radio Button List</a></p>

<h2><code>Umbraco.CheckBoxList</code></h2>

<p>Stored in SQL <code>varcharValue</code> column.</p>

<pre><code>["One","Two"]
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/checkbox-list#add-values-programmatically">How to programmatically update a Checkbox List</a></p>

<h2><code>Umbraco.TrueFalse</code></h2>

<p>Stored in SQL <code>intValue</code> column.</p>

<pre><code>0
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/true-false#add-values-programmatically">How to programmatically update a Toggle/True-False/checkbox</a></p>

<h2><code>Umbraco.Tags</code></h2>

<p>Stored in SQL <code>varcharValue</code> column.</p>

<pre><code>["Tag1","Tag2"]
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/tags#setting-tags-programmatically">How to programmatically update tags</a></p>

<h2><code>Umbraco.DateTime</code></h2>

<p>Stored in SQL <code>dateValue</code> column.</p>

<pre><code>2023-03-15 16:20:00
</code></pre>

<p>(Time value is <code>00:00:00</code> if time is excluded)</p>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/date-time#add-values-programmatically">How to programmatically update a DateTime</a></p>

<h2><code>Umbraco.MemberPicker</code></h2>

<p>Stored in SQL <code>varcharValue</code> column.</p>

<pre><code>umb://member/efe1f9fb4a1b460895e70f839ba114d6
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/member-picker#add-values-programmatically">How to programmatically update a Member picker</a></p>

<h2><code>Umbraco.UserPicker</code></h2>

<p>Stored in SQL <code>intValue</code> column.</p>

<pre><code>-1
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/user-picker#add-values-programmatically">How to programmatically update a User picker</a></p>

<h2><code>Umbraco.MultipleTextstring</code> (AKA Repeatable textstrings)</h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>One
Two
Three
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/multiple-textbox#add-values-programmatically">How to programmatically update repeatable textstrings</a></p>

<h2><code>Umbraco.UploadField</code> (AKA File upload)</h2>

<p>Stored in SQL <code>varcharValue</code> column.</p>

<pre><code>/media/inshb1yh/callum-au.png
</code></pre>

<p><del><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/file-upload#add-values-programmatically">How to programmatically update an Upload Field</a>.</del> <ins><strong>Note:</strong> This example is inconsistent with the default behaviour. It uploads the file to the Media library, unlike how File Uploads work via the UI.</ins></p>

<h2><code>Umbraco.ImageCropper</code></h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>{"src":"/media/nojjl5no/callum-au.png"}
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/image-cropper#add-values-programmatically">How to programmatically update an Image Cropper field.</a></p>

<h2><code>Umbraco.MediaPicker3</code></h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>[
  {
    "key": "22ff8e5b-062d-43ac-8aec-fb09ac2277ba",
    "mediaKey": "c63dc11a-4825-488b-8646-36211b812f3b"
  }
]
</code></pre>

<p><strong>The above is prettified for legibility. The value in Umbraco will have no spaces or new lines.</strong></p>

<p><del><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/media-picker-3#add-values-programmatically">How to programmatically update a media picker</a></del> <ins>This looks incorrect.</ins></p>

<h2><code>Umbraco.MediaPicker</code> (legacy AKA v2)</h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>umb://media/c63dc11a4825488b864636211b812f3b
</code></pre>

<p><a href="https://docs.umbraco.com/umbraco-cms/13.latest/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/media-picker#add-values-programmatically">How to programmatically update a legacy media picker</a></p>

<h2><code>Umbraco.BlockList</code></h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>{
  "layout": {
    "Umbraco.BlockList": [
      {
        "contentUdi": "umb://element/2515bbf8470c43a1bda132d643a4a418"
      },
      {
        "contentUdi": "umb://element/05c3c37d6a814f7a9ac67be3d01dd057"
      }
    ]
  },
  "contentData": [
    {
      "contentTypeKey": "ec586638-a55d-463a-b36e-62ed11e2b2df",
      "udi": "umb://element/2515bbf8470c43a1bda132d643a4a418",
      "property1": "Value"
    },
    {
      "contentTypeKey": "ec586638-a55d-463a-b36e-62ed11e2b2df",
      "udi": "umb://element/05c3c37d6a814f7a9ac67be3d01dd057",
      "property1": "Value"
    }
  ],
  "settingsData": []
}
</code></pre>

<p>The documentation does not currently document how to programmatically update a block list. See block grid for inspiration or read Nathaniel Nunes' 24 Days in Umbraco article on <a href="https://24days.in/umbraco-cms/2025/importing-blocklist-items/">Importing Block List Items Programmatically in Umbraco 17</a></p>

<h2><code>Umbraco.BlockList</code> (single mode)</h2>

<p>Exactly the same as non-single mode (but with only one item!)</p>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>{
  "layout": {
    "Umbraco.BlockList": [
      {
        "contentUdi": "umb://element/9ae5d1abe0a84b80a4ace9ae8ab1311b"
      }
    ]
  },
  "contentData": [
    {
      "contentTypeKey": "ec586638-a55d-463a-b36e-62ed11e2b2df",
      "udi": "umb://element/9ae5d1abe0a84b80a4ace9ae8ab1311b",
      "property1": "Value"
    }
  ],
  "settingsData": []
}
</code></pre>

<p><strong>The above is prettified for legibility. The value in Umbraco will have no spaces or new lines.</strong></p>

<p>The documentation does not currently document how to programmatically update a block list. See block grid for inspiration.</p>

<h2><code>Umbraco.BlockGrid</code></h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<pre><code>{
  "layout": {
    "Umbraco.BlockGrid": [
      {
        "contentUdi": "umb://element/df08225d94af450a98500b35d4b0e3d4",
        "areas": [],
        "columnSpan": 12,
        "rowSpan": 1
      },
      {
        "contentUdi": "umb://element/e373cf3ef53447f5858941a8640db5b4",
        "areas": [
          {
            "key": "2b314d42-64f8-4608-897b-0b55b449427c",
            "items": [
              {
                "contentUdi": "umb://element/81e9a611a5c84d929d1bb8d5026fc607",
                "areas": [],
                "columnSpan": 6,
                "rowSpan": 1
              }
            ]
          },
          {
            "key": "223adcd7-7e4d-4b95-9c6b-eb2047a01607",
            "items": [
              {
                "contentUdi": "umb://element/bb441112ad794756a542ac1cd7fc4477",
                "areas": [],
                "columnSpan": 6,
                "rowSpan": 1
              }
            ]
          }
        ],
        "columnSpan": 12,
        "rowSpan": 1
      }
    ]
  },
  "contentData": [
    {
      "contentTypeKey": "df88b972-6ba5-4da8-a966-036137a935ad",
      "udi": "umb://element/df08225d94af450a98500b35d4b0e3d4",
      "headline": "Value"
    },
    {
      "contentTypeKey": "cb50a504-b8cd-4262-8344-bddfddc09282",
      "udi": "umb://element/e373cf3ef53447f5858941a8640db5b4"
    },
    {
      "contentTypeKey": "df88b972-6ba5-4da8-a966-036137a935ad",
      "udi": "umb://element/81e9a611a5c84d929d1bb8d5026fc607",
      "headline": "Column 1"
    },
    {
      "contentTypeKey": "df88b972-6ba5-4da8-a966-036137a935ad",
      "udi": "umb://element/bb441112ad794756a542ac1cd7fc4477",
      "headline": "Column 2"
    }
  ],
  "settingsData": []
}
</code></pre>

<p><strong>The above is prettified for legibility. The value in Umbraco will have no spaces or new lines.</strong></p>

<p><a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/block-editor/block-grid-editor#creating-a-block-grid-programmatically">How to programmatically update a block grid</a></p>

<h2><code>Umbraco.RichText</code> (including RTE blocks)</h2>

<p>Stored in SQL <code>textValue</code> column.</p>

<p>Versions before v13 stored the markup as a string. With the introduction of blocks in the RTE the structure changed to JSON, with the markup stored in the <code>markup</code> property. The rest of the properties relate to blocks referenced in the markup.</p>

<pre><code>{
  "markup": "\u003Cp\u003E\u003Cumb-rte-block-inline data-content-key=\u0022fd41689d-5c48-47a3-91cb-a702e34f2716\u0022\u003E\u003C/umb-rte-block-inline\u003E\u003C/p\u003E",
  "blocks": {
    "contentData": [
      {
        "contentTypeKey": "6d09c413-9063-4ab2-bd6e-eea970903528",
        "udi": null,
        "key": "fd41689d-5c48-47a3-91cb-a702e34f2716",
        "values": [
          {
            "editorAlias": "Umbraco.MultiNodeTreePicker",
            "culture": null,
            "segment": null,
            "alias": "contactUsPage",
            "value": "umb://document/14e766739b714259aac3fc9507eb3bdb"
          }
        ]
      }
    ],
    "settingsData": [],
    "expose": [
      {
        "contentKey": "fd41689d-5c48-47a3-91cb-a702e34f2716",
        "culture": null,
        "segment": null
      }
    ],
    "Layout": {
      "Umbraco.RichText": [
        {
          "contentUdi": null,
          "settingsUdi": null,
          "contentKey": "fd41689d-5c48-47a3-91cb-a702e34f2716",
          "settingsKey": null
        }
      ]
    }
  }
}
</code></pre>

<p><strong>The above is prettified for legibility. The value in Umbraco will have no spaces or new lines.</strong></p>

<p>The docs guide on <a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/rich-text-editor#add-values-programmatically">How to programmatically update a Rich Text Editor</a> is currently out of date. Take inspiration from the Block Grid docs or my post on <a href="/ombek/blog/migrating-rte-macros/">Migrating RTE Macros</a>.</p>

<p>I'm not sure what the <code>expose</code> section does, but it may not be necessary?</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Migrating Rich Text Editor Macros to Blocks using uSync Migrations</title>
<link>https://joe.gl/ombek/blog/migrating-rte-macros/</link>                <pubDate>Thu, 04 Sep 2025 02:17:42 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/migrating-rte-macros/?rssid=#790bd665-3d7d-4f30-a1db-0e5adcd8dd33</guid>
                <enclosure url="https://joe.gl/media/giehifan/_de5c2270-3669-4f81-abbb-4d4012c5048e-1.jpg" length="294397" type="image/jpg" />
                <description>Modern Umbraco allows Blocks to be added inline in the Rich Text Editor. Umbraco 14 removes support for macros. This code sample will map macros to blocks with the help of uSync Migrations.</description>
                <content:encoded><![CDATA[
                    <p>Modern Umbraco allows Blocks to be added inline in the Rich Text Editor. Umbraco 14 removes support for macros. This code sample will map macros to blocks with the help of uSync Migrations.</p>
<p>I haven't yet worked out a way to contribute this back to the uSync Migrations project as it requires too much customisation. If anybody wants to take this and make it more generically compatible, please do.</p>

<p>The first step is to create element types equivalent to all your macros. Feel free to change names, as the next step is to map them.</p>

<p>Create a Migrator that inherits from <code>RichTextBoxMigrator</code>:</p>

<pre><code>[SyncMigrator(UmbConstants.PropertyEditors.Aliases.TinyMce, typeof(RichTextConfiguration), IsDefaultAlias = true)]
[SyncMigrator("Umbraco.TinyMCEv3")]
[SyncMigratorVersion(7, 8)]
public class RichTextBoxMacrosToBlocksMigrator : RichTextBoxMigrator
{
    private IJsonSerializer _jsonSerializer;
    private ILogger&lt;RichTextBoxMacrosToBlocksMigrator&gt; _logger;

    public RichTextBoxMacrosToBlocksMigrator(ILogger&lt;RichTextBoxMacrosToBlocksMigrator&gt; logger, IJsonSerializer jsonSerializer)
    {
        _logger = logger;
        _jsonSerializer = jsonSerializer;
    }
}
</code></pre>

<p>Then create a mapping for all the macros you want to replace with blocks. My mapping class has a field for the macro alias, the alias of the new element type and a dictionary optionally mapping the macro parameter name to the new property alias (no need to include properties that are the same).</p>

<pre><code>private class MacroMapping
{
    public required string MacroAlias { get; set; }
    public required string BlockTypeAlias { get; set; }
    public Dictionary&lt;string, string&gt;? PropertyMappings { get; set; }
}

private IEnumerable&lt;MacroMapping&gt; mappings = new List&lt;MacroMapping&gt;()
{
    new MacroMapping()
    {
        MacroAlias = "renderUmbracoForm",
        BlockTypeAlias = "RenderUmbracoFormEmbeddedBlock",
        PropertyMappings = new Dictionary&lt;string, string&gt;()
        {
            { "ExcludeScripts", "excludeScripts" },
            { "FormGuid", "form" },
            { "FormTheme", "theme" },
            { "RedirectToPageId", "redirectToPage" }
        }
    }
};
</code></pre>

<p>Then we can map these across by overriding the <code>GetContentValue</code> method:</p>

<pre><code>public override string? GetContentValue(SyncMigrationContentProperty contentProperty, SyncMigrationContext context)
{
    var rawValue = base.GetContentValue(contentProperty, context) ?? string.Empty;

    var value = new RteValue();

    // Newer sites or RTEs within blocks will already have been converted to the new format, so attempt to parse the JSON
    if (RichTextPropertyEditorHelper.TryParseRichTextEditorValue(rawValue, _jsonSerializer, _logger, out RichTextEditorValue? existingValue))
    {
        // Migrate existing blocks
        value.Blocks!.ContentData.AddRange(existingValue.Blocks?.ContentData.Select(x =&gt; new BlockListRowValue() { ContentTypeKey = x.ContentTypeKey, RawPropertyValues = x.RawPropertyValues, Udi = x.Udi?.ToString() }) ?? Enumerable.Empty&lt;BlockListRowValue&gt;());
        value.Blocks!.SettingsData.AddRange(existingValue.Blocks?.SettingsData.Select(x =&gt; new BlockListRowValue() { ContentTypeKey = x.ContentTypeKey, RawPropertyValues = x.RawPropertyValues, Udi = x.Udi?.ToString() }) ?? Enumerable.Empty&lt;BlockListRowValue&gt;());
        value.Blocks!.Layout!.BlockOrder.AddRange(existingValue.Blocks?.Layout?["Umbraco.TinyMCE"].Select(x =&gt; new BlockUdiValue() { ContentUdi = x["contentUdi"]?.Value&lt;string&gt;(), SettingsUdi = x["settingsUdi"]?.Value&lt;string&gt;() }) ?? Enumerable.Empty&lt;BlockUdiValue&gt;());
    }
    else
    {
        // Existing value is plain HTML markup, we need to update it to match the new format
        existingValue = new RichTextEditorValue() { Markup = rawValue, Blocks = new Umbraco.Cms.Core.Models.Blocks.BlockValue() };
    }

    value.Markup = Regex.Replace(existingValue.Markup, @"\&lt;\?UMBRACO_MACRO macroAlias=""(?&lt;macro&gt;[^""]+)""(?:\s*(?&lt;prop&gt;[^=]*)=""(?&lt;propval&gt;[^""]*)"")*? \/&gt;", match =&gt;
    {
        var macroAlias = match.Groups["macro"].Value;
        var mapping = mappings.FirstOrDefault(x =&gt; x.MacroAlias.InvariantEquals(macroAlias));
        if (mapping != null)
        {
            var block = new BlockListRowValue()
            {
                ContentTypeKey = context.ContentTypes.GetKeyByAlias(mapping.BlockTypeAlias),
                Udi = $"umb://element/{Guid.NewGuid():N}"
            };

            // Add block config

            var propVals = match.Groups["propval"].Captures;
            var propNames = match.Groups["prop"].Captures;

            for (int i = 0; i &lt; propVals.Count; i++)
            {
                var propVal = HttpUtility.HtmlDecode(propVals[i].Value);
                var propName = propNames[i].Value;
                propName = mapping.PropertyMappings?[propName] ?? propName;

                block.RawPropertyValues[propName] = propVal;
            }

            value.Blocks!.ContentData.Add(block);

            // Add settings if nescessary
            // value.Blocks.SettingsData.Add(settings);

            value.Blocks!.Layout!.BlockOrder.Add(new BlockUdiValue()
            {
                ContentUdi = block.Udi,
                //SettingsUdi = settings.Udi
            });

            return $"&lt;umb-rte-block class=\"ng-scope ng-isolate-scope\" data-content-udi=\"{block.Udi}\"&gt;&lt;!--Umbraco-Block--&gt;&lt;/umb-rte-block&gt;";
        }
        return match.Value;
    }, RegexOptions.IgnoreCase | RegexOptions.Multiline);

    if (value.Blocks?.Layout?.BlockOrder?.Any() != true)
    {
        // Clear this out if we have no blocks
        value.Blocks = null;
    }

    return JsonConvert.SerializeObject(value, Formatting.Indented);
}
</code></pre>

<p>This code sample makes use of custom strongly-typed models for the RTE value. These are included in the full code sample at the end.</p>

<p>You'll then need to add this migrator to your custom Migration Plan:</p>

<pre><code>using System;
using System.Collections.Generic;
using uSync.Migrations.Core;
using uSync.Migrations.Core.Composing;
using uSync.Migrations.Core.Configuration.Models;
using UmbConstants = Umbraco.Cms.Core.Constants;

public class MyMigrationPlan : ISyncMigrationPlan
{
    private readonly SyncMigrationHandlerCollection _migrationHandlers;

    public MyMigrationPlan(SyncMigrationHandlerCollection migrationHandlers)
    {
        _migrationHandlers = migrationHandlers;
    }

    public int Order =&gt; 250;
    public string Name =&gt; "My Migration Plan";
    public string Icon =&gt; "icon-brick color-green";
    public string Description =&gt; "";

    public MigrationOptions Options =&gt; new MigrationOptions
    {
        Group = "Convert",
        Source = "uSync/v9",
        Target = $"{uSyncMigrations.MigrationFolder}/{DateTime.Now:yyyyMMdd_HHmmss}",
        Handlers = _migrationHandlers.SelectGroup(8, string.Empty),
        SourceVersion = 8,

        // This adds our migrator
        PreferredMigrators = new Dictionary&lt;string, string&gt;
        {
            { UmbConstants.PropertyEditors.Aliases.TinyMce, "RichTextBoxMacrosToBlocksMigrator" },
        }
    };
}
</code></pre>

<p>You'll notice I'm using the <code>Convert</code> ("Convert this site" via the UI) method that uSync Migrations offers rather than <code>legacy</code> ("Migration" via the UI) . This is for converting content already on a site. This works well if you've upgraded a site to v13 (where macros and blocks in RTEs are both supported), run the migrators and then (optionally) migrate to a newer Umbraco version.</p>

<h3>Complete <code>RichTextBoxMacrosToBlocksMigrator.cs</code> file</h3>

<pre><code>using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
using UmbConstants = Umbraco.Cms.Core.Constants;
using uSync.Migrations.Core.Migrators;
using uSync.Migrations.Core.Migrators.Models;
using uSync.Migrations.Core.Context;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using System;
using System.Web;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Serialization;
using Microsoft.Extensions.Logging;

namespace uSync.Migrations.Migrators.Core;

[SyncMigrator(UmbConstants.PropertyEditors.Aliases.TinyMce, typeof(RichTextConfiguration), IsDefaultAlias = true)]
[SyncMigrator("Umbraco.TinyMCEv3")]
[SyncMigratorVersion(7, 8)]
public class RichTextBoxMacrosToBlocksMigrator : RichTextBoxMigrator
{
    private IJsonSerializer _jsonSerializer;
    private ILogger&lt;RichTextBoxMacrosToBlocksMigrator&gt; _logger;

    public RichTextBoxMacrosToBlocksMigrator(ILogger&lt;RichTextBoxMacrosToBlocksMigrator&gt; logger, IJsonSerializer jsonSerializer)
    {
        _logger = logger;
        _jsonSerializer = jsonSerializer;
    }


    // Map across each macro to an already created type and optionally map the properties
    private IEnumerable&lt;MacroMapping&gt; mappings = new List&lt;MacroMapping&gt;()
    {
        new MacroMapping()
        {
            MacroAlias = "renderUmbracoForm",
            BlockTypeAlias = "RenderUmbracoFormEmbeddedBlock",
            PropertyMappings = new Dictionary&lt;string, string&gt;()
            {
                { "ExcludeScripts", "excludeScripts" },
                { "FormGuid", "form" },
                { "FormTheme", "theme" },
                { "RedirectToPageId", "redirectToPage" }
            }
        }
    };

    public override string? GetContentValue(SyncMigrationContentProperty contentProperty, SyncMigrationContext context)
    {
        var rawValue = base.GetContentValue(contentProperty, context) ?? string.Empty;

        var value = new RteValue();

        // Newer sites or RTEs within blocks will already have been converted to the new format, so attempt to parse the JSON
        if (RichTextPropertyEditorHelper.TryParseRichTextEditorValue(rawValue, _jsonSerializer, _logger, out RichTextEditorValue? existingValue))
        {
            // Migrate existing blocks
            value.Blocks!.ContentData.AddRange(existingValue.Blocks?.ContentData.Select(x=&gt; new BlockListRowValue() { ContentTypeKey = x.ContentTypeKey, RawPropertyValues = x.RawPropertyValues, Udi = x.Udi?.ToString() }) ?? Enumerable.Empty&lt;BlockListRowValue&gt;());
            value.Blocks!.SettingsData.AddRange(existingValue.Blocks?.SettingsData.Select(x =&gt; new BlockListRowValue() { ContentTypeKey = x.ContentTypeKey, RawPropertyValues = x.RawPropertyValues, Udi = x.Udi?.ToString() }) ?? Enumerable.Empty&lt;BlockListRowValue&gt;());
            value.Blocks!.Layout!.BlockOrder.AddRange(existingValue.Blocks?.Layout?["Umbraco.TinyMCE"].Select(x =&gt; new BlockUdiValue() { ContentUdi = x["contentUdi"]?.Value&lt;string&gt;(), SettingsUdi = x["settingsUdi"]?.Value&lt;string&gt;() }) ?? Enumerable.Empty&lt;BlockUdiValue&gt;());
        }
        else
        {
            // Existing value is plain HTML markup, we need to update it to match the new format
            existingValue = new RichTextEditorValue() { Markup = rawValue, Blocks = new Umbraco.Cms.Core.Models.Blocks.BlockValue() };
        }

        value.Markup = Regex.Replace(existingValue.Markup, @"\&lt;\?UMBRACO_MACRO macroAlias=""(?&lt;macro&gt;[^""]+)""(?:\s*(?&lt;prop&gt;[^=]*)=""(?&lt;propval&gt;[^""]*)"")*? \/&gt;", match =&gt;
        {
            var macroAlias = match.Groups["macro"].Value;
            var mapping = mappings.FirstOrDefault(x =&gt; x.MacroAlias.InvariantEquals(macroAlias));
            if (mapping != null)
            {
                var block = new BlockListRowValue()
                {
                    ContentTypeKey = context.ContentTypes.GetKeyByAlias(mapping.BlockTypeAlias),
                    Udi = $"umb://element/{Guid.NewGuid():N}"
                };

                // Add block config

                var propVals = match.Groups["propval"].Captures;
                var propNames = match.Groups["prop"].Captures;

                for (int i = 0; i &lt; propVals.Count; i++)
                {
                    var propVal = HttpUtility.HtmlDecode(propVals[i].Value);
                    var propName = propNames[i].Value;
                    propName = mapping.PropertyMappings?[propName] ?? propName;

                    block.RawPropertyValues[propName] = propVal;
                }

                value.Blocks!.ContentData.Add(block);

                // Add settings if nescessary
                // value.Blocks.SettingsData.Add(settings);

                value.Blocks!.Layout!.BlockOrder.Add(new BlockUdiValue()
                {
                    ContentUdi = block.Udi,
                    //SettingsUdi = settings.Udi
                });

                return $"&lt;umb-rte-block class=\"ng-scope ng-isolate-scope\" data-content-udi=\"{block.Udi}\"&gt;&lt;!--Umbraco-Block--&gt;&lt;/umb-rte-block&gt;";
            }
            return match.Value;
        }, RegexOptions.IgnoreCase | RegexOptions.Multiline);

        if (value.Blocks?.Layout?.BlockOrder?.Any() != true)
        {
            // Clear this out if we have no blocks
            value.Blocks = null;
        }

        return JsonConvert.SerializeObject(value, Formatting.Indented);
    }

    private class MacroMapping
    {
        public required string MacroAlias { get; set; }
        public required string BlockTypeAlias { get; set; }
        public Dictionary&lt;string, string&gt;? PropertyMappings { get; set; }
    }

    // Strongly typed versions of RichTextEditorValue
    private class RteValue
    {
        [JsonProperty("markup")]
        public string? Markup { get; set; }
        [JsonProperty("blocks")]
        public BlockListValue? Blocks { get; set; } = new BlockListValue();
    }
    private class BlockListValue
    {
        [JsonProperty("layout")]
        public BlockListLayoutValue? Layout { get; set; } = new BlockListLayoutValue();

        [JsonProperty("contentData")]
        public List&lt;BlockListRowValue&gt; ContentData { get; set; } = new List&lt;BlockListRowValue&gt;();

        [JsonProperty("settingsData")]
        public List&lt;BlockListRowValue&gt; SettingsData { get; set; } = new List&lt;BlockListRowValue&gt;();
    }
    private class BlockListLayoutValue
    {
        [JsonProperty("Umbraco.TinyMCE")]
        public List&lt;BlockUdiValue&gt; BlockOrder { get; set; } = new List&lt;BlockUdiValue&gt;();
    }
    private class BlockUdiValue
    {
        [JsonProperty("contentUdi")]
        public string? ContentUdi { get; set; }

        [JsonProperty("settingsUdi")]
        public string? SettingsUdi { get; set; }
    }
    private class BlockListRowValue
    {
        [JsonProperty("contentTypeKey")]
        public Guid ContentTypeKey { get; set; }

        [JsonProperty("udi")]
        public string? Udi { get; set; }

        [JsonExtensionData]
        public IDictionary&lt;string, object?&gt; RawPropertyValues { get; set; } = new Dictionary&lt;string, object?&gt;();
    }
}
</code></pre>                ]]></content:encoded>
            </item>
            <item>
                <title>Codegarden: What to bring (2025 edition!)</title>
<link>https://joe.gl/ombek/blog/codegarden-packing-list/</link>                <pubDate>Thu, 15 May 2025 09:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/codegarden-packing-list/?rssid=#83cbf7a6-0d0f-4bb4-8bd0-86c09f9d6993</guid>
                <enclosure url="https://joe.gl/media/lwunqcat/change-the-text-on-the-lanyard-to-_codegarden_.png" length="1531307" type="image/png" />
                <description>This time next month I&#39;ll be boarding my plane to Denmark ahead of Codegarden 2025 so it&#39;s about time we thought about packing our bags! But what to bring? If you&#39;ve not been before, or if you&#39;re forgetful like I am, I&#39;ve written you a packing list...</description>
                <content:encoded><![CDATA[
                    <p>This time next month I'll be boarding my plane to Denmark ahead of Codegarden 2025 so it's about time we thought about packing our bags! But what to bring? If you've not been before, or if you're forgetful like I am, I've written you a packing list...</p>
<h2>Clothes</h2>

<p>Codegarden is a pretty casual affair - think jeans, tee and a hoodie. The weather is generally pretty mild, though it can get a little chilly and rainy - but we often see the sun too!</p>

<ul>
<li>swashbuckling pre-party outfit - the theme this year is pirates! 🏴‍☠️</li>
<li>warm jacket, just in case!</li>
<li>waterproof jacket</li>
<li>sun hat</li>
<li>hoodie/jacket/sweater, in case the venue is a little chilly or you're out late</li>
<li>shorts</li>
<li>tops and t-shirts (Umbraco swag, if you have them!)</li>
<li>jeans/casual trousers</li>
<li>dresses/skirts, but keep it casual</li>
<li>comfortable shoes/trainers <a href="https://www.proworks.com/blog/archive/its-beginning-to-look-a-lot-like-codegarden/">(Jen aggrees!)</a></li>
<li>flip-flops <a href="https://discord.com/channels/869656431308189746/882984622978379786/1117737130907672626">(if you ask Discord, that is!)</a></li>
<li>dancing shoes for Thursday night 💃🕺<a href="https://www.linkedin.com/feed/update/urn:li:activity:7196102038684876800?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7196102038684876800%2C7197338933247008768%29&amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287197338933247008768%2Curn%3Ali%3Aactivity%3A7196102038684876800%29">(so Steffie informs me!)</a></li>
<li>pyjamas</li>
<li>underwear</li>
</ul>

<h2>Feeling sporty?</h2>

<p>Codegarden offers a few sporting opportunities, if you're so inclined. 🏓🚲🧗🏃🏊</p>

<ul>
<li>sports-wear and -gear for <a href="https://codegarden.umbraco.com/codegarden-week/social-activities/">padel, cycling or bouldering, if you've signed up for Tuesday morning</a></li>
<li>running kit, if you want to join the CG Runners each morning - <a href="https://umbracocommunity.social/tags/cgRunners">#cgRunners</a></li>
<li>swimming kit, sauna towel and towel (there's a cohort of people swimming in the <em>free lido</em> each morning - keep an eye on the socials <a href="https://umbracocommunity.social/tags/cgSwimmers">#cgSwimmers</a>)</li>
</ul>

<h2>Electronics</h2>

<p>It's a tech conference after all!</p>

<ul>
<li>phone</li>
<li>local SIM if your roaming deal sucks - <a href="https://ref.airalo.com/yMiZ">$3 off an Airalo eSIM</a> with code <code>JOE9791</code>* or try <a href="https://www.firsty.app/">Firsty for free (but slow and ad-supported) data in Europe</a>.</li>
<li>power bank (planes generally restrict to &lt;100Wh and this <em>must</em> be in hand luggage)</li>
<li>charge cables</li>
<li>USB wall charger</li>
<li>laptop, for workshops, hackathons or inspired coding</li>
<li><strong>laptop charger</strong> (I'm looking at you, <a href="https://twitter.com/jasonelkin86/status/1668186388884344833">Jason</a>!)</li>
<li>EU adaptor, if your plug pins are an odd shape! (Denmark uses 230V, so you might need a voltage converter if your devices like voltages closer to 100V 🦅)</li>
<li>headphones (vital for the journey!)</li>
</ul>

<h2>Hygiene</h2>

<p>We like to hug and high-5!</p>

<ul>
<li>toothbrush</li>
<li>toothpaste</li>
<li>deodorant</li>
<li>other toiletries and grooming kit</li>
<li>medication (including <a href="https://umbracocommunity.social/@owaincodes/112554660007045292">antihistamines</a>, pain killers, etc.)</li>
<li>check what else you need if you're staying in an Airbnb! (Towel, shampoo, etc)</li>
</ul>

<h2>Travel</h2>

<ul>
<li>passport (check the validity!) or ID card (if you're lucky enough!)</li>
<li>visa (if required)</li>
<li>health insurance card, if your country has one (e.g. EHIC, GHIC) - <a href="https://tweets.joe.gl/1668266378804047873/">this still includes UK residents post-brexit</a></li>
<li>travel insurance documentation, if not travelling with work or you don't have a health insurance card</li>
<li>flight/train tickets (remember to check in!)</li>
<li>hotel reservation details</li>
<li>train reservation details (it can be cheaper and easier to prebook!)</li>
<li>fee-free payment card (<a href="https://revolut.com/referral/?referral-code=joejjcx!JUN1-23-AR">Revolut</a>*, <a href="https://join.monzo.com/c/ycrkzhf">Monzo</a>* and <a href="https://www.starlingbank.com/referral?code=1RXZAZ">Starling</a>* are popular options in the UK, register now to save money next time)</li>
<li><em>(Probably no)</em> Danish Kroner (optional, particularly with a fee-free card. Almost everywhere in Denmark accepts card - including hot dog carts! - and some places will refuse cash. <strong>I've never taken cash nor felt the need for it.</strong>)</li>
</ul>

<h2>Miscellaneous</h2>

<ul>
<li>backpack/bag - handy for lugging your stuff around the venues</li>
<li>water bottle - remember to empty before airport security, but you can usually fill it up again before the flight!</li>
<li>reusable coffee cup (free coffee, <a href="https://codegarden.umbraco.com/sustainability-at-codegarden/">but make it eco</a>!)</li>
<li>sun cream (you never know!)</li>
<li>sun glasses</li>
<li>wallet</li>
<li>notebook &amp; pen</li>
<li>snacks for the journey</li>
<li>entertainment for the journey - <strong>offline</strong> games, podcasts, movies, audiobooks and/or ebooks (or an analogue book if you're so inclined)</li>
</ul>

<h2>Presenting?</h2>

<ul>
<li>slides (offline) and a backup</li>
<li>display dongle (if your laptop doesn't have HDMI)</li>
<li>clicker</li>
<li>laser pointer</li>
</ul>

<p><small>*referral links mean we both get something nice!</small></p>

<h2>See you in Odense!</h2>

<p>So grab your laptops, charge up your brain cells, and get ready to embark on a wild journey where coding meets camaraderie, where networking knows no bounds, and where the exchange of knowledge is only rivaled by the exchange of witty tech puns. 🤓</p>

<p>I'll see you there!</p>

<h2>More tips</h2>

<p>There's now a <a href="https://discord.com/channels/869656431308189746/882984622978379786">whole Discord channel dedicated to Codegarden</a>, so ask any questions you have in there.</p>

<p>In 2024, Seb had a fantastic <a href="https://discord.com/channels/869656431308189746/882984622978379786/1239858566433869855">pre-Codegarden 2024 megathread on Discord</a> (<a href="https://discord-chats.umbraco.com/t/18809569/codegarden-2024-mega-tips-tricks-threa">also available without Discord login</a>) which is still relevant this year!</p>

<p>Print this page or use the checkboxes to mark off items as you pack! Checked items are saved locally in your browser.</p>

<p><style>.content li {
 list-style-type: "\2610";
 padding-left: 1ex;
}
 .content li.done {
  list-style-type: "\2611";
text-decoration: line-through;
opacity: 0.75;
 }
}</style></p>

<script>
const lis = document.querySelectorAll(".content li");
const lsKeyPrefix = "CgPackingList:";
lis.forEach(li => {
  var lsKey = `${lsKeyPrefix}${li.innerText}`;
  if (localStorage.getItem(lsKey) == 'true') {
    li.classList.add("done");
  }
  li.addEventListener(
    "click",
    (ev) => {
      if (ev.target.tagName != "A") {
        li.classList.toggle("done");
        if (li.classList.contains("done")) {
          localStorage.setItem(lsKey, true);
        } else {
          localStorage.removeItem(lsKey, true);
        }
      }
    },
    false,
  );
});
var reset = document.createElement("button");
reset.innerText = "Clear checked items";
reset.addEventListener("click", (ev) => {
  Object.keys(localStorage)
    .filter(x =>
      x.startsWith(lsKeyPrefix))
    .forEach(x =>
      localStorage.removeItem(x));
  document.querySelectorAll("li.done").forEach(li => li.classList.remove("done"));
});
document.querySelector(".content").appendChild(reset);
</script>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbraco Spark 2025: The Ultimate Guide!</title>
<link>https://joe.gl/ombek/blog/umbraco-spark-guide/</link>                <pubDate>Mon, 17 Feb 2025 10:51:16 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/umbraco-spark-guide/</guid>
                <enclosure url="https://joe.gl/media/jbboftwy/_26a3519c-579e-490a-b438-73d477ba4f3d.jpeg" length="238203" type="image/jpeg" />
                <description>Are you heading to Umbraco Spark this year but not sure what to expect? I live near Bristol and now consider myself a Spark veteran, so thought I&#39;d share my insights!</description>
                <content:encoded><![CDATA[
                    <p>Are you heading to Umbraco Spark this year but not sure what to expect? I live near Bristol and now consider myself a Spark veteran, so thought I'd share my insights!</p>
<h2>Getting hyped up for Spark</h2>

<p>Keep up to date with the goings on by <a href="https://umbracocommunity.social/@umbracospark">following Spark on Mastodon</a> or <a href="https://bsky.app/profile/umbracospark.bsky.social">BlueSky</a> as the <a href="https://umbracocommunity.social/@umbracospark/113992024110779635">details unfold</a> and they share crucial event information. Keep an eye on the #UmbracoSpark, #Umbraco and #UmbracoCommunity hashtags in the lead up to the event to keep in touch with the communuity.</p>

<h2>The fun before the party starts!</h2>

<p>Although the Spark conference is only one day, the fun starts the day before with a hackathon and pre-party.</p>

<h3><a href="https://www.meetup.com/umbristol/events/305647372/">Hackathon</a></h3>

<p>Umbraco and true digital are hosting a hackathon all day on Thursday 7 March!</p>

<p>This is a great opportunity to get stuck in with the Umbraco source code, with <a href="https://24days.in/umbraco-cms/2023/what-we-mean-by-umbraco/">Umbraco HQ crew</a> and community experts on site to help you out!</p>

<p>Alternatively, use the hackathon as a chance to get started on your <a href="https://gibe.digital/blog/behind-the-umbracospark-hackathon/">PackageJam</a> submission!</p>

<h4>What is an Umbraco Hackathon?</h4>

<p>Unlike some other hackathons you may have attended, Umbraco hackathons don't have a strict theme or overall goal. They're an excuse to get together and code together. People work on Umbraco source code bugs, Umbraco packages and community projects. You don't need to have an idea before you attend - but be sure to ask for inspiration when you get there.</p>

<p>The hackathon will include refreshments and lunch (usually pizza or burritos).</p>

<h4>Convince your boss!</h4>

<p>How do you convince your boss to let you take an extra day for Spark? Simple! Hackathons allow you to get your head into the source code and understand how the product works better. You might even be able to fix some bugs that have been niggling your business for a while! You also get the assistance of Umbraco employees and community experts - who better to help you understand the Umbraco ecosystem!? Plus the usual networking - it's a very sociable way to code!</p>

<p>You don't have to be there for the whole thing either - just turn up for the afternoon if that's all you can muster!</p>

<p><a href="https://www.meetup.com/umbristol/events/305647372/">More information and sign up on Meetup. <strong>This is a separate event and requires signing up (free!) on Meetup to attend.</strong></a> Once signed up, keep an eye on your inbox in the weeks leading up to the event for more details.</p>

<h3><a href="https://www.meetup.com/umbristol/events/306101777/">Pre-party!</a></h3>

<p>Every year <a href="https://www.umbristol.co.uk/">umBristol, the Bristol Umbraco Meetup</a>, host a pre-party to make friends (it's like "networking" but more fun!) and have fun. This year we're all meeting just 5 minutes from the hackathon venue at Roxy Lanes to play some duck-pin bowling, ice-free curling and other games. A few drinks and/or snacks are sometimes thrown in by the sponsor, and there's an opportunity to buy food and drink.</p>

<p><a href="https://www.meetup.com/umbristol/events/306101777/">More information and sign up on Meetup. <strong>This is a separate event and requires signing up (free!) on Meetup to attend.</strong></a></p>

<p><img src="/media/jvhcsi4y/spark-preparty.jpg" alt="A split-screen cartoon. One side is of a unicorn in a neon-lit room with arcade games while sat on a curling stone, brush in hand. The other side is a unicorn in a bowling shirt holding a pink bowling ball, in a neon-lit bowling alley. Generated by AI." /></p>

<h2>The big day - what to expect</h2>

<h3>Morning run (optional!)</h3>

<p>Spark are hosting a morning 5km run. It should be "easy-paced" (but Frederik always claims his pre-Codegarden runs will be easy too - maybe I'm just too slow!) You'll need to <a href="https://www.meetup.com/umbristol/events/306006480/">register your interest in joining the run</a> and meet outside The Bristol Hotel
Prince Street, BS1 4QF.</p>

<h3>Umbraco Spark!</h3>

<p><img src="/media/h13f535n/2l1a7816-editjpg_53585355651_o.jpg" alt="Joe speaking at Spark 2024 wearing a unicorn onesie and stood behind a poduim with the Umbraco Spark logo on it." /></p>

<p>Spark is hosted at the curiosity-inducing <a href="https://www.wethecurious.org">We The Curious</a> in Millennium Square. This is an exciting new venue for Spark, but once at We The Curious expect the usual Spark signage pointing you to the venue and you can expect to be greeted by some friendly Bristolians to get your name badge and merch. Coffee and tea are usually available and it's time to mingle! The Umbraco community are very friendly (it's kinda their thing!), so don't be afraid to chat to people you've never met before (even if they're a community celebrity!) I'll be there awkwardly mingling too, so come and chat to me - "I read your blog post" is always a great conversation opener!</p>

<p>Spark offers one track this year which, looking at <a href="https://umbracospark.com#what">the lineup</a>, promises to be interesting. The other option at Spark this year is to enter the <a href="https://gibe.digital/blog/behind-the-umbracospark-hackathon/">PackageJam</a> which will run alongside the talks. If you miss some talks, why not ask the speaker to repeat at your local meetup?</p>

<p>Lunch and snacks are provided at various points throughout the day, as well as a cheeky drink at the end before we move on to...</p>

<h3>The after-party</h3>

<p>No need to book this one, just turn up to <a href="https://thelounges.co.uk/ritorno/">Ritorno Lounge</a> next to the harbour with your lanyard and follow signs upstairs to <strong>The Caraboo</strong> (it's privately hired). It's always nice to join folks for a drink and food before heading home - so try to plan around this if you can! There will be food and drink to buy at the after-party venue (this one's not included).</p>

<p>It's usually a casual affair where we sit and chat with friends new and old late into the night!</p>

<h2>What do I need to bring/wear?</h2>

<p>Spark is a casual conference, so a hoodie and jeans will suit for all events. Umbraco merch is always a bonus if you want to fit in! Also, remember a raincoat - we're only a stones throw from Wales after all!</p>

<p>You're also unlikely to need a laptop during the conference, but you might want to bring a notebook or take photos. If you elected to receive merch when buying your ticket, you'll get a branded notebook and pen on arrival as well as a t-shirt.</p>

<p>Don't forget your  laptop and charger if you're also attending the hackathon!</p>

<h2>Map</h2>

<p>A map of the primary locations is included below. It also includes my recommendations for Bristol - read on for details!</p>

<iframe src="https://www.google.com/maps/d/u/0/embed?mid=1XcOzjW7wX6kg2l3Iwpbqodh-t_kS8D8&ehbc=2E312F" style="width:100%" height="480"></iframe>

<p><a href="https://www.google.com/maps/d/u/0/edit?mid=1XcOzjW7wX6kg2l3Iwpbqodh-t_kS8D8&amp;usp=sharing">Open map full screen.</a></p>

<h2>The Umbracian's guide to Bristol</h2>

<p><img src="/media/bbxpi3dl/img_3014-1.jpg" alt="A photo of a light up sign reading &quot;Bristol is always a good idea&quot; in large pink lettering." /></p>

<p>Bristol is a lovely city - so lovely, in fact, I moved here in 2022 - so many people elect to stay in the area for the weekend afterwards. Here's some advice for those people:</p>

<h3>Getting around</h3>

<p>Although the city is quite sprawling, a lot of Bristol is accessible by foot (particularly if you're good with hills!)</p>

<p>As much as the locals complain, Bristol has a pretty good bus network (just don't compare it to London or any continental European city!) Fares are £2.40 for a single with a cap at £6.50 for the day, if you stay within Bristol. Busses all support tap-on-tap-off which are automatically capped, so no cash is needed.</p>

<p>For <a href="https://ridedott.com">bike and electric scooter hire you'll need the Dott app</a> (this is a different app to last year!) which you'll need your driving license (full or provisional) to sign up for, so be sure to do that ahead of time.</p>

<h3>Food and drink</h3>

<p>Bristol is home to fantastic independent cafes, pubs, restaurants and food stalls - as well as the birthplace of small chains like The Lounges and Boston Tea Party.</p>

<p>My favourite food stalls are in <a href="https://www.wappingwharf.co.uk/">Wapping Wharf (Spike Island, harbourside)</a> (St Nicholas Market is closed at the weekend!)</p>

<p>You can't go wrong with most cafes in Bristol, but some of my regulars are <a href="https://www.mud-dock.co.uk/cafe/">Mud Dock (harbourside)</a>, <a href="https://thebristolloaf.co.uk/beacon/">The Bristol Loaf ("The Centre")</a>, <a href="https://www.eastvillagecafe.co.uk/">East Village Cafe (Clifton Village)</a> and Santiago's (particularly if you're waiting for a bus at the bus station!)</p>

<p>Some of my favourite pubs include <a href="https://applecider.co.uk">The Apple (central)</a> (don't miss having a half - too strong to sell it in pints! - of Old Bristolian cider on this cider-barge), <a href="https://strawberrythiefbar.com/">The Strawberry Thief (central)</a>, <a href="https://butcombe.com/the-cottage-inn-bristol/">The Cottage Inn (Spike Island, harbourside)</a> and pretty much anywhere on King Street (central).</p>

<h3>A touch of nature</h3>

<p>Leigh Woods is a fantastic woodland a short walk over Clifton Suspension Bridge. You'll want some boots but it's easy to follow signposted routes.</p>

<p>Ashton Court is a deer park with great views over Bristol and a lovely cafe. You can stick to tarmac and gravel paths here if you want clean shoes! Also a Parkrun location.</p>

<p>Brandon Hill is a lovely park right next to Park Street and is home of Cabot Tower - which is worth a climb (free) for some more fantastic views.</p>

<h3>Shopping</h3>

<p>Cabot Circus and Bristol Shopping Quarter are mostly (although not entirely!) big chain shops you'd find in most cities.</p>

<p>Clifton Village (further out than Clifton but within the city, unlike it's name implies!) is where you'll find the best of Bristol's local boutique-y gift shops and cafes. Also worth visiting the iconic suspension bridge while you're there.</p>

<p>A little closer to the centre of town is Park Street and Whiteladies Road as well as Gloucester Road, less classy than Clifton but some lovely smaller shops. (Gloucester Road is the charity-shopper's heaven!)</p>

<h3>Sightseeing</h3>

<p>Bristol is home to some great museums. M Shed is the historic venue for Spark but also a great little free museum. You can find <a href="https://www.bristolmuseums.org.uk/">more information about all the Bristol Museums on their website</a>.</p>

<p>This year's venue is also a museum - <a href="https://www.wethecurious.org">We The Curious</a> is an interactive science museum that's recently reopened after a large fire. I've not been since the reopening, but have fond memories of going as a child!</p>

<p>Next to We The Curious is the aquarium (central) which is expensive but very good. Bristol Zoo is no longer located in Clifton, and is now outside the city.</p>

<p>Cabot Tower in Brandon Hill park (central) is free to climb for some epic views.</p>

<p>Clifton Suspension Bridge (Clifton Village) is free to walk across and Observatory Hill is nearby for some good views and a nice coffee shop. Or head into Clifton Village for endless cafes and pubs.</p>

<p>Bristol University has several beautiful historic buildings. The closest to the city centre are at the top of Park Street along with the Bristol Art Gallery.</p>

<p>Personally, I like to explore new cities using virtual audio tours. <a href="https://voicemap.me/share/xzplzs">Claim a free one using my VoiceMap referral link.</a>  There are 5 highly-rated tours of Bristol to choose from.</p>

<p><img src="/media/ahclxydl/_5015bce3-afd6-46c0-94ad-0f1aa160e3cd.jpg" alt="A cartoon unicorn sat in a harbour, surrounded by boats. The Clifton Suspension Bridge is behind it and hot air balloons float in the sky above." /></p>

<h3>Nearby</h3>

<p>The cities of Bath and Cardiff, Wales are within an easy train ride (15 minutes or 1 hour respectively) if you want to spread your wings a little further.</p>

<h2>See you there!</h2>

<p>I'll be at the hackathon right the way through to the after party - so I'll see you there! Please come and say hi, and see my talk too if it appeals!</p>

<p>If you think I've missed something vital, <a href="/ombek/contact/">let me know on socials!</a></p>

<p><img src="/media/cvdp05yj/spark-unicorn.png" alt="A more realistic unicorn stood on a hill with Bristol and the suspension bridge behind it at sunset. AI generated." /></p>

<p><small>
<em>Thanks to <a href="https://www.linkedin.com/in/tristanjthompson/">Tristan Thompson</a> for his contributions to the 2024 post which have been brought forwards.</em>
</small></p>                ]]></content:encoded>
            </item>
            <item>
                <title>How to Copy &amp; Paste: Effectively working with strangers (and robots) on the internet</title>
<link>https://24days.in/umbraco-cms/2024/copy-paste/</link>                <pubDate>Sat, 07 Dec 2024 08:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://24days.in/umbraco-cms/2024/copy-paste/</guid>
                <enclosure url="https://joe.gl/media/4xyf5lrd/52993360181_f7bded0189_k.jpg" length="529040" type="image/jpg" />
                <description>Since the dawn of CodeProject and Stack Overflow, we&#39;ve been copying and pasting other developers&#39; code and now with the advent of ChatGPT, Copilot and other AI solutions, it&#39;s easier than ever to use code somebody (or something) else wrote... without necessarily understanding the code completely.</description>
                <content:encoded><![CDATA[
                    <p>Since the dawn of CodeProject and Stack Overflow, we've been copying and pasting other developers' code and now with the advent of ChatGPT, Copilot and other AI solutions, it's easier than ever to use code somebody (or something) else wrote... without necessarily understanding the code completely.</p>
<p>This content is also available with examples as <a href="/ombek/talks/wwc24-how-to-copy-and-paste/">a talk recording from WeAreDevelopers 2024</a>.</p>

<hr />

<h2>Hello there, professional googlers!</h2>

<p>We've all heard that software developers are just "professional googlers" and the more senior you are
is mostly a product of how good you are at
Googling things... and it's a great joke... but have
you ever tried being a developer <em>without</em> internet
access? It doesn't feel like much of a joke then,
does it?</p>

<p>But I don't see that as a negative,
as such. When we're isolated we may feel weak
and hopeless but as a team, as a community,
we're quick and efficient problem solvers.</p>

<p>And that's where the idea for this article came
from - how can we work together more effectively
as a worldwide developer community without getting
stung along the way?</p>

<h2 id="google-it">How can I ask for help?</h2>

<p>Before we start with how and what to copy and paste, let's first start with some places we might find answers.</p>

<p>Obviously, please google* your query before you start
asking people!</p>

<p><small>* I'm using the term "google" as a search-engine-agnostic term. Personally, I <a href="https://www.theverge.com/2012/9/13/3324696/microsoft-bing-google-verb-search">google it on Bing</a> these days.</small></p>

<p>Sources that you might find useful include the official
documentation; YouTube demos, tutorials,
talk recordings (there's even <a href="https://joe.gl/ombek/talks/wwc24-how-to-copy-and-paste/">a talk I did covering this very topic on YouTube</a>
if you're not a fan of reading!); people's blog posts (hello 24 Days in Umbraco!); or even
looking at the source code.</p>

<p>Particularly if you're looking for help around open source projects like Umbraco, existing
answers to other people's questions are very prevelant. So any destination
we're going to talk about with regards to getting help, there might already be solutions there for you
without you even needing to ask the question.</p>

<p><img src="/media/h3hoipd3/joey.png" alt="Phoebe and Joey meme from where she&#39;s trying to teach him french. The panels read Phoebe: &quot;Read&quot;, Joey: &quot;Read&quot;, P: &quot;the&quot;, J: &quot;the&quot;, P: &quot;documentation&quot;, J: &quot;documentation&quot;, P: &quot;Read then documentation&quot;, J: &quot;Copy StackOverflow answer!&quot;" /></p>

<p><em>Image source: @VishalMalvi_ on X (probably, memes are really hard to attribute!)</em></p>

<h2 id="where-to-ask"><em>Where</em> to ask for help</h2>

<p>Now we've established we can't
find the answer already on the internet, so we need to ask for help but where do
go to do that?</p>

<p>StackOverflow is good for some more
generic development questions.</p>

<p>Some repositories on GitHub have discussions or some developers
might like you to raise issues if you're confused
about a situation.</p>

<p>Are there any specialised communities that you're part of? (I'm looking at you, Umbraco!)
Specialized communities are smaller communities who
are experts in the thing you're trying to do.
There are forums, Discords, Slacks for these groups of developers.</p>

<p>Umbraco has a thriving community with plenty of places to ask questions,
and we should take advantage of that!</p>

<p>Social media is a another fantastic place to find
people to help you - Mastodon, X, Bluesky,
Reddit, whatever your social media of choice is and wherever a developer community exists.</p>

<h2 id="how-to-ask"><em>How</em> to ask for help</h2>

<p>Now we have these places to ask for help, but how exactly do we do it? It feels like an odd question but perhaps more specifically, how can I ask a question in a way that's going to get an answer that's most appropriate and most helpful to me?
Because although the community is generally very helpful,
it's to our own benefit to ask the right questions in the first place.</p>

<p>I've developed a 6 item checklist for a good question (and we know this is good advice, by the way,
because it's inspired by <a href="https://stackoverflow.com/help/how-to-ask">StackOverflow's "How to Ask"</a> and if anyone is fussy about how you ask a question it's StackOverflow!)</p>

<ol>
<li><h4>Do your research first</h4>

<p>Although we've already searched for answers to the question it's also important that we do some background research so that we can explain ourselves correctly and be very specific about our problem. There's also very little point asking a question if we don't know what we're asking - if we don't understand what we're asking then we need to ask a different question.</p></li>
<li><h4>Comprehend the problem</h4>

<p>I'm using the word "comprehending" here to mean "understand". But I'm using the word "comprehend" because it emphasizes the fact we don't need to know everything - we don't need to "understand" everything, just "comprehend" what we want to understand.</p>

<p>Try <a href="https://rubberduckdebugging.com/">rubber duck debugging</a> as a helpful tool to help comprehend your problem.</p></li>
<li><h4>Be polite and patient</h4>

<p>...because nobody <em>has</em> to help you. Any prospective answerer is just on the internet looking to reach out and give you a bit of a hand. They don't have to do it. So being polite and patient goes a long way to getting the best answer to your questions.</p></li>
<li><h4>Be on topic and be suitable</h4>

<p>This includes any tags and categories you apply to your topic. So if you're posting on a forum make sure you're posting it in the right category and use the right tags.
If you're using social media, it's really, really important to ask- if you're going to be asking the right people the right questions you need to make sure you're asking these questions in the right places in places people are expecting questions on the subject you're asking questions about.</p></li>
<li><h4>Summarize your problem</h4></li>
<li><h4>Include a reproducible example</h4>

<p>I’ve grouped these two together here. It's really important to get across what you're trying to say in a concise manner but with an example. It also can really help to simplify your example too, removing any code and context that's irrelevant to your question - you don't need to give everybody all the information about the project you're working on but if you can find an example that is a little bit simplified, it can make everyone's life a lot easier. (Actually, simplifying your code can help you narrow down the problem yourself!)</p></li>
</ol>

<p><img src="/media/h2loxruj/rubber-ducks.jpg" alt="Rubber ducks sat at desks with computers in an office setting. AI Generated." /></p>

<p><em>Rubber duck debugging can be a useful tool to help comprehend a problem. Created with DALL-E.</em> </p>

<h2 id="ai">What about AI?</h2>

<p>So what about AI? I've been talking a lot about community and collaboration so why am I bringing up artificial intelligence? (It's not just so I can apply the trendy "AI" tag to the article, honest!) LLMs (Large Language Models) have been scouring the internet looking at <em>our</em> problems and <em>our</em> code samples and, as I see it, they’re tools effectively built by the community and it takes a lot of the same skills to effectively use an AI-generated answer as it does to pick the right StackOverflow answer.</p>

<p>LLMs are the AI tools we’ve seen popping up all over the place in the past few years, be that <a href="https://chatgpt.com/">OpenAI ChatGPT</a>, <a href="https://copilot.microsoft.com/">Microsoft CoPilot</a>, <a href="https://github.com/features/copilot">Github Copilot</a> or <a href="https://gemini.google.com/">Google Gemini</a> - they’re all LLMs and largely work in the same sort of way and we have to provide the model with the same context (and treat their responses with the same level of scrutiny as) we would if we were asking a stranger on the Internet.</p>

<h3>What can I ask AI to do?</h3>

<p>AI is really good as <em>a starting point</em> - and this is quite key for most AI generated responses - for various programming tasks.</p>

<p>Take writing regular expressions for example. Say we ask it to "give me a regular expression for UK phone numbers", it will give you a starting point for that. But it will also give you a starting point for sort of more generic coding questions as well.</p>

<p>AI is also good as an autocomplete. One of the big use cases I have for GitHub Copilot is as I'm typing some very repetitive code Copilot will quite often suggest doing the same thing I've been doing to the code above to the code below. It's very good at picking up repeating patterns or spitting out very common structures.</p>

<p>You can also use AI, though, to explain code samples and error messages or to convert from one format to another. Perhaps "convert the following XML file into JSON" isn’t the best example, there are numerous XML to JSON converters online, but if you're trying to convert between two very niche formats or between programming languages where there wouldn't necessarily make sense to be a direct converter, AI is pretty good at that sort of thing.</p>

<p>Another option is to use LLMs to be your rubber duck.</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/PmWVlbKNvxo?si=rIa84g8vxmjY9QJC" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<p><img src="/media/riqfvqxv/5695cd31-12b3-4b4e-85eb-a9bc3922183f.jpeg" alt="A rubber duck sat at a laptop &quot;rubber ducking&quot; with a rubber human. AI Generated." /></p>

<p><em>As we saw earlier, not only is AI good at helping with code, AI tooling is also pretty good at generating images of rubber ducks in an office setting. But, it turns out, it's also pretty good at generating images of rubber ducks "rubber ducking" with a desktop rubber human.  Created with DALL-E.</em></p>

<h3>When might I want to ask elsewhere?</h3>

<p>AI isn’t always useful, though.</p>

<p>LLMs can have a poor understanding of performance.</p>

<p>It's pretty bad at understanding versions too, particularly in the .NET world (Microsoft's not always the best at naming things!) When .NET Framework was completely rewritten as .NET Core and then renamed .NET, AI is not particularly good at understanding the difference between those two (three?) versions. In the front-end world too distinguishing between Angular and AngularJS isn’t always clear. AI is not very good at picking out on these specific weird naming conventions and giving you answers that make sense for the language you're asking them in.</p>

<p>It’ll often invert boolean logic (which is a problem for me because my human brain <em>also</em> likes to invert boolean logic!) so asking AI for a string of ands, ors and boolean operations is not always the best option because it will often get those completely the wrong way around (or should that be “way wrong around”?).</p>

<p>You can struggle to persuade LLMs to make modifications to previous code samples. I’ve often asked for tools to "write me a regular expression for international phone numbers", which it does, but often omitting one thing or another. Following that up with a query like “please also allow the hyphen character” (as our friends across the Pond are fond of them) has previously resulted in the model returning a US-specific expression, ignoring the “international” requirement. It’s always worth double checking both the initial response as well as any later modifications.</p>

<p>And, boy, does it make things up! In my experience, AI will rarely tell you that you <em>can't</em> do something. It will generally make up a way that you can even if those ways are not possible, using API methods that don't exist, mythical namespaces and libraries that it’s invented - it will never say no.</p>

<p>It will never ask questions or give options like a human would. If a human doesn't fully understand the question it will say "oh sorry, I'm not sure I quite follow - can you provide me this context?" AI doesn't do that, it will give you an answer to what it "thinks" the question is which can lead you down the wrong rabbit holes!</p>

<h2 id="assessing-suitability">Assessing the suitability of an answer</h2>

<p>All right, so we've asked questions to humans, we've asked questions to AI now we need to know how to copy and paste.</p>

<p>We've established AI can be wrong because AI learns from people and people are wrong too so <em>whoever</em> you've asked it's very important that we assess the suitability of an answer.</p>

<p>It’s important to reiterate how important it is to comprehend the problem we’re experiencing. We can’t assess the suitability of an answer without this initial comprehension - we can’t grade an answer without knowing the criteria we’re scoring against.</p>

<p>Once we’ve comprehended the problem, it’s time to comprehend the solution. As before, we don't need to understand every intricacy of the code, we don't need to understand line by line exactly what's happening but we should know roughly what each line of code is doing and why it's there - we should have an idea of how it works.</p>

<p>This brings us to another checklist (I really like checklists, don’t I?!), my solution suitability checklist!</p>

<ul>
<li><p>Does it <strong>answer my question</strong>?</p>

<p>Does the answer that's been given make sense as a response to the question that I asked? It could have been misread or misunderstood. And if I’m reading a solution to someone else’s question, is my question the same one as is being answered? A keyword search might have brought up questions that are very subtly different, which means they could have a less subtly different solution.</p></li>
<li><p><strong>Line by line</strong>, what is this doing?</p>

<p>If it's a code solution what does each line do? You don't have to understand every intricacy of the code but we should have an idea line by line what it's doing.</p></li>
<li><p>Does the <strong>code match the description</strong>?</p>

<p>If someone's written a description of what a code sample does (LLMs usually do this too) does that match up with what the code actually does? If
it doesn't, then we've potentially got a problem!</p></li>
<li><p>Does that <strong>feel right</strong>?</p>

<p>This feels like a bit of a “fluffy” checklist item, but as a developer I often rely on my “gut feel” to hunt down “code smell”! We should ask how can we apply our existing knowledge to the answer to see if it makes sense.</p></li>
<li><p>How can we <strong>increase our confidence</strong> in this answer?</p>

<p>If we don’t have enough background knowledge to instantly know the solution is correct, what else could we ask or what else could we look up to
help us reassure us that this is the way forwards? If the solution is a regular expression, for example, perhaps we could plumb it into a regular expression explainer tool to ensure it matches our expectations. We could even <a href="https://www.bbc.co.uk/bitesize/guides/zg4j7ty/revision/3"><strong>dry run</strong></a> the code as a sanity check.</p></li>
</ul>

<p><img src="/media/xmmpmdeb/desert-run.jpg" alt="Four young adults running away from the camera through the desert, towards cacti and large rocks" /></p>

<p><em>(Please appreciate the visual pun here. They're running in the desert, therefore, they're dry running!) Photo by Annie Spratt on Unsplash.</em></p>

<h2 id="adapt">Adapting code to meet our needs</h2>

<p>The next thing we might need to do is adapt some code to meet our needs. Perhaps we simplified our question and now we need to adapt the answer to fit back into our more complex solution. This is only possible when we’ve understood the solution, but now that we have a better understanding we can tweak it to what we're expecting it to be.</p>

<p>And if we’ve established the answer we’ve found doesn’t <em>quite</em> meet our suitability criteria, through our further research we may be able to tweak an answer that’s close to correct to be the ideal solution.</p>

<h2>How to Copy &amp; Paste</h2>

<p>(Hey look! That's the title of the article!)</p>

<p>If you thought two checklists were enough for one article, you were wrong! I've condensed the whole article into five-point checklist for how to copy and paste:</p>

<ul>
<li><a href="#google-it">Google it!</a></li>
<li>Ask the <strong>best</strong> people (<a href="#ai">or robots</a>) in the <a href="#where-to-ask"><strong>best</strong> place</a> in the <a href="#how-to-ask"><strong>best</strong> way</a></li>
<li><a href="#how-to-ask">Know <strong>what</strong> and <strong>why</strong> you're asking</a></li>
<li><a href="#assessing-suitability"><strong>Comprehend</strong> and <strong>validate</strong> the answer</a></li>
<li><a href="#adapt"><strong>Adapt</strong> to meet your needs</a></li>
</ul>

<p>With these tips, I hope we can level-up our Ctrl+V game and become better developers and a together stronger community.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>You probably don&#39;t need a custom index - Modifying the ExternalIndex in Examine and Umbraco</title>
<link>https://joe.gl/ombek/blog/you-probably-dont-need-a-custom-index/</link>                <pubDate>Tue, 12 Nov 2024 07:08:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/you-probably-dont-need-a-custom-index/</guid>
                <enclosure url="https://joe.gl/media/g0clqrks/_f66723bd-9130-4ccd-8738-2c3f90c86f16.jpeg" length="300497" type="image/jpeg" />
                <description>I often see developers creating whole new Examine indexes for their blog or shop search, but this isn&#39;t necessarily needed, especially when paired with a Path query (with the help of the Search Extensions package)</description>
                <content:encoded><![CDATA[
                    <p>I often see developers creating whole new Examine indexes for their blog or shop search, but this isn't necessarily needed, especially when paired with a Path query (with the help of the <a href="https://marketplace.umbraco.com/package/our.umbraco.extensions.search">Search Extensions package</a>)</p>
<p>Additional indexes means more storage and more places for index corruption, so I prefer to keep my indexes to a minimum. Umbraco comes with an ExternalIndex out the box - this is designed to be used for end-user site search, and doesn't contain any unpublished nodes (unlike the InternalIndex which is intended to be used by the backoffice).</p>

<h2>Defining fields in an existing index</h2>

<p>To add our own fields to an index or to change the type of a field, we need to create an <code>IConfigureNamedOptions&lt;LuceneDirectoryIndexOptions&gt;</code>. This class will need registering in a composer (see below).
The important lines are:</p>

<pre><code>options.FieldDefinitions.AddOrUpdate(new FieldDefinition("searchCategories", FieldDefinitionTypes.Raw));
options.FieldDefinitions.AddOrUpdate(new FieldDefinition("searchDate", FieldDefinitionTypes.DateMonth));
</code></pre>

<p>The definition name is a magic string we'll use to reference the field when setting the values or searching. It can be anything so long as it is unique (avoid calling it the same thing as one of your properties!) and is conventionally camelCased.</p>

<p>Select the most relevant field definition type from the <a href="https://shazwazza.github.io/Examine/articles/configuration.html#value-types">Examine Value Types documentation</a>. In this example, I've used Raw (for exact matches) and DateMonth (for storing dates as Ticks to the precision of the month).</p>

<p>My full class looks like this:</p>

<pre><code>public class ConfigureExternalIndexOptions : IConfigureNamedOptions&lt;LuceneDirectoryIndexOptions&gt;
{
    private readonly IOptions&lt;IndexCreatorSettings&gt; _settings;

    public ConfigureExternalIndexOptions(IOptions&lt;IndexCreatorSettings&gt; settings)
        =&gt; _settings = settings;

    public void Configure(string? name, LuceneDirectoryIndexOptions options)
    {
        if (name?.Equals(Constants.UmbracoIndexes.ExternalIndexName) is false)
        {
            return;
        }

        options.FieldDefinitions.AddOrUpdate(new FieldDefinition("searchCategories", FieldDefinitionTypes.Raw));
        options.FieldDefinitions.AddOrUpdate(new FieldDefinition("searchDate", FieldDefinitionTypes.DateMonth));
    }

    // not used
    public void Configure(LuceneDirectoryIndexOptions options) =&gt; throw new NotImplementedException();
}
</code></pre>

<h2>Adding or updating values in an existing index</h2>

<p>If you only want to modify the value of a field or have created the new field, as above, we can start populating the fields in the index.</p>

<p>To do this, we have to <a href="https://our.umbraco.com/forum/using-umbraco-and-getting-started/109345-examine-transforming-index-values-in-v10#comment-339214">copy the existing values to a dictionary</a>, add or modify the values and save them back using the <code>SetValues</code> method.</p>

<p>Each field can have multiple values, as shown in the case of <code>searchCategories</code>, but this is often an array of one item, like with <code>searchDate</code>.</p>

<p>I've also commented out an example of modifying a value which is the example given in my blog post on <a href="/ombek/blog/tag-style-exact-matching-with-examine/">Tag-style exact-matching with Examine</a>. In this case, I could have done the same thing with categories, but have chosen to index the sanitized values as a new field in this example.</p>

<pre><code>public class ExternalIndexValueTransformationComponent : IComponent
{
    private readonly IExamineManager _examineManager;
    private readonly IShortStringHelper _shortStringHelper;

    public ExternalIndexValueTransformationComponent(IExamineManager examineManager, IShortStringHelper shortStringHelper)
    {
        _examineManager = examineManager;
        _shortStringHelper = shortStringHelper;
    }
    public void Initialize()
    {
        if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.ExternalIndexName,
                out var index))
        {
            return;
        }

        if (!(index is BaseIndexProvider indexProvider))
        {
            return;
        }

        indexProvider.TransformingIndexValues += ExternalIndex_TransformingIndexValues;
    }


    private void ExternalIndex_TransformingIndexValues(object? sender, IndexingItemEventArgs e)
    {
        var values = e.ValueSet.Values.ToDictionary(x =&gt; x.Key, x =&gt; (IEnumerable&lt;object&gt;)x.Value);

        // Insert new values

        if (e.ValueSet.Values.ContainsKey("displayDate") &amp;&amp; DateTime.TryParse(e.ValueSet.GetValue("displayDate").ToString(), out DateTime date))
        {
            var searchDate = date.ToString("MM-yyyy");
            values.Add("searchDate", new [] { searchDate });
        }

        if (e.ValueSet.Values.ContainsKey("categories"))
        {
            var categories = e.ValueSet.GetValues("categories")
                .Select(x =&gt; x.ToString()?.ToCleanString(_shortStringHelper, CleanStringType.UrlSegment))
                .WhereNotNull().ToArray();
            if (categories?.Any() ?? false)
            {
                values.Add("searchCategories", categories);
            }
        }

        // Modify existing values
        //foreach (var value in e.ValueSet.Values)
        //{
        //    if (value.Key == "myKey")
        //    {
        //        var values = value.Value.FirstOrDefault().ToString().Split(Environment.NewLine);
        //        updatedValues["myKey"] = values.Cast&lt;object&gt;().ToList();
        //    }
        //}

        e.SetValues(values.ToDictionary(x =&gt; x.Key, x =&gt; x.Value));

    }

    public void Terminate()
    {
        if (_examineManager.TryGetIndex(UmbracoIndexes.ExternalIndexName, out IIndex index)
            &amp;&amp; index is BaseIndexProvider externalIndex)
        {
            externalIndex.TransformingIndexValues -= ExternalIndex_TransformingIndexValues;
        }
    }
}
</code></pre>

<h2>Registering the configuraiton</h2>

<p>Both of these need adding to a composer's compose method</p>

<pre><code>public class Composer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.ConfigureOptions&lt;ConfigureExternalIndexOptions&gt;();
        builder.Components().Append&lt;ExternalIndexValueTransformationComponent&gt;();
    }
}
</code></pre>

<h2>Rebuild the indexes</h2>

<p>You can debug the methods above either by saving a content node or by rebuilding the index with a breakpoint in the above code. You'll also need to rebuild the index to get the new/modified values for all content.</p>

<p>To rebuild the index, navigate to Settings > "Examine Management" tab > ExternalIndex > "Rebuild index" button.</p>

<h2>Searching with the modified index</h2>

<p>Here's an example of a blog search functionality. This could live in a controller or service. This makes use of the <a href="https://marketplace.umbraco.com/package/our.umbraco.extensions.search">Search Extensions package</a>, which I generally recommend, for some of the extension methods and <code>path</code> format.</p>

<pre><code>if (_examineManager.TryGetIndex(UmbracoIndexes.ExternalIndexName, out IIndex index) == false)
{
    throw new Exception($"Failed to find {UmbracoIndexes.ExternalIndexName}");
}

var type = typeof(BlogPost).Name.ToLower();

var query = index.Searcher.CreateQuery("content").NodeTypeAlias(type);

query.And().Field("path", blogRoot.Id.ToString());

if (!string.IsNullOrEmpty(category))
{
    query.And().Field("searchCategories", category);
}

if (!string.IsNullOrEmpty(month))
{
    if (DateTime.TryParseExact(month, "MM-yyyy", null, DateTimeStyles.None, out var date))
    {
        query.And().Field("searchDate", date);
    }
}

if (sortAscending == true)
{
    query.OrderBy(new SortableField("searchDate", SortType.Long));
}
else
{
    query.OrderByDescending(new SortableField("searchDate", SortType.Long));
}

var searchResults = query.Execute();

return searchResults.GetResults&lt;BlogPost&gt;();
</code></pre>

<h2>Further Reading</h2>

<ul>
<li><a href="/ombek/blog/tag-style-exact-matching-with-examine/">Tag-style exact matching</a>
<!-- - <a href="https://24days.in/umbraco-cms/2024/multi-node-tree-picker-search/">How to Search by Picked Multi-Node Tree Picker Values</a>--></li>
</ul>                ]]></content:encoded>
            </item>
            <item>
                <title>CODECABIN Packing List</title>
<link>https://joe.gl/ombek/blog/codecabin-packing-list/</link>                <pubDate>Thu, 19 Sep 2024 12:32:31 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/codecabin-packing-list/</guid>
                <enclosure url="https://joe.gl/media/jjthl32g/supply-xcgsvyolcwe-unsplash.jpg" length="2828491" type="image/jpg" />
                <description>At the request of Jesper, I&#39;ve put together a packing list for CODECABIN.</description>
                <content:encoded><![CDATA[
                    <p>At the request of <a href="https://dev.to/jemayn">Jesper</a>, I've put together a packing list for CODECABIN.</p>
<h2>Clothes</h2>

<p>CODECABIN is a very casual affair - wear whatever you'll be comfortable in. The weather in the Peak District in September is <em>varied</em>. Check the weather forecast because it could be boiling hot and sunny, thunderstorms, or anything in between (perhaps even both).</p>

<ul>
<li>hoodie/jacket/sweater, in case the venue is a little chilly or you're out late</li>
<li>shorts, if it's likely to be hot enough!</li>
<li>tops and t-shirts (Umbraco swag, if you have them!)</li>
<li>jeans/casual trousers/joggers</li>
<li>dresses/skirts, but keep it casual</li>
<li>comfortable shoes/trainers</li>
<li>flip-flops or slippers</li>
<li>underwear</li>
</ul>

<h2>Touching grass</h2>

<p>The location of CODECABIN is beautiful. If you want to make the most of the area, people often go running and there's usually a walk.</p>

<ul>
<li>waterproof jacket, if you want to go outside whatever the weather</li>
<li>sun hat, if you want to go outside whatever the weather</li>
<li>running kit, if you want to join the runners</li>
<li>shoes suitable for walking, if you want to join the walk</li>
<li>National Trust membership card (even if you don't plan to walk, others can use it for free parking!)</li>
</ul>

<h2>Electronics</h2>

<p>It's a tech un-conference after all!</p>

<ul>
<li>phone</li>
<li>local SIM if your roaming deal sucks - <a href="https://ref.airalo.com/yMiZ">$3 off an Airalo eSIM</a> with code <code>JOE9791</code>* or try <a href="https://www.firsty.app/">Firsty for free (but slow and ad-supported) data</a>.</li>
<li>power bank (planes generally restrict to &lt;100Wh and this <em>must</em> be in hand luggage)</li>
<li>charge cables</li>
<li>USB wall charger</li>
<li>laptop</li>
<li><strong>laptop charger</strong> (I'm looking at you, <a href="https://twitter.com/jasonelkin86/status/1668186388884344833">Jason</a>!)</li>
<li>UK adaptor, if your plug pins are an odd shape! (the UK uses 240V, so you might need a voltage converter if your devices like voltages closer to 100V 🦅)</li>
<li>headphones (vital for the journey!)</li>
</ul>

<h2>Sleeping</h2>

<p>You'll be sharing a room, so prepare for the worst!</p>

<ul>
<li>eye mask</li>
<li>ear plugs (if you have nice ones, otherwise speak to Lee for some disposable ones)</li>
<li>pyjamas that you don't mind being seen by a <del>stranger</del> <ins>future friend</ins> in!</li>
</ul>

<h2>Hygiene</h2>

<p>We like to hug and high-5!</p>

<ul>
<li>toothbrush</li>
<li>toothpaste</li>
<li>deodorant</li>
<li>shower gel/shampoo etc.</li>
<li>other toiletries and grooming kit</li>
<li>medication (including comforts like pain killers, etc.)</li>
</ul>

<p>Towels are provided by the venue.</p>

<h2>Travel</h2>

<p>Coming to our little island from abroad? I've got you!</p>

<ul>
<li>passport (check the validity!)</li>
<li>visa (if required)</li>
<li>health insurance card, if your country has one (e.g. <a href="https://www.gov.uk/guidance/healthcare-for-eu-and-efta-citizens-visiting-the-uk#what-you-need-to-do">EHIC</a>)</li>
<li>travel insurance documentation, if not travelling with work or you don't have a health insurance card</li>
<li>flight/train tickets (remember to check in!)</li>
<li>hotel reservation details</li>
<li>train reservation details (it can be cheaper and easier to prebook!)</li>
<li>fee-free payment card (<a href="https://revolut.com/referral/?referral-code=joejjcx!JUN1-23-AR">Revolut</a>*, <a href="https://join.monzo.com/c/ycrkzhf">Monzo</a>* and <a href="https://www.starlingbank.com/referral?code=1RXZAZ">Starling</a>* are popular options in the UK, register now to save money next time)</li>
<li><em>(Probably no)</em> Pounds Sterling (optional, particularly with a fee-free card <strong>I live in the UK and almost never need cash</strong>)</li>
</ul>

<h2>Miscellaneous</h2>

<ul>
<li>reusable coffee cup (for the journey)</li>
<li>wallet</li>
<li>snacks for the journey</li>
<li>entertainment for the journey - <strong>offline</strong> games, podcasts, movies, audiobooks and/or ebooks (or an analogue book if you're so inclined)</li>
</ul>

<p><small>*referral links mean we both get something nice!</small></p>

<p>Print this page or use the checkboxes to mark off items as you pack! Checked items are saved locally in your browser.</p>

<p>I'll see you there!</p>

<p><style>.content li {
 list-style-type: "\2610";
 padding-left: 1ex;
}
 .content li.done {
  list-style-type: "\2611";
text-decoration: line-through;
opacity: 0.75;
 }
}</style></p>

<script>
const lis = document.querySelectorAll(".content li");
const lsKeyPrefix = "CcPackingList:";
lis.forEach(li => {
  var lsKey = `${lsKeyPrefix}${li.innerText}`;
  if (localStorage.getItem(lsKey) == 'true') {
    li.classList.add("done");
  }
  li.addEventListener(
    "click",
    (ev) => {
      if (ev.target.tagName != "A") {
        li.classList.toggle("done");
        if (li.classList.contains("done")) {
          localStorage.setItem(lsKey, true);
        } else {
          localStorage.removeItem(lsKey, true);
        }
      }
    },
    false,
  );
});
var reset = document.createElement("button");
reset.innerText = "Clear checked items";
reset.addEventListener("click", (ev) => {
  Object.keys(localStorage)
    .filter(x =>
      x.startsWith(lsKeyPrefix))
    .forEach(x =>
      localStorage.removeItem(x));
  document.querySelectorAll("li.done").forEach(li => li.classList.remove("done"));
});
document.querySelector(".content").appendChild(reset);
</script>                ]]></content:encoded>
            </item>
            <item>
                <title>Plex and USB TV Tuners: TVHeadEnd and Antennas to build a DIY Plex DVR</title>
<link>https://joe.gl/ombek/blog/plex-and-usb-tv-tuners/</link>                <pubDate>Tue, 23 Jul 2024 11:27:17 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/plex-and-usb-tv-tuners/</guid>
                <description>Plex allows for streaming live TV when hooked up to TV tuners. This can allow for TV to be viewed around the house without an aerial cable or using internet bandwidth. Plex also makes a great DVR (live TV recorder) with the recordings then instantly available on your own personal streaming service.</description>
                <content:encoded><![CDATA[
                    <p>Plex allows for streaming live TV when hooked up to TV tuners. This can allow for TV to be viewed around the house without an aerial cable or using internet bandwidth. Plex also makes a great DVR (live TV recorder) with the recordings then instantly available on your own personal streaming service.</p>
<p>This is primarily a note to myself for how this is set up and to help me fix it when it goes wrong (again). But this is the detail for how to set it up, so may be of use to others.</p>

<p>This isn't an article explaining how to set up Plex, this assumes you have Plex already set up and running. (FYI, if you're starting from scratch, Kodi is open source and supports TVHeadEnd directly, without the need for Antennas) I also assume you've got a x64 Linux Docker server up and running on your home network. Instructions vary for other Docker setups.</p>

<p><strong>Plex requires a paid Plex Pass to record live TV</strong>. Watching live TV is free.</p>

<h2>My tuners</h2>

<p>I've got 3 tuners, all second hand. In fact, generally, the older the better. So long as they support digital TV.</p>

<p>I have two XBox One TV tuners (thanks to my friend <a href="https://umbracocommunity.social/@CodeBunTes">Terrance</a>, who is sadly no longer with us) and a Hauppauge myTV.t 326. I have three so that I can record or watch three channels at once. Only one is required.</p>

<p><a href="https://github.com/J0hnMatrix/tvheadend-xboxone-tuner/blob/master/README.md">The XBox One tuner may require additional firmware.</a></p>

<h2>TVHeadEnd</h2>

<p>TVHeadEnd is a fantastic piece of software which provides video streams over HTTP for each TV channel.</p>

<p>I've set this up as a Docker container using the following <code>docker-compose.yml</code> file:</p>

<pre><code>services:
  tvheadend:
    image: lscr.io/linuxserver/tvheadend:latest
    container_name: tvheadend
    environment:
      - PUID=0
      - PGID=0
      - TZ=Europe/London
      - RUN_OPTS= #optional
    volumes:
      - /home/joe/tvheadend/recordings:/recordings
      - /home/joe/tvheadend/config:/config
      - /home/joe/tvheadend/data:/data #not sure this is needed any more
    ports:
      - 9981:9981
      - 9982:9982
    devices:
      - /dev/dvb:/dev/dvb
    restart: unless-stopped
</code></pre>

<p>I also have created the folder /home/joe/tvheadend and its subfolders. The PUID and PGID must be the owner of the directories. To find the IDs for a user run <code>id [username]</code>.</p>

<p>Only one <code>device</code> is required to be configured even if you have multiple tuners, all physical devices sit under <code>/dev/dvb</code>.</p>

<p>To run, run <code>docker-compose up -d tvheadend</code> in the directory containing the above compose file.</p>

<h3>GUI Setup of TVHeadEnd</h3>

<p>Now you can navigate to your Docker server on port 9981 (e.g. <code>http://dockersever.local:9981</code>) and set up the tuners. This is complicated and I don't understand exactly what I did. <a href="https://gist.github.com/ProfYaffle/654aa5da1983d651d367#4-ensure-tuners-are-available-for-use">I found this gist that may be of help.</a></p>

<p>Also, set up a user for Plex's use. I set the username to <code>plex</code>. I granted this user all the permissions for now, but will revise these later.</p>

<p>One of my tuners does not support DVB-T2 (used for HD channels in the UK), so I have set this as a <em>higher</em> priority in TVHeadend. This means that any requests to play non-HD content are sent to the non-HD tuner, leaving the HD tuner free to be used for HD content. This setting is under Configuration > DVB Inputs > TV Adapters. Then select the <strong>non-HD</strong> adapter, set the view level (bottom right) to "Advanced" and set "Streaming priority" to a value higher than 0. I used 10.</p>

<h2>Antennas</h2>

<p>Antennas takes the streams provided by TVHeadEnd, and emulates an HDHomeRun device so that Plex can see it.</p>

<p>The <code>docker-compose.yml</code> file for this looks like this:</p>

<pre><code>services:
  antennas:
    image: thejf/antennas:latest
    container_name: antennas
    volumes:
      - /home/joe/antennas:/antennas/config
    ports:
      - 5004:5004
    restart: unless-stopped
</code></pre>

<p>and the <code>/home/joe/antennas</code> directory contains a file called <code>config.yml</code>:</p>

<pre><code>tvheadend_url: http://user:password@192.168.0.255:9981
antennas_url: http://192.168.0.255:5004
tuner_count: 3
</code></pre>

<p>With the above values containing the correct IP addresses/hostnames, username, password (set up earlier) and number of tuners you have. I've found IP addresses work best here, perhaps my Docker setup isn't quite right for local hostnames to work?</p>

<p>To run, <code>docker-compose up -d antennas</code> in the directory containing the above compose file.</p>

<h2>Plex</h2>

<p>Navigate to Settings > Live TV and DVR and add a tuner. This tends to automatically find the Antennas app, but manually entering the URL is a fallback. I haven't managed to get XMLTV to work yet, so have entered a postcode.</p>

<p>Select the correct EPG location (e.g. Freeview) and match up all the TV channels you want to be able to watch.</p>

<p>It will take a while to set up, but once it has, click the settings cog under the device in the "Channel Sources" heading and ensure the quality is set to "original". This seems to be the least likely to cause issues.</p>

<p>Streaming from the web app doesn't always work, but the native apps are more reliable.</p>

<p>The guide takes a <em>long</em> time to populate but you can stream channels to test by clicking the channel name.</p>

<h2>Issues</h2>

<p>I've realised that TVHeadEnd doesn't always use a second device for streaming to Plex. I assume this has to do with which muxes channels are on, but I'm only guessing! I think this means that theoretically Plex will stop recording concurrent streams before the theoretical maximum capacity of TVHeadEnd is used up. If you know of any more details or how to configure this better, please <a href="/ombek/links/">let me know</a>. It does work well enough, though!</p>

<p>I also failed to swap my old Haupague tuner for a 3rd XBox tuner. I'm yet to diagnose what went wrong but will update this guide if I have any luck in future.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbraco Flavored Markdown (UFM) cheat sheet for v14</title>
<link>https://joe.gl/ombek/blog/v14-ufm/</link>                <pubDate>Thu, 11 Jul 2024 01:11:56 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/v14-ufm/</guid>
                <enclosure url="https://joe.gl/media/j3whcpwt/screenshot-2024-07-11-133912.png" length="9123" type="image/png" />
                <description>Umbraco v14.1 contains a partial replacement for AngularJS Filters for generating dynamic labels for blocks and list views in the backoffice: Umbraco Flavored Markdown (or UFM). Here&#39;s a quick guide.</description>
                <content:encoded><![CDATA[
                    <p>Umbraco v14.1 contains a partial replacement for AngularJS Filters for generating dynamic labels for blocks and list views in the backoffice: Umbraco Flavored Markdown (or UFM). Here's a quick guide.</p>
<div class="notification is-warning is-light">
<p>⚠️ In v14 this functionality was extremely limited. This article is here for posterity and only applies to v14.1 and close versions.</p>
</div>

<p>See my <a href="/ombek/blog/umbraco-angularjs-filter-cheat-sheet/">Umbraco AngularJS filter cheat sheet</a> for v13 and below.</p>

<p>I'm working on a modern UFM cheat sheet for v17+.</p>

<p>When you're configuring a block type in a block list or block grid, you're provided with a "Label" field. Similarly, custom columns in your list views have a "label template". You can just type some plain text in here, but using dynamic labels is where it really shines.</p>

<p><img src="/media/j3whcpwt/screenshot-2024-07-11-133912.png" alt="Screenshot of the block configuration panel in Umbraco showing the label field." /></p>

<h2>Text string properties</h2>

<p>If your content item has a field with the alias <code>heading</code>, to render this in UFM we simply type <code>{=heading}</code>. This might render as:</p>

<blockquote>
  <p>Contact us</p>
</blockquote>

<p>You can also wrap this with other content such as <code>Text block: {=heading}</code> to render</p>

<blockquote>
  <p>Text block: Contact us</p>
</blockquote>

<p>This syntax works the same for text box, text area, email, decimal (non-zero values only), numeric (non-zero values only) but <strong>not</strong> rich text editor, slider, etc.</p>

<p>⚠️ Before v14.2, if the field is empty the alias of the property is returned in curly braces. So, <code>{=heading}</code> with no value becomes <q>{heading}</q></p>

<h2>Markdown and HTML</h2>

<p>Since UFM is Markdown, we are able to use Markdown syntax or HTML as well.</p>

<p>For example, <code>**{=heading}** - {=subheading}</code> would render</p>

<blockquote>
  <p><strong>Contact us</strong> - How to get in touch</p>
</blockquote>

<p>Or, using HTML, <code>**Quote:** &lt;q&gt;{=quote}&lt;/q&gt; - {=attribution}</code>. Might render you:</p>

<blockquote>
  <p><strong>Quote:</strong> <q>Joe, you're actually quite funny sometimes</q> - Lotte Pitcher</p>
</blockquote>

<h2>Toggles (true/false or boolean)</h2>

<p>These fields will spit out a "true" or "false". Or, if left as default, will be blank.</p>

<p>So, <code>{=heading} (Disabled: {=disabled})</code> may become:</p>

<blockquote>
  <p>My block heading (Disabled: true)</p>
  
  <p>My block heading (Disabled: false)</p>
</blockquote>

<p>Or if left unset:</p>

<blockquote>
  <p>My block heading (Disabled: )</p>
</blockquote>

<h2>Content pickers</h2>

<p>Not supported in 14.1, but a <a href="https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/src/packages/ufm/ufm-components/document-name.element.ts">work-in-progress exists</a>.</p>

<h2>Writing your own</h2>

<p>Much like in AngularJS, it's possible to write your own UFM components. Components follow the same pattern as the default, but the <code>=</code> is replaced with a custom symbol.</p>

<p>In this example, I use the symbol <code>%</code>, so <code>{% Hello}</code> would render as:</p>

<blockquote>
  <p>👋🏻 Hello!</p>
</blockquote>

<p>First, I created a package manifest at <code>~/App_Plugins/umf-customisations/umbraco-package.json</code>, and set my <code>marker</code> to <code>%</code>:</p>

<pre><code>{
  "id": "UFM Customisations",
  "name": "Joe Glombek",
  "version": "1.0.0",
  "allowPackageTelemetry": true,
  "extensions": [
    {
      "type": "ufmComponent",
      "alias": "My.CustomComponent",
      "name": "My Custom UFM Component",
      "api": "/App_Plugins/ufm-customisations/components/my-custom.component.js",
      "meta": {
        "marker": "%"
      }
    }
  ]
}
</code></pre>

<p>Then, in the referenced <code>~/App_Plugins/ufm-customisations/components/my-custom.component.js</code>:</p>

<pre><code>export default class MyCustomComponent {
  render(token) {
    return `👋🏻 ${token.text}!`;
  }
}
</code></pre>

<p>For more complex scenarios, you'd likely want to use Typescript and implement <code>UfmComponentBase</code>:</p>

<pre><code>export class MyCustomComponent implements UfmComponentBase {
    render(token: Tokens.Generic) {
        // Do something more clever here
    }
}
</code></pre>

<p>and make use of the <code>@umbraco-cms/backoffice</code> NPM package.</p>

<p>The return value can also be a web component, like <a href="https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/src/packages/ufm/ufm-components/label-value.component.ts">the default <code>{=name}</code> component</a>:</p>

<pre><code>return `&lt;ufm-label-value alias="${token.text}"&gt;&lt;/ufm-label-value&gt;`;
</code></pre>

<p>Allowing for all complex logic to sit inside a separate web component.</p>

<p>A better example will follow, once I have one.</p>

<h2>Bugs and quirks</h2>

<p>The following have been <a href="https://github.com/umbraco/Umbraco-CMS/issues/16776">raised on the Umbraco issue tracker</a>, but are worth noting.</p>

<ul>
<li><del>In a collection view, the value outputted by the <code>{=x}</code> syntax always outputs the value of the field selected</del> <ins><a href="https://github.com/umbraco/Umbraco.CMS.Backoffice/pull/2139">As of v14.2, in collection views the syntax is now always <code>{=value}</code>, much like the Angular format</a></ins></li>
<li><del>Null fields (or fields with a default value of null) render the name of the label, with no option to coalesce to something else</del> <ins>This has <a href="https://github.com/umbraco/Umbraco.CMS.Backoffice/pull/2139">a fix in v14.2</a>, but I haven't tested it yet</ins></li>
<li><del>Null-like values will not render (e.g. a <code>0</code> in a numeric field)</del> <ins><a href="https://github.com/umbraco/Umbraco.CMS.Backoffice/pull/2139">As of v14.2, falsey values render correctly</a></ins></li>
<li><del>I could not get localization (<code>{#general_chose}</code>) to work in the dynamic labels</del> <ins>This was a typo but requires further testing for custom dictionary items</ins></li>
</ul>

<h2>Further reading</h2>

<ul>
<li><a href="https://github.com/umbraco/UmbracoDocs/pull/6250/files">PR for the documentation of UFM</a></li>
<li><a href="https://github.com/umbraco/Umbraco.CMS.Backoffice/pull/2045">Merged PR for this feature</a></li>
</ul>                ]]></content:encoded>
            </item>
            <item>
                <title>Tag-style exact-matching multiple strings when searching with Examine in Umbraco</title>
<link>https://joe.gl/ombek/blog/tag-style-exact-matching-with-examine/</link>                <pubDate>Fri, 21 Jun 2024 01:14:10 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/tag-style-exact-matching-with-examine/</guid>
                <enclosure url="https://joe.gl/media/5qcdetjs/_29673c63-6ef1-45d7-8dbb-b48b3155cc2b.jpeg" length="126066" type="image/jpeg" />
                <description>Searching for exact strings in Examine can be a little counter-intuitive. Couple that with the confusion of multiple values (potentially using the Repeatable text strings or tag data types) and inconsistencies between the backoffice tooling and the fluent API and you&#39;ve got a right potential mess on your hands!</description>
                <content:encoded><![CDATA[
                    <p>Searching for exact strings in Examine can be a little counter-intuitive. Couple that with the confusion of multiple values (potentially using the Repeatable text strings or tag data types) and inconsistencies between the backoffice tooling and the fluent API and you've got a right potential mess on your hands!</p>
<p>The situation is this: I have some content nodes representing Meetup groups which have a repeatable text string field on them to list the organisers of the meetup. In my case, these are people's names but they could just as easily be blog categories. This content needs to be filterable using an Examine search query. In my case, I want to list out meetups which are run by "Joe Bloggs", but again, this could easily be listing out blog posts in each tag.</p>

<p>I'd have the same issue if these repeatable text strings were using the tags data type instead, but a slightly different issue if they were picked content (indexing the IDs or UDI makes this a lot easier!)</p>

<h2>How an Examine search usually works</h2>

<p>A typical search function may look like this:</p>

<pre><code>// Get the index
if (!_examineManager.TryGetIndex("ExternalIndex", out var index))
{
    return (Enumerable.Empty&lt;MeetupDetailPage&gt;(), 0);
}

// Create a query (some extension methods are from the Search Extensions package)
IBooleanOperation query = index
    .Searcher
    .CreatePublishedQuery()
    .And().NodeTypeAlias(MeetupDetailPage.ModelTypeAlias)
    .And().ParentId(parentId)
    ;

if (organisers?.Any() == true)
{
    query.And().GroupedAnd("organisers".AsEnumerableOfOne(), organisers);
}

var res = query.Execute();
// etc.
</code></pre>

<p>If I run the above code, the Lucene query generated will be something along the lines of:</p>

<pre><code>+__Published:y -umbracoNaviHide:1 -templateID:0 +__NodeTypeAlias:meetupdetailpage +(parentID:[1000 TO 1000]) +organisers:Joe Bloggs
</code></pre>

<p>This will return groups organised by "Joe Smith" and "Jane Bloggs" as well as the desired "Joe Bloggs"-organised endeavours!</p>

<h2>Exact match</h2>

<p>Examine gives me the option of specifying I want this to match exactly, by using the <code>.Escape()</code> extension method:</p>

<pre><code>query.And().GroupedAnd("organisers".AsEnumerableOfOne(), organisers.Select(x=&gt;x.ToLower().Escape()).ToArray());
</code></pre>

<p>Since the "terms" within the values in the index will be lower-cased, we also have to lower case the search query.</p>

<p>However, this also has some downsides. If I have a "<em>Where American Politics meets Umbraco</em>" meetup organised by "Elizabeth Warren" and "Warren Buckley", searching for "Warren Warren" (cruel parents, I know!) would return meetups run by the two former organisers as well since by default Examine strips out whitespace.</p>

<p>The value is stored as a series of tokens, <code>elizabeth</code>, <code>warren</code>, <code>warren</code> and <code>buckley</code>. The <code>.Escape()</code> extension ensures the tokens are in the specified sequence, but not that they are different names.</p>

<p>Examine will also split out "stop words" - filler words in a common search phrase -  such as "of", "and" and "the". Meaning "The Umbraco Rabbit of Codegarden Fame" gets tokenized as <code>umbraco</code> <code>rabbit</code>, <code>codegarden</code>, <code>fame</code> which an exact query will never find!</p>

<h2>None of that tokenisation nonsense here, thank you very much!</h2>

<p>A cleaner way to store this value is to not allow Examine to split it into smaller tokens. Using the <code>Raw</code> value type, Examine will treat the name as an atomic <code>Joe Bloggs</code>.</p>

<p>To do this, we can make use of the IConfigureNamedOptions interface:</p>

<pre><code>public class ConfigureExternalIndexOptions : IConfigureNamedOptions&lt;LuceneDirectoryIndexOptions&gt;
{
    private readonly ILoggerFactory _loggerFactory;

    public ConfigureExternalIndexOptions(ILoggerFactory loggerFactory)
    {
        _loggerFactory = loggerFactory;
    }
    public void Configure(string name, LuceneDirectoryIndexOptions options)
    {
        if (name is Constants.UmbracoIndexes.ExternalIndexName)
        {
            // Setting this value to raw saves each name as its own term e.g. "The Umbraco Rabbit of Codegarden Fame", rather than "umbraco" "rabbit" "codegarden", "fame" which is default.
            options.FieldDefinitions.AddOrUpdate(new FieldDefinition("organisers", FieldDefinitionTypes.Raw));
        }
    }

    public void Configure(LuceneDirectoryIndexOptions options)
    {
        // We don't need this bit
        throw new System.NotImplementedException();
    }
}
</code></pre>

<p>The downside here, is that multiple values are now atomic too. That meetup organised by Elizabeth and Warren we mentioned earlier would be indexed as <code>Elizabeth Warren Warren Buckley</code> and would only be returned when searching for that exact phrase.</p>

<p>So we need to split these values into multiple atomic values.</p>

<h2>Splitting the value into multiple values</h2>

<p>Examine provides a very handy event called <code>TransformingIndexValues</code> that allows us to manipulate values entering the index.</p>

<p>To access this, we need to create a component, listen to the <code>TransformingIndexValues</code> event on the index provider, and split the value into its multiple parts.</p>

<pre><code>public class MeetupIndexingComposer : ComponentComposer&lt;MeetupIndexingComponent&gt;
{
}

public class MeetupIndexingComponent : IComponent
{
    private readonly IExamineManager _examineManager;

    public MeetupIndexingComponent(IExamineManager examineManager)
    {
        _examineManager = examineManager;
    }
    public void Initialize()
    {
        if (!_examineManager.TryGetIndex(Umbraco.Cms.Core.Constants.UmbracoIndexes.ExternalIndexName,
                out var index))
        {
            return;
        }

        if (!(index is BaseIndexProvider indexProvider))
        {
            return;
        }

        indexProvider.TransformingIndexValues += IndexProviderOnTransformingIndexValues;
    }

    private void IndexProviderOnTransformingIndexValues(object sender, IndexingItemEventArgs e)
    {
        if (e.ValueSet.Category == IndexTypes.Content)
        {
            foreach (var value in e.ValueSet.Values)
            {
                if (value.Key == "organisers")
                {
                    var updatedValues = e.ValueSet.Values.ToDictionary(x =&gt; x.Key, x =&gt; x.Value.ToList());

                    // Here we split by new line, to split repeatable text strings. You might need to split by something else. Comma, maybe?
                    var values = value.Value.FirstOrDefault().ToString().Split(Environment.NewLine);
                    updatedValues["organisers"] = values.Cast&lt;object&gt;().ToList();

                    e.SetValues(updatedValues.ToDictionary(x =&gt; x.Key, x =&gt; (IEnumerable&lt;object&gt;)x.Value));
                }
            }
        }
    }

    public void Terminate()
    {
    }
}
</code></pre>

<p>(<a href="https://our.umbraco.com/forum/using-umbraco-and-getting-started/109345-examine-transforming-index-values-in-v10#comment-339214">Thanks to Marc Love for pointing out how to save these values in Umbraco 10+!</a>)</p>

<p>Our tokenized values are now <code>Joe Bloggs</code> or <code>Elizabeth Warren</code>, <code>Warren Buckley</code>! Now, how do we search for them?</p>

<h2>Searching against multiple values</h2>

<p>This is now even simpler than before:</p>

<pre><code>query.And().GroupedAnd("organisers".AsEnumerableOfOne(), organisers);
</code></pre>

<p>Which generates a query ending:</p>

<pre><code>+organisers:Joe Bloggs
</code></pre>

<p>The Examine query parser will treat this nicely and only return meetups where the exact phrase "Joe Bloggs" is entered as the entire line of the repeatable text strings field.</p>

<h2>Searching in the backoffice</h2>

<p>It's worth noting that this query won't work in the handy backoffice dashboard (<code>/umbraco/#/settings?dashboard=settingsExamine</code>) to test Lucene queries.</p>

<p>This is due to the fact that <a href="https://github.com/Shazwazza/Examine/issues/329#issuecomment-2168484838">the backoffice doesn't use a query analyser</a>.</p>

<p>You'll need to modify your generated queries to add double-quotes around each value like so:</p>

<pre><code>+organisers:"Joe Bloggs"
</code></pre>

<h2>Further reading</h2>

<p>If you're doing something like the above example and need to generate a filter with all the possible values of organisers (or tags or whatever), it might be worth looking into faceting (<a href="https://dev.to/jemayn/facetted-search-with-examine-umbraco-13-k7i">guide to faceting with Examine in Umbraco</a>, <a href="https://shazwazza.github.io/Examine/articles/searching.html#faceting">Examine faceting documentation</a>).</p>

<p>As it happens, <a href="https://umbracocommunity.social/@Jmayn/112655383259022323">Jesper was grappling with the same issue recently</a>, but <a href="https://dev.to/jemayn/searching-with-umbraco-examine-avoid-these-common-filtering-mistakes-1oin">came up with a different solution, which he also blogged about</a>.</p>

<p><a href="https://docs.umbraco.com/umbraco-cms/reference/searching">The Umbraco documentation for searching</a> is getting more comprehensive too.</p>

<p><a href="https://our.umbraco.com/packages/website-utilities/search-extensions/">Search Extensions</a> has some useful extension methods for using with Examine searches, as well as some sensible defaults (filtering by ancestors using the <code>path</code> field, for one). (A little bird told me faceting may be coming to Search Extensions in a release near you.)</p>

<p>Luke is a useful tool for peeking inside your index. It's a Java tool that now comes bundled with the Java version of Lucene. You'll need to use <a href="https://github.com/DmitryKey/luke/releases/tag/4.8.0">the Luke version which matches the version of Lucene.Net used by Umbraco - 4.8.0 at the time of writing</a>. You'll need the <a href="https://www.oracle.com/java/technologies/downloads/">JDK</a> to run it. Also, don't try to run this on a monitor with any scaling - 1080p at 100% works best!</p>

<p>This article and topic are covered on <a href="https://www.youtube.com/live/ApAdVfU53i8?si=6KCcrFwAR0ZkqLWc">umbraCoffee's July 2024 episode</a>.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Reset Umbraco user passwords - code free!</title>
<link>https://joe.gl/ombek/blog/reset-umbraco-user-passwords/</link>                <pubDate>Mon, 03 Jun 2024 01:48:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/reset-umbraco-user-passwords/</guid>
                <enclosure url="https://joe.gl/media/oszdbs02/forgotten-password.png" length="26521" type="image/png" />
                <description>The number of times I get access to a project to work on locally without knowing the admin password, you wouldn&#39;t believe! And every time, I forget the steps to enable a password reset!</description>
                <content:encoded><![CDATA[
                    <p>The number of times I get access to a project to work on locally without knowing the admin password, you wouldn't believe! And every time, I forget the steps to enable a password reset!</p>
<p>It's got a lot simpler to reset an admin's password in modern Umbraco. You used to have to mess with hashes in the database and before that, drag a mysterious DLL into your bin folder!</p>

<p>But now, it's simpler than ever to initiate a password reset - via the UI! It's a simple matter of clicking the "Forgotten password?" link on the Umbraco login screen.</p>

<p>Unless...</p>

<h2>No "Forgotten password?" link?</h2>

<p>To enable password reset you'll need the following appsettings (set them in <a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&amp;tabs=windows#use-visual-studio">User Secrets</a> if you only want it locally):</p>

<pre><code>{
  "Umbraco": {
    "CMS": {
      "Security": {
        "AllowPasswordReset": true
      }
    }
  }
}
</code></pre>

<p>(<a href="https://docs.umbraco.com/umbraco-cms/fundamentals/backoffice/login#password-reset">Umbraco docs</a>)</p>

<h2>No access to the email inbox?</h2>

<p>If you've not got access to the admin email address, you can set up a local SMTP server. I like to use <a href="https://github.com/rnwood/smtp4dev">smtp4dev</a> these days.</p>

<pre><code>{
  "Umbraco": {
    "CMS": {
      "Global": {
        "Smtp": {
          "From": "noreply@example.com",
          "Host": "localhost",
          "Port": 25,
          "SecureSocketOptions": "Auto",
          "PickupDirectoryLocation": "",
          "DeliveryMethod": "Network"
        }
      }
    }
  }
}
</code></pre>

<h2>Don't know the admin email?</h2>

<p>If you don't even know the admin email, you'll need to look in the database (sorry, not quite codeless!):</p>

<pre><code>SELECT userLogin, userEmail
  FROM [umbracoUser]
  -- Optionally, this returns the original admin account. Delete the following to see all users:
  WHERE id &lt;= 0
</code></pre>

<h2>Other settings</h2>

<p>You may also need to override other settings like <code>UmbracoApplicationUrl</code> for the reset to work completely.</p>

<p>A fairly standard user secrets file to accomplish a password reset might look like this:</p>

<pre><code>{
  "Umbraco": {
    "CMS": {
      "Global": {
        "Smtp": {
          "From": "noreply@example.com",
          "Host": "localhost",
          "Port": 25,
          "SecureSocketOptions": "Auto",
          "PickupDirectoryLocation": "",
          "DeliveryMethod": "Network"
        }
      },
      "Security": {
        "AllowPasswordReset": true
      },
      "WebRouting": {
        "UmbracoApplicationUrl": ""
      }
    }
  }
}
</code></pre>

<p><small>Thanks to <a href="https://cultiv.social/@sebastiaan/112553761450281890">Sebastiaan Janssen for his correction</a>.</small></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Candid Contribs talks Mastodon: Some corrections and notes</title>
<link>https://joe.gl/ombek/blog/candid-contribs-talks-masto/</link>                <pubDate>Thu, 02 May 2024 11:57:14 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/candid-contribs-talks-masto/</guid>
                <enclosure url="https://joe.gl/media/2dmctsdb/_3084cc28-ef95-4df6-bb65-17b7e8e5250d.jpeg" length="344486" type="image/jpeg" />
                <description>The Candid Contributions Podcast episode &quot;Toots, Tweets and Time Management&quot; mentioned the Umbraco Community Mastodon but made some comments I think may have been misleading, so I thought I&#39;d clarify.</description>
                <content:encoded><![CDATA[
                    <p>The Candid Contributions Podcast episode "<a href="https://www.spreaker.com/episode/toots-tweets-and-time-management--59747213">Toots, Tweets and Time Management</a>" mentioned the Umbraco Community Mastodon but made some comments I think may have been misleading, so I thought I'd clarify.</p>
<p>No disrespect to the Candid Contribs team - these are common misconceptions and you tackled some of these misconceptions in the podcast too. But I felt it's important to get this out there.</p>

<p>Twitter's userbase disbanding has been detrimental to many online communities. The Umbraco Community has largely moved to LinkedIn, Discord and Mastodon. This means that it is fragmented, and it's highly likely that none will ever replace the critical mass Twitter had.</p>

<h2>"Enclosed"</h2>

<p>Let's not forget that Twitter never had the whole community. People who miss the 'whole community' being together, just happened to be at the centre of the Twitter community. I never used Twitter prior to getting involved with Umbraco and never managed to fit it into my routine - or truly feel a part of the Umbraco Community on Twitter.</p>

<p>A few of the Candid Contributions team feel the same way about Mastodon - and that's expected. You spent years working on your Twitter presence and curating your Twitter timeline by finding interesting people and speaking about interesting things (only for the that timeline to be polluted in the latter years, leading to the disenchantment of many). Mastodon could replace that platform. But it takes time to build a following and curate that feed. (One of the benefits of Mastodon and potentially the Fediverse is that you might not have to do this going forwards - you can take your following and followers with you!)</p>

<p>As for the Umbraco Community Mastodon - it's not an "enclosed space" as suggested in the podcast. The Fediverse is designed to be exactly the opposite of that. There are features such as the <a href="https://umbracocommunity.social/public/local">local feed</a> that only show content from the server you're currently on - but this is just one view! Think of the discoverability provided by being on the same server as the "suggested follows" feature Twitter had when you created your account. Think of it as a starting place and then find people you're interested in, follow hashtags, and begin curating your experience. The most reliable way to find Umbraco content on Mastodon is to follow the <a href="https://umbracocommunity.social/tags/umbraco">#Umbraco hashtag</a>, rather than watching a local feed.</p>

<p>Think of Mastodon like email or podcasting. Just because Candid Contribs uses Spreaker to distribute the podcast, it doesn't stop me listening with Pocket Casts (or Apple Podcasts or Spotify etc.). Just because I use Gmail, doesn't stop me email people on Outlook or Yahoo. Sure, Gmail has some nice features when emailing other Gmail users - like automatic profile picture population - but it's not essential to the experience. Email, podcasting and RSS are all part of the open web, just like the Fediverse and Mastodon.</p>

<p>Like Lotte, I was never a heavy Twitter user, using it primarily for Umbraco content. Which admittedly makes the Umbraco Community Mastodon a very easy drop-in replacement. But now, I've found myself using Mastodon outside of work - for entertainment and education. I've even <a href="https://expl.red/@joe">replaced my use of Instragram with a fediverse alternative - Pixelfed</a>.</p>

<h2>"Confusing"</h2>

<p>Mastodon is techy and confusing - but no more so than Twitter was when we all started there! Twitter had only just reached the mainstream as it began its fall back into obscurity - only with the far right being it's new niche rather than technologists. Huge swathes of people still "never really got Twitter" and that's fine - nobody <em>needs</em> microblogging!</p>

<h2>Discoverability</h2>

<p>Emma mentioned how she misses "stumbling" across things. Stumble-upon-ability is something that I'd suggest Twitter never did that well - it was a follower-lead platform. The "for you" algorithms of other platforms are actually far better at allowing you to stumble upon other people. Just like the early days of Twitter, hashtags and boosts are the best ways of spreading content to new eyes. I "stumble upon" organic content on Mastodon more than I would a corporate-backed social media platform lead more by ads - I now have a great set of people I follow, who boost me interesting content every day on a breadth of topics, from the wider .NET community to general web and tech to the environment and international politics.</p>

<h2>In the open</h2>

<p>Mastodon is not as closed as Twitter is and is certainly more open and discoverable than Discord. Everything is public facing and is <a href="https://www.google.com/search?q=site:umbracocommunity.social+umbraco">discoverable on Google</a> and by other Mastodon and Fediverse instances. LinkedIn is another closed community, requiring a login to see content.</p>

<p>If you want to join a wider community, try <a href="https://dotnet.social">DotNet.Social</a>, <a href="https://fosstodon.org/">FOSStodon</a> or a <a href="https://joinmastodon.org/servers">generic Mastodon server</a>. You can always come to the <a href="https://umbracocommunity.social/directory">Umbraco Community Mastodon profile listing if you need inspiration for who to follow</a>. The call-to-action for getting new people on the Fediverse should be just that: join the Fediverse, not "join the Umbraco Community Mastodon".</p>

<h2>In conclusion</h2>

<p>If you don’t want to dedicate your time to Mastodon - don’t! If you’re missing microblogging in your life - and the ability to “just put a thought out into the world” - give it a go!</p>

<p>I previously blogged about <a href="/ombek/blog/umbraco-in-the-fediverse/">Umbraco in the Fediverse</a> and <a href="/ombek/blog/masto-meta/">Gaining insights into fediverse accounts</a> which may also be of interest.</p>

<h2>Suggested follows</h2>

<p>If you do feel a bit enclosed in the Umbraco-spere on Mastodon, try following some of these accounts to broaden your fedi-horizons.</p>

<h3>Politics and news</h3>

<ul>
<li>Washington Post Opinions @postopinions@threads.net</li>
<li>Joe Biden @potus@threads.net</li>
<li>Barack Obama @barackobama@threads.net</li>
<li>BBC Radio 4 @BBCRadio4@social.bbc</li>
<li>BBC Radio 5 Live @BBC5Live@social.bbc</li>
</ul>

<h3>Nature, science and climate</h3>

<ul>
<li>RSPB @RSPB@mastodonapp.uk</li>
<li>Scientist Rebellion @ScientistRebellion@social.rebellion.global</li>
<li>Greenpeace @greenpeace@mastodon.social</li>
<li>Vagina Museum @vagina_museum@masto.ai</li>
<li>Prof. Alice Roberts @aliceroberts@mastodonapp.uk</li>
<li>Prof. Stefan Rahmstorf @rahmstorf@fediscience.org</li>
<li>Extinction Rebellion @ExtinctionR@social.rebellion.global</li>
<li>Greta Thunberg @gretathunberg@mastodon.nu</li>
</ul>

<h3>Comedy &amp; authors</h3>

<ul>
<li>John Green @johngreenwritesbooks@threads.net</li>
<li>James Veitch @veitch@mastodon.social</li>
<li>NewsThump (news satire) @newsthump@mastodon.online</li>
<li>J. L. Westover (web comics) @MrLovenstein@mastodon.social</li>
<li>The Oatmeal (web comics) @oatmeal@mastodon.social</li>
</ul>

<h3>Non-Umbraco tech</h3>

<ul>
<li>The Verge @verge@mastodon.social</li>
<li>Technology Connections @TechConnectify@mas.to</li>
<li>.NET Bot (boosts posts tagged #DotNet) @bot@dotnet.social</li>
<li>ORG (campaigning for UK digital rights) @openrightsgroup@social.openrightsgroup.org</li>
<li>Internet Archive @internetarchive@mastodon.archive.org</li>
<li>DEV (shares articles from dev.to) @thepracticaldev@fosstodon.org</li>
<li>Mekka Okereke @mekkaokereke@hachyderm.io</li>
<li>Jon Skeet @jonskeet@hachyderm.io</li>
<li>Dylan Beattie @dylanbeattie@hachyderm.io</li>
<li>Mozilla @mozilla@mozilla.social</li>
<li>Scott Hanselman @shanselman@hachyderm.io</li>
<li>Layla Porter (LaylaCodesIt) @layla@dotnet.social</li>
<li>Chris Coyier @chriscoyier@front-end.social</li>
</ul>                ]]></content:encoded>
            </item>
            <item>
                <title>Masto-meta: Gaining insights into fediverse accounts</title>
<link>https://joe.gl/ombek/blog/masto-meta/</link>                <pubDate>Sun, 24 Mar 2024 08:49:47 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/masto-meta/</guid>
                <enclosure url="https://joe.gl/media/xwthctak/masto-verified.jpg" length="204432" type="image/jpg" />
                <description>Authenticity in the fediverse isn&#39;t as simple as a blue tick (then again, it&#39;s not that simple on Twitter any more either!) but there are some insights we can gather from a quick glace at a profile. I often see people attempting to interact with fediverse accounts that aren&#39;t real people, so these tips ought to help.</description>
                <content:encoded><![CDATA[
                    <p>Authenticity in the fediverse isn't as simple as a blue tick (then again, it's not that simple on Twitter any more either!) but there are some insights we can gather from a quick glace at a profile. I often see people attempting to interact with fediverse accounts that aren't real people, so these tips ought to help.</p>
<h2>Usernames and instances</h2>

<p>A fediverse username has two parts, the "local username" and the "instance name".
For example, my current username is <code>@joe@umbracocommunity.social</code>. <code>joe</code> is my chosen username and <code>umbracocommunity.social</code> is the domain name (web address) of the server or instance I signed up with. This isn't always a Mastodon server, but can be any ActivityPub compliant site.</p>

<h3><a href="https://press.coop/about">press.coop</a></h3>

<p>An account ending with <code>@press.coop</code> are unofficial bots which scrape RSS feeds of various news sites. They reject replies to posts (but that won't stop you tagging them!) and no replies are visible to anyone at the news organisations.
All accounts are marked as bots (see below) and have a meaningless gold checkmark emoji.
<a href="https://press.coop/@BBCNews">@BBCNews@press.coop</a> is a popular account but has nothing to do with the BBC. Although <a href="https://social.bbc/about">the BBC is running a trial of Mastodon</a>, this does not yet include any official news sources.</p>

<h3>bird.makeup (and other domains containing <code>bird</code> or <code>birdsite</code>)</h3>

<p><a href="https://sr.ht/~cloutier/bird.makeup/">Bird.Makeup</a> is an open-source one-way bridge from Twitter/X to the Fediverse. You cannot interact with a bird.makeup user, since the bridge is one way. There can also be sizable delays in posts appearing. There's no way to opt out or redirect from an old account to a new one, so <code>@umbristol@bird.makeup</code> still works even though we have an active Mastodon account at <code>@umbristol@umbracocommunity.social</code> - so make sure if you're tagging someone, you tag the right account!
These are only good for reading posts from people not on the fediverse. Think <a href="https://umbracocommunity.social/@potus@bird.makeup">@potus</a> or <a href="https://umbracocommunity.social/@callumbwhyte@bird.makeup">@callumbwhyte</a>.</p>

<h3>threads.net</h3>

<p>This week, Instagram Threads announced they were extending their Fediverse trial to a wider audience - including all Threads accounts in the US, Canada and Japan (at time of writing). The integration must be <a href="https://help.instagram.com/760878905943039">manually enabled</a> is currently limited to one-way (Threads to Fediverse) meaning replies can't be seen from Threads and Threads users can't yet follow other Fediverse accounts, but Fediverse likes do show up in Threads.</p>

<p>Interestingly verified accounts on Threads don't show up in the Mastodon interface in any way yet. But if you want, you can now follow what @zuck@threads.net has to say!</p>

<p><img src="/media/hh2pghqu/zuck.png" alt="A screenshot of Mark Zuckerberg&#39;s Threads profile from Mastodon." /></p>

<h3>Sites that aren't a social network</h3>

<p>We already touched on <code>bird.makeup</code> not being a Mastodon server. It's its own thing. But there's nothing stopping any site being on the Fediverse - you just need to implement certain API endpoints! This means that <a href="https://wordpress.com/support/enter-the-fediverse/">many Wordpress blogs</a> and other non-social network sites are followable from your Mastodon feed. Replies to posts from non-social networks can be a bit of an unknown. They may be rejected, ignored or show up as comments. Who knows!</p>

<p>Warren's Hack Make Do blog is followable at <a href="https://umbracocommunity.social/@blog.hackmakedo.com@blog.hackmakedo.com">@blog.hackmakedo.com@blog.hackmakedo.com</a>, or you can choose to follow the human <a href="https://umbracocommunity.social/@warrenbuckley">@warrenbuckley@umbracocommunity.social</a>.</p>

<h2>Checkmarks</h2>

<p>You might see some accounts with checkmarks in the names of accounts. These are simply custom emoji on the instance the account lives on and means nothing special. Some instances <em>may</em> restrict which accounts they allow to have a certain emoji, but there's no guarantee.</p>

<h2>Bot accounts</h2>

<p>Mastodon has a feature to clearly show whether an account is automated (a bot) or not. This isn't a bulletproof system - it generally relies on folks being honest about it. But if a profile displays a "Bot" or "Automated" flag (depending on the client you're using), don't expect a human reply if you get a reply at all!</p>

<p><a href="https://umbracocommunity.social/@umbfyi">@umbfyi@umbracocommunity.social</a> is a bot account.</p>

<p><img src="/media/4bedxzw0/umbfyi.png" alt="A screenshot of @umbfyi@umbracocommunity.social&#39;s profile, complete with an &quot;Automated&quot; or bot flag." /></p>

<h2>Link verification</h2>

<p>The best way to tell if a profile is genuine is through checking for verified links. Umbraco's official account is a great example of this. Since we already trust umbraco.com to be owned by Umbraco, the fact that @Umbraco@umbracocommunity.social has a verified link for umbraco.com in their profile shows they have editorial control over that page. <a href="https://joinmastodon.org/verification">You can verify multiple links using the <code>rel="me</code> syntax</a> in any page you can edit (your blog, Github profile, etc).</p>

<p>The first verified link shows up in search results on the default Mastdon client:</p>

<p><img src="/media/2d1d52x2/umb-search.png" alt="A search result for Umbraco with the verified umbraco.com link" /></p>

<p>and all links, verified or not, show up in the bio, although the verified links show in green with a checkmark:</p>

<p><img src="/media/ngggesak/umb-profile.png" alt="A screenshot of Umbraco&#39;s profile with both verified links" /></p>

<h2>How to tell a user is genuine</h2>

<p>So, in summary, to check you're interacting with the correct account:</p>

<ol>
<li><strong>Check for validated links</strong></li>
<li>Do you trust the server?</li>
<li>Are they a bot?</li>
<li>Check their bio (sometimes they'll state they're unofficial)</li>
</ol>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbraco Spark 2024: The Ultimate Guide!</title>
<link>https://joe.gl/ombek/blog/umbraco-spark-guide-24/</link>                <pubDate>Thu, 08 Feb 2024 02:13:35 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/umbraco-spark-guide-24/</guid>
                <enclosure url="https://joe.gl/media/ahclxydl/_5015bce3-afd6-46c0-94ad-0f1aa160e3cd.jpg" length="291461" type="image/jpg" />
                <description>Are you heading to Umbraco Spark this year but not sure what to expect? I live near Bristol and now consider myself a Spark veteran, so thought I&#39;d share my insights!</description>
                <content:encoded><![CDATA[
                    <p>Are you heading to Umbraco Spark this year but not sure what to expect? I live near Bristol and now consider myself a Spark veteran, so thought I'd share my insights!</p>
<h2>Getting hyped up for Spark</h2>

<p>Keep up to date with the goings on by <a href="https://umbracocommunity.social/@umbracospark">following Spark on Mastodon</a> or Twitter as they <a href="https://umbracocommunity.social/@umbracospark/111885245095461269">announce speakers</a> and share crucial event information. Keep an eye on the #UmbracoSpark, #Umbraco and #UmbracoCommunity hashtags in the lead up to the event to keep in touch with the communuity.</p>

<h2>The fun before the party starts!</h2>

<p>Although the Spark conference is only one day, the fun starts the day before with a hackathon and pre-party.</p>

<h3><a href="https://www.meetup.com/umbraco-hq-hackathons-workshops-and-more/events/297959356/">Hackathon</a></h3>

<p>Umbraco and true digital are hosting a hackathon all day on Thursday 7 March!</p>

<p>This is a great opportunity to get stuck in with the Umbraco source code, with <a href="https://24days.in/umbraco-cms/2023/what-we-mean-by-umbraco/">Umbraco HQ crew</a> and community experts on site to help you out!</p>

<h4>What is an Umbraco Hackathon?</h4>

<p>Unlike some other hackathons you may have attended, Umbraco hackathons don't have a strict theme or overall goal. They're an excuse to get together and code together. People work on Umbraco source code bugs, Umbraco packages and community projects. You don't need to have an idea before you attend - but be sure to ask for inspiration when you get there.</p>

<p>This hackathon is a little different to past Umbraco Hackathons in that there's some <a href="https://github.com/orgs/umbraco-community/projects/3/views/1">suggested tasks to work on</a> (or <a href="https://github.com/umbraco-community/spark-hackathon-2024/issues/new">add your own</a>). Comment on <a href="https://github.com/orgs/umbraco-community/projects/3/views/1">an issue</a> to register your interest in working on it.</p>

<p>The hackathon will include refreshments and lunch (usually pizza).</p>

<p><a href="https://github.com/umbraco-community/spark-hackathon-2024">More information is available on the Github repo.</a></p>

<h4>Convince your boss!</h4>

<p>How do you convince your boss to let you take an extra day for Spark? Simple! Hackathons allow you to get your head into the source code and understand how the product works better. You might even be able to fix some bugs that have been niggling your business for a while! You also get the assistance of Umbraco employees and community experts - who better to help you understand the Umbraco ecosystem!? Plus the usual networking - it's a very sociable way to code!</p>

<p>You don't have to be there for the whole thing either - just turn up for the afternoon if that's all you can muster!</p>

<p><a href="https://www.meetup.com/umbraco-hq-hackathons-workshops-and-more/events/297959356/">More information and sign up on Meetup. <strong>This is a separate event and requires signing up (free!) on Meetup to attend.</strong></a> Once signed up, keep an eye on your inbox in the weeks leading up to the event and <a href="https://github.com/umbraco-community/spark-hackathon-2024">check out the Github repo</a> for more details.</p>

<h3><a href="https://www.meetup.com/umbristol/events/298926853/">Pre-party!</a></h3>

<p>Every year <a href="https://www.umbristol.co.uk/">umBristol, the Bristol Umbraco Meetup</a>, host a pre-party to make friends (it's like "networking" but more fun!) and have fun. This year we're all meeting just 5 minutes from the hackathon venue at Roxy Lanes to play some duck-pin bowling, ice-free curling and other games. A few drinks are sometimes thrown in by the sponsor, and there's usually an opportunity to buy food.</p>

<p><a href="https://www.meetup.com/umbristol/events/298926853/">More information and sign up on Meetup. <strong>This is a separate event and requires signing up (free!) on Meetup to attend.</strong></a></p>

<p><img src="/media/jvhcsi4y/spark-preparty.jpg" alt="A split-screen cartoon. One side is of a unicorn in a neon-lit room with arcade games while sat on a curling stone, brush in hand. The other side is a unicorn in a bowling shirt holding a pink bowling ball, in a neon-lit bowling alley. Generated by AI." /></p>

<h2>The big day - what to expect</h2>

<h3>Morning run (optional!)</h3>

<p>Spark are hosting a morning 5km run. It should be "easy-paced" (but Frederik always claims his pre-Codegarden runs will be easy too - maybe I'm just too slow!) You'll need to <a href="https://docs.google.com/forms/d/e/1FAIpQLSeyIgioOsB_vjKKE6PKxxoQrZ9x8Wa_tm7bQdGh_VDtIPo8fA/viewform">register your interest in joining the run</a> and meet outside Ibis Bristol Centre Hotel, Explore Lane, Bristol, BS1 5TY (by the We The Curious disco ball!)</p>

<h3>Umbraco Spark!</h3>

<p>Spark is hosted at the fantastic <a href="https://www.bristolmuseums.org.uk/m-shed/plan-your-visit/getting-here/">M Shed on the harbourside</a>. Upon entry, follow the signage upstairs and be greeted by some friendly Bristolians to get your name badge and merch. Coffee and tea are available in the foyer and it's time to mingle! The Umbraco community are very friendly (it's kinda their thing!), so don't be afraid to chat to people you've never met before (even if they're a community celebrity!) I'll be there awkwardly mingling too, so come and chat to me - "I read your blog post" is always a great conversation opener!</p>

<p>Spark offers two tracks, and both are usually fantastic. So plan ahead as much as you can - you'll likely want to swap between tracks a lot! If you miss some talks, why not ask the speaker to repeat at your local meetup?</p>

<p>Lunch and snacks are provided at various points throughout the day, as well as a cheeky drink at the end before we move on to...</p>

<h3>The after-party</h3>

<p>No need to book this one, just turn up to <a href="https://arnolfini.org.uk/cafe-bar/">Arnolfini</a> across the harbour with your lanyard (it's privately hired). It's always nice to join folks for a drink and food before heading home - so try to plan around this if you can! There will be food and drink to buy at the after-party venue (this one's not included).</p>

<p>It's usually a casual affair where we sit and chat with friends new and old late into the night!</p>

<h2>What do I need to bring/wear?</h2>

<p>Spark is a casual conference, so a hoodie and jeans will suit for all events. Umbraco merch is always a bonus if you want to fit in! Also, remember a raincoat - we're only a stones throw from Wales after all!</p>

<p>You're also unlikely to need a laptop during the conference, but you might want to bring a notebook or take photos. If you elected to receive merch when buying your ticket, you'll get a branded notebook and pen on arrival as well as a t-shirt.</p>

<p>Don't forget your  laptop and charger if you're also attending the hackathon!</p>

<h2>Map</h2>

<p>A map of the primary locations is included below. It also includes my recommendations for Bristol - read on for details!</p>

<iframe src="https://www.google.com/maps/d/embed?mid=1EUFFYe4lGA5tcY5Rw9TrFcy6XBDwY04&hl=en&ehbc=2E312F" style="width:100%" height="480"></iframe>

<p><a href="https://www.google.com/maps/d/u/0/edit?mid=1EUFFYe4lGA5tcY5Rw9TrFcy6XBDwY04&amp;usp=sharing">Open map full screen.</a></p>

<h2>The Umbracian's guide to Bristol</h2>

<p>Bristol is a lovely city - so lovely, in fact, I moved here in 2022 - so many people elect to stay in the area for the weekend afterwards. Here's some advice for those people:</p>

<p><img src="/media/ahclxydl/_5015bce3-afd6-46c0-94ad-0f1aa160e3cd.jpg" alt="A cartoon unicorn sat in a harbour, surrounded by boats. The Clifton Suspension Bridge is behind it and hot air balloons float in the sky above." /></p>

<h3>Getting around</h3>

<p>Although the city is quite sprawling, a lot of Bristol is accessible by foot (particularly if you're good with hills!)</p>

<p>As much as the locals complain, Bristol has a pretty good bus network (just don't compare it to London or any continental European city!) Fares are £2 for a single, £3.80 for 2-trips and cap at £6 for the day, if you stay within Bristol. Busses all support tap-on-tap-off which are automatically capped, so no cash is needed.</p>

<p>For <a href="https://www.tier.app/en/">bike and electric scooter hire you'll need the TIER app</a> which you'll need your driving license (full or provisional) to sign up for, so be sure to do that ahead of time.</p>

<h3>Food and drink</h3>

<p>Bristol is home to fantastic independent cafes, pubs, restaurants and food stalls - as well as the birthplace of small chains like The Lounges and Boston Tea Party.</p>

<p>My favourite food stalls are in <a href="https://www.wappingwharf.co.uk/">Wapping Wharf (Spike Island, harbourside)</a> (St Nicholas Market is closed at the weekend!)</p>

<p>You can't go wrong with most cafes in Bristol, but some of my regulars are <a href="https://www.mud-dock.co.uk/cafe/">Mud Dock (harbourside)</a>, <a href="https://thebristolloaf.co.uk/beacon/">The Bristol Loaf ("The Centre")</a>, <a href="https://www.eastvillagecafe.co.uk/">East Village Cafe (Clifton Village)</a> and Santiago's (particularly if you're waiting for a bus at the bus station!)</p>

<p>Some of my favourite pubs include <a href="https://strawberrythiefbar.com/">The Strawberry Thief (central)</a>, <a href="https://butcombe.com/the-cottage-inn-bristol/">The Cottage Inn (Spike Island, harbourside)</a> and pretty much anywhere on King Street (central). (The Apple is closed for refurbishment).</p>

<h3>A touch of nature</h3>

<p>Leigh Woods is a fantastic woodland a short walk over Clifton Suspension Bridge. You'll want some boots but it's easy to follow signposted routes.</p>

<p>Ashton Court is a deer park with great views over Bristol and a lovely cafe. You can stick to tarmac and gravel paths here if you want clean shoes! Also a Parkrun location.</p>

<p>Brandon Hill is a lovely park right next to Park Street and is home of Cabot Tower - which is worth a climb (free) for some more fantastic views.</p>

<h3>Shopping</h3>

<p>Cabot Circus and Bristol Shopping Quarter are mostly (although not entirely!) big chain shops you'd find in most cities.</p>

<p>Clifton Village (further out than Clifton but within the city, unlike it's name implies!) is where you'll find the best of Bristol's local boutique-y gift shops and cafes. Also worth visiting the iconic suspension bridge while you're there.</p>

<p>A little closer to the centre of town is Park Street and Whiteladies Road as well as Gloucester Road, less classy than Clifton but some lovely smaller shops. (Gloucester Road is the charity-shopper's heaven!)</p>

<h3>Sightseeing</h3>

<p>Bristol is home to some great museums. M Shed is the venue for Spark but also a great little free museum. You can find <a href="https://www.bristolmuseums.org.uk/">more information about all the Bristol Museums on their website</a>.</p>

<p>The aquarium (central) is expensive but very good. Bristol Zoo is no longer located in Clifton, and is now outside the city.</p>

<p>Cabot Tower in Brandon Hill park (central) is free to climb for some epic views.</p>

<p>Clifton Suspension Bridge (Clifton Village) is free to walk across and Observatory Hill is nearby for some good views and a nice coffee shop. Or head into Clifton Village for endless cafes and pubs.</p>

<p>Bristol University has several beautiful historic buildings. The closest to the city centre are at the top of Park Street along with the Bristol Art Gallery.</p>

<p>Personally, I like to explore new cities using virtual audio tours. <a href="https://voicemap.me/share/xzplzs">Claim a free one using my VoiceMap referral link.</a>  There are 5 highly-rated tours of Bristol to choose from.</p>

<h3>Nearby</h3>

<p>The cities of Bath and Cardiff, Wales are within an easy train ride (15 minutes or 1 hour respectively) if you want to spread your wings a little further.</p>

<h2>See you there!</h2>

<p>I'll be at the hackathon right the way through to the after party - so I'll see you there! Please come and say hi, and see my talk too if it appeals!</p>

<p>If you think I've missed something vital, <a href="/ombek/contact/">let me know on socials!</a></p>

<p><img src="/media/cvdp05yj/spark-unicorn.png" alt="A more realistic unicorn stood on a hill with Bristol and the suspension bridge behind it at sunset. AI generated." /></p>

<p><small>
<em>Thanks to <a href="https://www.linkedin.com/in/tristanjthompson/">Tristan Thompson</a> for his contributions.</em>
</small></p>                ]]></content:encoded>
            </item>
            <item>
                <title>What do we mean when we say “Umbraco”?</title>
<link>https://24days.in/umbraco-cms/2023/what-we-mean-by-umbraco/</link>                <pubDate>Thu, 07 Dec 2023 08:00:56 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://24days.in/umbraco-cms/2023/what-we-mean-by-umbraco/</guid>
                <enclosure url="https://joe.gl/media/qwvd2tz5/cms.jpg" length="399530" type="image/jpg" />
                <description>Spoiler: we don’t always mean the CMS.

If you’re reading this article, there’s a high chance you&#39;re acquainted with Umbraco, but perhaps you&#39;re not fully aware of the nuance that comes with the phrase. So what exactly do we mean when we say “Umbraco”?</description>
                <content:encoded><![CDATA[
                    <p><strong>Spoiler: we don’t always mean the CMS.</strong></p>

<p>If you’re reading this article, there’s a high chance you're acquainted with Umbraco, but perhaps you're not fully aware of the nuance that comes with the phrase. So what exactly do we mean when we say “Umbraco”?</p>
<p>The term encompasses more than just a CMS; it embodies a rich, open-source ecosystem consisting of the Umbraco CMS, the vibrant Umbraco Community, and our custodian and driving force, Umbraco HQ. Let's delve into all things “Umbraco” to uncover the multifaceted meanings and explore the intriguing interplay between these components.</p>

<h2>“Unbrako” the contraption</h2>

<p>Unbrako is the Danish name for an Allen key - that’s a “hex wrench” if you want the <em>real</em> name - and although to most of you reading this article this one is unlikely to cause any confusion Umbraco HQ say they do <a href="https://umbraco.com/about-us/why-are-we-called-umbraco/">occasionally get Allen-key purchase requests</a>. But that’s not <em>really</em> what this article is about!</p>

<p><img src="/media/kjcnid4c/anton-savinov-edhzouemhxo-unsplash-cropped.jpg" alt="Hex wrenches of multiple sizes hanging on a wall, surrounded by other tools" />
<em>Photo by <a href="https://unsplash.com/@tonchik?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Anton Savinov</a> on <a href="https://unsplash.com/photos/a-bunch-of-tools-are-hanging-on-a-wall-EDhZOuEMHXo?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></em>  </p>

<h2>“Umbraco” the CMS</h2>

<p>According to Umbraco HQ (we’ll get onto that!):</p>

<blockquote>
  <p>“Umbraco is a CMS that’s fully extendable, highly customizable, and user-friendly.”</p>
</blockquote>

<p>Umbraco CMS is the product all this “Umbraco” hangs off of. Created in 1999 by Niels Hartvig and open sourced in 2005, Umbraco has undergone significant transformations, evolving from a basic CMS to a sophisticated platform that powers websites of all sizes.</p>

<p>Umbraco has always (at least since I’ve been around) been pitched as <a href="https://umbraco.com/about-us/values/why-the-friendly-cms/">“The Friendly CMS”</a> which, as well as hinting at it’s user- and developer-friendliness and open-source nature, indicates how welcoming the community is. Community is a large part of what makes Umbraco, Umbraco. But it’s sometimes important to make the distinction between the two.</p>

<p><img src="/media/qwvd2tz5/cms.jpg" alt="A photo by Umbraco HQ on Flickr of large 3D letters &quot;CMS&quot; at Codegarden." /></p>

<h2>“Umbraco” the community</h2>

<p>The first Codegarden in 2005 is our first indication of the beginnings of the Umbraco Community and it’s grown hugely from there. Communities often form around open source projects as people from across the world problem-solve and learn together to produce something that benefits themselves and other users, all with a shared philosophy for free (as in beer and as in speech) software. But I’ve heard <a href="https://umbracocommunity.social/@emaburst">Emma Burstow, Director of Developer Relations at Umbraco HQ</a>, proudly state that people refer to Umbraco as “the one with the community”, which surely shows we have something special going on!</p>

<p>The Umbraco Community is also set apart from other software communities because of who make up our community. In a recent conversation with <a href="https://umbracocommunity.social/@carlcod_es">Carl Sargunar</a>, he pointed out:</p>

<blockquote>
  <p>“Very unusually Umbraco isn’t a community of developers, it’s a community of people who use the platform”</p>
</blockquote>

<p>The Umbraco Community is so much more than the occasional developer making the odd code contribution - it’s a lifestyle choice! And I only mean that partially as a joke! Codegarden, community-run conferences, <a href="https://codecab.in">unconferences</a>, <a href="https://www.meetup.com/pro/umbraco/">meetups</a>, gaming sessions, social media, podcasts, a whole <a href="https://skrift.io">digital magazine</a>, <a href="https://umbracocommunity.social">a federated social network instance</a>, and even daily Wordle-sharing mean we’re never far away from our friends in the community.</p>

<p>Although largely developers, the community very actively includes agency owners, account and project managers, CMS administrators, end users and Umbraco HQ employees - the Umbraco Community is more about the lifestyle surrounding Umbraco than the code.</p>

<p><img src="/media/lujkwaxm/mvps.jpg" alt="A photo by Umbraco HQ on Flickr of MVPs and a couple of HQers on stage at Codegarden 2023, celebrating the announcements of the new MVPs" /></p>

<p>Repeat <del>offenders</del> <ins>contributors</ins> are acknowledged by Umbraco HQ through the <a href="https://mvp.umbraco.com/">MVP programme</a>.</p>

<p>Umbraco community members call each other “Umbracians” (<em>um-brak-ians</em> or <em>umb-raishe-ans</em>).</p>

<h2>“Umbraco” the company</h2>

<p>Umbraco, Umbraco HQ, or just “HQ” is the name we use for the company, Umbraco A/S (and Umbraco, LLC in the US). HQ are the maintainers of the Umbraco CMS but also own the Umbraco commercial products Cloud, Heartcore, Forms, Commerce, Deploy, Workflow and UI Builder. They also monetize Umbraco through the sale of support plans.</p>

<p>HQ hires a developer relations team including developer advocates (also <a href="https://medium.com/@unicodeveloper/the-birth-of-developer-avocados-61e4ac235033">called developer avocados 🥑</a>) whose role is to encourage and grow the community.</p>

<p><img src="/media/b4dhfdyy/hq-ers.jpg" alt="A photo by Umbraco HQ on Flickr of a group photo of HQ employees from Codegarden 2022. They have their hands raised in the air and are smiling and cheering." /></p>

<p>Members of staff at HQ are “HQers”.</p>

<p>Just to confuse things even more “HQ” can also refer to the <a href="https://umbraco.com/contact-us/">physical office locations of Umbraco A/S and Umbraco, LLC</a>.</p>

<h2>Where paths cross</h2>

<h3>Community recognition</h3>

<p>It surprises me how often a community-run conference, meetup or package gets attributed to Umbraco HQ or assuming organisers, speakers or community teams members are paid for their efforts.</p>

<p>Although I consider open-source as part of my job (and I’m lucky enough to have an employer who agrees), even I pour more emotional and time effort into community contributions than I do into the rest of my day job (it’s hard to get too emotionally invested in C#, eh?). And I’m one of the lucky ones: many community members are dedicating evenings and weekends to make their contributions.</p>

<p>The community can benefit itself by acknowledging non-profit community effort. Acknowledgement and appreciation go a long way to making a contribution feel worthwhile.</p>

<p><img src="/media/txxj0sws/shield.jpg" alt="A photo by Umbraco HQ on Flickr of a man holding a Captain America-style shield with an orange Umbraco logo in the centre" />
<em>Not all heroes wear capes, but this one <strong>does</strong> have a shield. Photo by <a href="https://www.flickr.com/photos/149619560@N07/52993707730/">Umbraco HQ on Flickr</a>.</em></p>

<p>But also, <a href="https://engineering.atspotify.com/2022/10/open-source-work-is-work/">open source <em>is</em> work</a> and employers who profit from the Umbraco CMS and community should do more to support their own employees’ contributions as well as sponsor open source packages and community events that directly benefit their business. Umbraco co-founder, community member and Director at Docker, <a href="https://twitter.com/pploug">Per Ploug</a>, has <a href="https://engineering.atspotify.com/2022/10/open-source-work-is-work/">written about this</a> and his <a href="https://www.youtube.com/watch?v=QAttjA8brVs">Codegarden talk is available to watch online</a>.</p>

<h3>Money for “nothing”</h3>

<p>Umbraco CMS, being open source, is free to anyone who wants to download and use it. This makes things difficult for open source maintainers, like Umbraco HQ, who need to sustain a business while giving their primary product away for free.</p>

<p><img src="/media/0qtdu0st/free-candy.jpg" alt="A photo by Umbraco HQ on Flickr of Joe helping himself to sweets at Codegarden, next to a sign reading “Candy bar - Self-service”" />
<em>Rumour has it people like free things. Photo by <a href="https://www.flickr.com/photos/149619560@N07/52993834798/">Umbraco HQ on Flickr</a></em>.</p>

<p>Since 2021 Umbraco HQ has been owned by Monterro, a growth investor. Open source maintenance is an unusual business model, and since the Monterro investment, we’ve seen a larger push for commercial offerings from Umbraco but there remains a huge focus on open-source offering and the community.</p>

<p>Traditionally, <a href="https://www.forbes.com/sites/forbestechcouncil/2023/02/02/how-to-make-open-source-profitable/">open source maintainers make money by providing services</a> and Umbraco HQ is no different. HQ sell Umbraco Cloud as a <abbr title="Software As A Service">SAAS</abbr> offering along with additional premium packages and support. Although Umbraco tends to avoid these situations, this business model can result in priorities misaligning with the community.</p>

<h3>Community as a Commodity</h3>

<p>It’s sometimes important to attach a business case to the value of a community to ensure a business like Umbraco HQ can continue to invest in their communities. And as <a href="https://twitter.com/umbraco/status/1714197052735832199">pointed out by Emma Burstow</a>, businesses are finally starting to see value in community because</p>

<blockquote>
  <p>“People are less inclined to buy things from sales people and far more likely to buy things from people like them”.</p>
</blockquote>

<p><img src="/media/yy4i5pyj/rabbits-with-bats.jpg" alt="A photo by Umbraco HQ on Flickr of two people in rabbit costumes holding up a board covered in “bats” including a jar of rubber bat toys, a model Batmobile, a padel racket and two ping-pong paddles." />
<em>Are bats on a board a “commodity”? Photo by <a href="https://www.flickr.com/photos/149619560@N07/52992756002/">Umbraco HQ on Flickr</a>.</em></p>

<p>However, the attitude of community members and their contributions being seen as commodities can be difficult for community members.</p>

<blockquote>
  <p>“Venture capitalists are actually using community activity as one of the metrics that they’re going to measure against before they acquire companies”</p>
</blockquote>

<p>Contributions aren’t made to keep Umbraco HQ valuable and afloat, they’re made for the betterment of the open-source product, to encourage the progression of individuals within the community and to grow the community itself. But in doing so, contributors provide value to HQ. And although we all see value in Umbraco HQ doing well, a volunteer contribution benefiting a profitable company can feel uncomfortable.</p>

<p>It is important to comprehend that businesses understanding the value of community does mean that they can justify investing more time and money into fostering the community. And Umbraco HQ does put a lot of time and effort into fostering the Umbraco Community: Umbraco now has a dedicated developer relations team and fund community-run events. A strong community (as a commodity) can ensure their stewardship of the open source portions of the platform for many more years to come.</p>

<h3>Accountability</h3>

<p>“<em>Stewardship</em>” is an important word there. Umbraco HQ own the trademarks around the name Umbraco, but the CMS itself is in the public domain. Under <a href="https://www.tldrlegal.com/license/mit-license">the MIT License</a>, people are free to “use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies” of Umbraco CMS. If Umbraco HQ ceased to be, or made a questionable product decision, the Umbraco CMS would likely continue to exist under a different name, possibly along with the community.</p>

<p>This has happened before, with the <a href="https://umbraco.com/about-us/what-happened-to-version-5/">death of Umbraco 5</a>, members of the community split off from Umbraco and created <a href="https://github.com/RebelCMS/rebelcmsxu5">RebelCMS</a>. The CMS was short-lived, with the last commit now almost 12 years ago, but it does show that it can be done.</p>

<p>This is an important thing to remember because, although it’s very unlikely to happen, it makes Umbraco HQ accountable to the community - they can’t do something strongly against what the community wants and need to listen to the community’s requests to keep their stewardship. This fact helps to ensure that community-effort, although benefiting Umbraco HQ, means that HQ’s profitability benefits the community, in a circular way.</p>

<h2>A delicate balance</h2>

<p>I don’t mean for this article to cause any controversy, but believe the discussion around profitability and open source is an important one and needs to be had out in the open. The Umbraco CMS can continue to provide a valuable product for both Umbraco HQ and the Umbraco Community, but it’s a delicate balance and as a community we need to acknowledge where conflicts of interest may arise.</p>

<p>As contributors we should know who our contribution benefits, and ensure we’re getting enough value for ourselves from that interaction to prevent feeling cheated or burning out.</p>

<p>It’s also vital that Umbraco HQ continue to recognise the value of the community in steering the direction of the Umbraco CMS.</p>

<p>Together, we’ve build something great and are continuing to build something even better.</p>

<p><img src="/media/qs4fokz5/cg-bingo.jpg" alt="A photo by Umbraco HQ on Flickr of Codegarden 2023 attendees sat at long tables, with hands raised in the air ready to play bingo!" /></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Azure DevOps Pipelines breaks my &quot;additional arguments&quot; when using Deploy to Azure</title>
<link>https://joe.gl/ombek/blog/pipelines-breaks-deploy-to-azure-additional-parameters/</link>                <pubDate>Thu, 16 Nov 2023 12:21:55 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/pipelines-breaks-deploy-to-azure-additional-parameters/</guid>
                <description>Recently, something in the Azure DevOps Pipelines &quot;Deploy to Azure&quot; flow has been messing up parameters in the &quot;additional arguments&quot; field under &quot;Additional Deployment Options&quot;.</description>
                <content:encoded><![CDATA[
                    <p>Recently, something in the Azure DevOps Pipelines "Deploy to Azure" flow has been messing up parameters in the "additional arguments" field under "Additional Deployment Options".</p>
<p>We historically had parameters to skip the <code>umbraco</code> and <code>umbraco_client</code> folders:</p>

<pre><code>-skip:objectname=filePath,absolutepath="\\(umbraco|umbraco_client)\\" -skip:objectname=dirPath,absolutepath="\\(umbraco|umbraco_client)\\"
</code></pre>

<p>However, somewhere along the lines, this was being corrupted to produce the following command:</p>

<pre><code>msdeploy.exe [...] -skip:objectname=filePath,absolutepath="\\\(umbraco|umbraco_client)\\" -skip:objectname=dirPath\,absolutepath=\\(umbraco|umbraco_client)\\
</code></pre>

<p>And thus, the error:</p>

<pre><code>##[error]Error: 'umbraco_client)\\' is not recognized as an internal or external command, operable program or batch file.
</code></pre>

<p>It looks like Pipelines is doing <em>some</em> of the work to escape backslashes (<code>\</code>) and double quotes (<code>"</code>) itself but getting confused, removing some, and adding them back in all the wrong places!</p>

<p>We tried various variations of removing and escaping with different characters, trying to reverse-engineer what Pipelines was doing to our simple text string until <a href="https://ie.linkedin.com/in/fnmenezes/en">Fernando</a> suggested removing the (required) quotes altogether, which worked!</p>

<p>Additional arguments:</p>

<pre><code>-skip:objectname=filePath,absolutepath=\\(umbraco|umbraco_client)\\ -skip:objectname=dirPath,absolutepath=\\(umbraco|umbraco_client)\\
</code></pre>

<p>Generated command:</p>

<pre><code>msdeploy.exe [...] -skip:objectname=filePath,absolutepath="\\(umbraco|umbraco_client)\\" -skip:objectname=dirPath,absolutepath="\\(umbraco|umbraco_client)\\"
</code></pre>

<p>If anybody has any further insight into this, please do to <a href="/ombek/contact/">get in touch via social media</a> - I'd love to expand on the details here.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbrastodon: the Umbraco Community in the Fediverse and on Mastodon</title>
<link>https://joe.gl/ombek/blog/umbraco-in-the-fediverse/</link>                <pubDate>Fri, 15 Sep 2023 02:29:11 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/umbraco-in-the-fediverse/</guid>
                <enclosure url="https://joe.gl/media/skcovtg2/dall-e-2023-09-15-13-40-24-a-unicorn-and-a-mastodon-floating-in-space.png" length="2889202" type="image/png" />
                <description>A guide to getting started with Mastodon and the Fediverse tailored to the Umbraco Community.</description>
                <content:encoded><![CDATA[
                    <p>A guide to getting started with Mastodon and the Fediverse tailored to the Umbraco Community.</p>
<p>Following on from discussions around how distributed our online communities have become since the ongoing fall of Twitter/X on <a href="https://owain.codes/blogs/2023/september/where-is-the-umbraco-community/">Owain's blog</a> and <a href="https://www.youtube.com/live/uIxj2gzMqSQ?si=5kARiGdX1jsW9rQr&amp;t=1817">UmbraCoffee #292</a>, Mastodon has been raised as one of the locations the community is slowly moving to. As an established Fediverse user, and admin of the <a href="https://umbracocommunity.social">UmbracoCommunity.social</a> Mastodon instance, some people have been asking me Mastodon questions and I thought a blog post might be a good way to communicate these tips to a wider audience.</p>

<p>I've <a href="https://umbracocommunity.social/@joe/111022977851392865">mentioned before</a> how I hope Federation is the future of social networking and how <a href="https://umbracocommunity.social/@joe/111062571685624450">Mastodon may well fit the needs of the Umbraco Community going forwards</a>, and I truly believe we need something analogous to "old" Twitter, that's less instant than chat tools such as Discord, more social than the Forums and shorter form than blog posts. Mastodon can be just that.</p>

<h2>What is the Fediverse?</h2>

<p>This blog post is tailored to the Umbraco Community, so I won't go into all the details here, but will keep to the relevant parts.</p>

<p>The Fediverse is generally used to refer to a group of servers that conform to the ActivityPub standard. Each of these servers pushes content out to the Fediverse and wider internet. The content could be blog posts, microblogs, photos, comments or anything else we as people create on the internet.</p>

<p>These servers within the Fediverse are subcategorised by the software they run. The software is generally analogous with a "traditional" social media platform. For example, Mastodon focusses on microblogging (think Twitter), Pixelfed is all about social image sharing (think Instagram) and Lemmy hosts communities (think Reddit). You can follow a Lemmy community or Pixelfed account from your Mastodon account too - because they all use the same protocol.</p>

<h2>What is Mastodon?</h2>

<p>I've explained how Mastodon is the ActivityPub-compliant software running on a server., but the concept is much greater than that! Because the ActivityPub protocol allows these servers to talk to one another and because Mastodon is free and open source, you don't have to sign up to Mastodon's own server - you can join a community (<em>server</em> or <em>instance</em>) of your choice.</p>

<p>This can be a barrier to entry for those unsure about how Mastodon works, but in practice this architecture helps us build a more resilient and tailored social media platform.</p>

<p>Because Mastodon is distributed across multiple servers, usernames take the format <code>@[local-username]@[server-domain]</code>, for example, my username is <a href="https://umbracocommunity.social/@joe">@joe@umbracocommunity.social</a>.</p>

<h2>Why does it fit so well with the Umbraco Community?</h2>

<p>I've already used the phrases "open source" and "communities" when describing Mastodon, and this is a large reason I believe Mastodon could be a large part of the future Umbraco Community: our <abbr title="ethoses">ethea</abbr> align.</p>

<p>An Umbraco-focusses Mastodon instance allows Umbraco to be the primary focus of an instance with additional Umbraco-related content easier to discover for users. But the way that federation works, means that the common interests of Umbracian's on the Umbraco Mastodon also become discoverable by the community, such as .NET, open source and Lego.</p>

<h2>How do I join?</h2>

<p>So, if you want to become a part of the Fediverse, you first choose a server. It's not the be-all and end-all if you change your mind later, you can move (although your <del>Tweets</del> <ins>posts (or "Toots")</ins> don't come with you) your account to a different server at a later date (see below).</p>

<h3>Which server(s)?</h3>

<p>Mastodon instances tend to cater to interests. Although there are generic instances too.</p>

<p>The more generic and larger your instance, the closer to a Twitter experience you get. But that's not where the power of Mastodon lies, in my opinion you're better off joining a niche.</p>

<p>Personally, I find <a href="https://umbracocommunity.social">UmbracoCommunity.social</a> a fantastic compromise - the community is niche enough to mean the public feed (see "Getting the most out of Umbrastodon") is relevant to me but not so small as to limit it, although as an admin there, I may be biased!</p>

<h4>Popular interest-specific servers within the Umbraco Community</h4>

<p>If a lot of what you do online is techy, these instances may work better for you:</p>

<ul>
<li><a href="https://umbracocommunity.social">UmbracoCommunity.social</a> is a Mastodon server set up specifically for the Umbraco Community and is hosted by <a href="https://umbracocommunity.social/@steve_gibe">Steve Temple</a></li>
<li><a href="https://hachyderm.io">Hachyderm</a>, pronounced hack-a-derm (that's right, it's a mastodon/pachyderm pun!) is a hugely popular generic-tech instance</li>
<li><a href="https://fosstodon.org">Fosstodon</a> is dedicated to open source software</li>
<li><a href="https://infosec.exchange">Infosec.exchange</a> for you infosec folk</li>
<li><a href="https://front-end.social">front-end.social</a> for you frontenders</li>
</ul>

<p>You can also find your own niche by checking the <a href="https://joinmastodon.org/servers">Mastodon server listing</a> where you can filter by language, hosting country and interest.</p>

<h4>Popular generic instances</h4>

<p>From the UmbracoCommunity.social statistics, here are the most popular generic instances used by the Umbraco Community:</p>

<ul>
<li><a href="https://mastodon.social/">Mastodon.social</a> is the original server operated by Mastodon, its hugely popular and a great place to start if you can't find a niche</li>
<li><a href="https://mstdn.social">mstdn.social</a> another generic instance with a shorter URL</li>
<li><a href="https://mastodon.green">Mastodon.Green</a> a paid-for server who run off renewable energy and plant trees with 20% of the monthly fee</li>
</ul>

<h4>Host your own</h4>

<p>If none of these take your fancy, you can also host your own instance and be in full control of your own data - <a href="https://cultiv.social/@sebastiaan">Seb has a server just for his account</a>. <a href="https://masto.host/">Mastohost</a> is a simple and affordable way to set up your own instance. Be aware, though, it can get lonely on your own - you lose a lot of discoverability by being on a single-person server.</p>

<h2>Already a Mastodon/Fediverse user?</h2>

<p>If you're already in the Fediverse but want more Umbraco in your feed, here are some options:</p>

<ul>
<li>Use your existing Mastodon account and follow whoever you want from UmbracoCommunity.social by <a href="https://umbracocommunity.social/directory">looking at the directory to find accounts</a></li>
<li>Create a second Mastodon account on <a href="https://umbracocommunity.social">UmbracoCommunity.social</a> if you want to keep your
Umbraco Fediverse account separate from your other account</li>
<li><a href="https://docs.joinmastodon.org/user/moving/">Migrate your account</a> to <a href="https://umbracocommunity.social">UmbracoCommunity.social</a>. Your old account gets redirected, but beware your old posts will stay put.</li>
<li>Make use of the
public feeds on UmbracoCommunity as it’s quite a good mix of Umbraco
and .NET devs: <a href="https://umbracocommunity.social/public/local">the local feed (all posts on UmbracoCommunity instance)</a> and <a href="https://umbracocommunity.social/public">the federated feed (all posts from people followed by anybody on the UmbracoCommunity instance)</a>. Some mastodon clients let you add these feeds into your app too!</li>
</ul>

<h2>Getting the most out of Umbrastodon!</h2>

<ul>
<li>Follow the #Umbraco hashtag (you can follow hashtags as well as people on Mastodon!)</li>
<li>As above, <a href="https://cultiv.social/@sebastiaan/111062300610264145">Seb suggests</a> making full use of the use of the <strong>public feeds</strong> to find relevant content.</li>
<li>Make use of a <strong>browser plugin</strong> (like <a href="https://graze.jaredzimmerman.com/">Graze for Mastodon</a>) to make your web browsing experience of Mastodon easier</li>
<li>Find <strong>apps</strong> that works for you - I personally like <a href="https://apps.apple.com/app/ice-cubes-for-mastodon/id6444915884">Ice Cubes on iOS</a> and <a href="https://elk.zone">Elk for desktop</a> but <a href="https://joinmastodon.org/apps">the app directory has loads of options</a></li>
<li><strong>Follow me!</strong> I'm <a href="https://umbracocommunity.social/@joe">@joe@umbracocommunity.social</a> on Mastodon and <a href="https://expl.red/@joe">@joe@expl.red</a> on Pixelfed.</li>
</ul>

<p>See you in the Fediverse!</p>                ]]></content:encoded>
            </item>
            <item>
                <title>CODECABIN: What Nerds do in the Woods</title>
<link>https://joe.gl/ombek/blog/codecabin-23/</link>                <pubDate>Thu, 14 Sep 2023 09:53:19 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/codecabin-23/</guid>
                <enclosure url="https://joe.gl/media/kcnlbn5n/20230910_101109.jpg" length="1180781" type="image/jpg" />
                <description>22 Umbraco community members in a luxury cabin nestled into a hillside in the UK&#39;s Peak District National Park talking all things Umbraco over a long weekend.

A summary of my experiences at CODECABIN &#39;23.</description>
                <content:encoded><![CDATA[
                    <p>22 Umbraco community members in a luxury cabin nestled into a hillside in the UK's Peak District National Park talking all things Umbraco over a long weekend.</p>

<p>A summary of my experiences at CODECABIN '23.</p>
<h2>What is CODECABIN?</h2>

<p>According to the <a href="https://codecab.in/">CODECABIN website</a>, it's "An Umbraco Event Like No Other".</p>

<blockquote>
  <p>CODECABIN is the premier, invite-only weekend away for Umbraco
  developers, providing time to code, learn and network in a completely
  relaxed and open environment away from the hustle and bustle of every
  day life.</p>
</blockquote>

<p>Which is a pretty good summary.</p>

<p>We hacked on Umbraco related packages; played with new Umbraco features; discussed all things Umbraco past, present and future; played games; enjoyed the scenery; and, boy, did we eat <em>well</em> (thank you again <a href="https://x.com/lucybrailsford">Lucy</a>!)</p>

<figure>
<img alt="Joe queueing up to get his bowl of delicious gelato in the CODECABIN kitchen. There are 5 ice cream flavours to choose from and Joe already has one scoop in his bowl!" src="/media/21xhez1e/pxl_20230909_182433096-mp.jpg?width=1300" />
<figcaption>
<p>Food featured some fabulous local gelato</p>
<small>Photo by <a href="https://owainjones.dev/">Owain Jones</a></small>
</figcaption>
</figure>

<h2>What happened this year?</h2>

<p>This year's programme included 4 discussion slots (which worked a little differently to previous years), dedicated hacking time, a walk to the nearby Froggatt Edge viewpoint as well as a quiz and awards night!</p>

<h2>What did we talk about?</h2>

<p>This year the programme included slots to discuss items from our "discussion backlog" where in the weeks leading up to CODECABIN we has contributed suggested discussion topics to and voted on our favourites. In each slot, we'd discuss the most popular topic remaining to be discussed until it came to a logical conclusion before moving onto the next most popular topic on the list. This is different to previous years but seemed to work well with Matt and Lee's guidance setting up the next topic and suggesting when may be good to move on.</p>

<figure>
<img alt="Joe, Rachel Breeze, Mike Macey, Laura Weatherhead and Andy Boot sat on a long L-shaped sofa beady to start discussing." src="/media/a00nm2en/img_7291.webp?width=1300" />
<figcaption>
<small>Photo by <a href="https://x.com/ravimotha">Ravi Motha</a></small>
</figcaption>
</figure>

<p>Here are my takeaways from each discussion we had.</p>

<p><em>(Please note: these are my takeaways from each topic, not necessarily the "correct" takeaways or what the group as a whole took from the discussion.)</em></p>

<h3>Headless is the new black</h3>

<p>Discussion centred around static site generation and single-page JavaScript applications (SPAs) using the new headless Umbraco Content Delivery API as well as non-web applications for the API, such as mobile apps.</p>

<p>Some had reservations about the application of headless sites being used for SPAs unnecessarily but the performance and sustainability benefits of static sites were also highlighted.</p>

<h3>Segments</h3>

<p>In this session we discussed some confusion and potential UI bugs for the non-language variant variants, segment variants. The experience feels less polished than the language variant journey and lack of up-to-date guide made things challenging for some developers. Although the <a href="https://24days.in/umbraco-cms/2022/using-segmentation/">2022 24 Days article</a> was still useful to help get things started, some cheese had been moved.</p>

<p>We looked at a sample implementation and discussed ways of improving it.</p>

<p>Segmentation seemed to be on many developers radar, but it seems to be underutilised except by uMarketingSuite so far.</p>

<h3>Front-end frameworks for websites (not web apps)</h3>

<p>The discussion began as a search for lighter-weight frontend frameworks than Angular, Vue or React and more in line with how we might've used jQuery, jQuery UI and jQuery plugins in the past. The consensus was that these larger frameworks are more suited to "JavaScript applications" and are too much for many "lighter sprinklings" of JavaScript.</p>

<p><a href="https://alpinejs.dev/">Apline.js</a> was discussed as a lightweight framework which allows for considerably less JS to be written since a lot of Alpine is applied by adding attributes to elements.</p>

<p>However, general consensus seemed to be that vanilla (frameworkless) JavaScript was incredibly capable these days, particularly for selecting elements by selector (<code>document.querySelector</code> and <code>document.querySelectorAll</code>) and <code>fetch</code> in place of AJAX.</p>

<p>For interactive components, people suggested finding vanilla, single-purpose libraries (e.g. one for a carousel and another for tabs) which should be checked for accessibility and maintainability on an individual basis. The use of these components is much easier using JS modules or compiling modules using a JS bundler like WebPack or Vite.</p>

<p>Vue was suggested as the lighter of the "full-fat" frameworks and it was highlighted that using these larger frameworks for a single module within a larger site may be appropriate - the whole site doesn't have to be Vue, perhaps just a smaller interactive component.</p>

<h3>Deployment basics</h3>

<p>We talked about the lack of documentation around deployment using Azure Pipelines and Github Actions. Although it's potentially something that sits outside the realms of the Umbraco Documentation, it was agreed it'd be helpful. I believe there is some draft documentation in the works, so we look forward to seeing that in the future.</p>

<h3>Build a better back office</h3>

<p>Discussion mostly lead to ways of improving the default dashboard to make it more easily customisable for users and developers. The concept of customisable widgets which a user can add and remove from their own dashboard was suggested by the group where these widgets could be more easily generated by developers. The widgets may also be able to be added to content apps with extra context. Although we believed this would be a fantastic addition to Core, Markus suggested he might add some of these improvements to his <a href="https://marketplace.umbraco.com/package/our.umbraco.thedashboard">The Dashboard package</a>.</p>

<h3>Current state of package development</h3>

<p>Concerns were raised by package developers about the skill gap and effort involved in converting packages for the new backoffice (expected in v14). Additional support for package developers from HQ would be welcomed.</p>

<p>The group suggested that the community as a whole wouldn't be "expecting" developers of free, open-source packages to be ready for v14 on release date and that a pace to suit developers is the most appropriate.</p>

<p>After CODECABIN, <a href="https://umbracocommunity.social/@lee/111052939360457283">Lee announced that he was no longer planning to release any version of Contentment for Umbraco 14, instead choosing to aim for v15's release</a>.</p>

<p>Another concern was that package development may have slowed in newer versions. Some suggested reasons for this included:</p>

<ul>
<li>developers don't want to build packages only to rebuild the frontend in v14 (see Web components, below)</li>
<li>duplicate/similar packages are less commonplace than they used to in earlier versions, one package has come out on top and the similar packages have died off</li>
<li>we've refined the set of packages we use as a community</li>
</ul>

<h3>Web Components</h3>

<p>Web components were a running theme throughout a few sessions. Many developers were daunted by them and they were briefly explained within the context of the backoffice and packages (with Lit) as well as other uses for them.</p>

<p>I suggested that it's possible to develop a new package in current Umbraco versions as a web component, with only the Angular-to-web-component layer that would need replacing in the v14 backoffice. This is a subject <a href="https://elk.zone/umbracocommunity.social/@jason">Jason Elkin</a> is planning to talk about at upcoming Umbraco events. I'll update this post when more information becomes available.</p>

<p>I also demo'd an obscure use case of web components and the shadow DOM as an example of how web components can be used which I will also be talking about at Upcoming Umbraco events and will update this post when more information becomes available.</p>

<h3>Grading and categorising community blog posts</h3>

<p>(originally "<strong>How can a 'best practice' Umbraco demo site be created?</strong>")</p>

<p>This one went a little off-topic! Although we were aware of many existing “best practice” starter kits and demo sites, we realised it was actually quite difficult to create a best practice site for all occasions - even taking opinion out of the equation, we don't have one way of building a site even for ourselves. If performance isn't a priority but being flashy is, we'll architect and code a site completely differently to one where performance is the be all and end all.</p>

<p>We then started to think that perhaps marking content as "beginner" and "advanced" may help to distinguish a generic "best practice" from a "best practice for lightning performance" or "best practice for <em>[insert niche here]</em>".</p>

<p>We also thought that standardising categories and hashtags would help with this problem, as well as discovery - another issue with the existing “best practice” sites.</p>

<p>A common set of categories would also allow the <a href="https://community.umbraco.com/learn-about-the-community/blog-posts/">Umbraco Community Blog Posts page</a> to be filterable, making it much more useful.</p>

<p>Discussed categories were "beginner", "advanced", "content editors", "developers", "accessibility" and "performance" as well as version-specific classifications. But no documentation has been generated as of yet. I'll update this blog post if something does.</p>

<h3>Taming AI</h3>

<p>This discussion was mostly about utilising generative AI (OpenAI's ChatGPT and GitHub CoPilot in particular) for software development purposes. Although many had not used AI much for software questions, those that had noted how the answers were often a good <em>starting point</em> but often needed correction. In fact, <a href="https://www.theregister.com/2023/08/07/chatgpt_stack_overflow_ai/">a study was referenced where the researchers found that 52% of ChatGPT's answers to code questions were incorrect</a>.</p>

<p>Developers should use AI generated code with caution and check code samples even more thoroughly than those found on forums. It was, however, agreed that developers using AI would likely be more productive and that we should be embracing this technology.</p>

<p>I mentioned that <a href="https://summit.umbraco.com/program/">my talk "How to Copy &amp; Paste" at the Umbraco US Summit</a> may be pertinent, and I will share a recording of it when I have one.</p>

<h3>Umbraco Builds - LTS or STS?</h3>

<p>Deciding which version to put a client on is never a straight answer!</p>

<p>Some newer STS (Short Term Support) releases go into the "security phase" of support at about the same time as their most recent LTS (Long Term Support) version, so is it worth sticking to the LTS version or providing the latest and greatest? For example, v12 (STS) enters its security phase on 29 March 2024, just 2.5 months before v10 (LTS) enters its security phase on 16 June.</p>

<p>"Does the client even require LTS support?" is the first question. If you have a client on retainer including regular upgrades, it may prove easier to always be on the latest.</p>

<p>If a build is going to last a long time, we generally start the client on the current latest (or even preview) and upgrade them as the project goes on - stopping at the LTS if nescessary.</p>

<p>Of course, if a client needs a new feature (Content Delivery API, for example), it's rarely worth them holding back to the previous LTS. You'd probably start on the latest STS and factor the upgrade to the next LTS into the cost, if an LTS version is even required.</p>

<p>It was understood that upgrades from LTS to LTS are possible (without upgrading to intermediate STS versions), but it was raised that community and even official Umbraco packages may not offer the same support. It was suggested to HQ employees that for consistency this should be the case for official packages, particularly those that come with Umbraco Cloud such as Forms and Deploy.</p>

<h3>Multi-sites and shared content</h3>

<p>This discussion was around the <a href="https://github.com/umbraco/rfcs/blob/0025-reusable-content-with-global-blocks-2/cms/0025-reusable-content-with-global-blocks.md">Reusable Content with Global Blocks RFC (Request For Comments)</a> and having reusable content within an Umbraco site. In particular, consensus seemed to coincide with <a href="https://github.com/umbraco/rfcs/discussions/35#discussioncomment-6088921">Matt's comments on the RFC that global content could be nodes in their own right, not just blocks</a>.</p>

<p>Reusable nodes could include anything that does not logically fit into the tree structure, such as office locations or team members. This repository pattern is seen in other CMS'.</p>

<p>You could even have blog posts as repository items to more easily replicate a WordPress-style URL structure like <code>/blog/2022/09/14/codecabin-23</code>, since WordPress also has a repository structure for blog posts. Where some Umbraco blog solutions require blog posts to be manually sorted into a date-based folder structure.</p>

<p>We discussed that having multiple library tabs (/applications) in the navigation bar may be desirable (e.g. Blocks, Blog) similar to how Konstruct works.</p>

<p>It was also suggested that media should be a library type as well, since it's essentially the same thing.</p>

<h3>Other sessions</h3>

<p>There was also shorter sessions on "<strong>Optimising and caching Umbraco websites</strong>" and "<strong>Umbraco server beyond .NET</strong>" as well as a <strong>show &amp; tell</strong> session but I have little to comment on from these.</p>

<h2>What was I hacking on?</h2>

<p><a href="https://elk.zone/umbracocommunity.social/@matt">Matt</a>, <a href="https://elk.zone/umbracocommunity.social/@rachelbreezedev@geekdom.social">Rachel</a> and I started work on an ActivityPub integration for Umbraco to allow for blog posts created in Umbraco sites to be pushed out to ActivityPub servers (like Mastodon) as posts to aid in their sharing. WordPress has a plugin to do this, and we'd like to release an Umbraco package in the future. ActivityPub comments could also be a future enhancement. We also realised this integration could generate RSS feeds and trigger <a href="https://www.indexnow.org/">IndexNow</a> requests in the future. I'll update this post when something becomes public.</p>

<h2>What else did we do?</h2>

<p>It couldn't all be Umbraco-talk now could it!?</p>

<p>We were kept well fed and watered (of the regular, cola, hop and juniper-flavoured varieties) too, with some great discussions over the bar and dinner table (writing the script to <em>Bloodswarm: Revenge of the Gnats</em> comes to mind!)</p>

<p>A keen crew got up for a run every morning although I decided I ought to stick to my rule of one 5km run per year (at CodeGarden), as is tradition.</p>

<p>But I did attend the walk with a large group. Although only a few kilometers, the walk was spectacular and allowed us to see the cabin from the scenic Froggatt Edge on the other side of the valley, as pictured.</p>

<figure>
<img alt="A group photo of those who went on the walk. We're stood on a large flat boulder at the top of Froggatt Edge, with a view of the patchwork of green fields in the valley below." src="/media/wmsfsss5/20230910_101232.webp?width=1300" />
<figcaption>
<p>The view from the top</p>
<small>Photo by <a href="https://github.com/jacksorjacksor/">Richard Jackson</a></small>
</figcaption>
</figure>

<p><a href="https://www.karltynan.co.uk/">Karl</a> hosted us a fantastic <del>children's birthday party</del> <ins>pirate themed quiz</ins>, in which we came last but with a smile on our faces and a rather respectable score of 69.</p>

<figure>
<img alt="Delia, Laura, Matt, Joe and Bjarne - all in rather fetching CODECABIN t-shirts - sat on a sofa studying the picture quiz round." src="/media/21ybdw5w/pxl_20230909_193000071.webp?width=1300" />
<figcaption>
<p>Last place, but enjoying ourselves</p>
<small>Photo by <a href="https://www.karltynan.co.uk/">Karl Tynan</a></small>
</figcaption>
</figure>

<p>Inspired by the pirate music used in the quiz, there was also an impromptu "Pirates of the Carribean" movie night (can you believe <a href="https://www.linkedin.com/in/delia-b-51429a17b/">Delia</a> has never seen it?!)</p>

<p><a href="https://linktr.ee/lottepitcher">Lotte</a> hosted the fabulous CODECABIN Awards evening, the 6 awards I earned proudly displayed on my shelves behind my desk!</p>

<p><a href="https://linktr.ee/lottepitcher">Lotte</a> also provided entertainment throughout the weekend by providing homemade Umbraco-themed boardgames - <em>CODECABIN Who?</em> and <em>UmbDobble</em>, which were both fantastic!</p>

<figure>
<img alt="A photograph taken by Joe's Guess Who opponent, with a custom Guess Who board made up of CODECABIN attendees. Joe is sat opposite the photographer at the bar, laughing. Bottles of drink can be seen stacked along the bar." src="/media/czraj0q2/img_2655.webp?width=1300" />
<figcaption>
<p>Lotte's personalised Guess Who? board.</p>
<small>Photo by <a href="https://x.com/MikeMasey">Mike Macey</a></small>
</figcaption>
</figure>

<h2>Next CODECABIN</h2>

<p>CODECABIN 24 is set to be from <strong>20-23 September 2024</strong>.</p>

<p>I'll certainly be applying for next year, I had a fantastic time. Not only did I learn a load about the Umbraco ecosystem, I've had my enthusiasm for contributing reignited and I had a great laugh doing it.</p>

<p>CODECABIN isn't like a regular conference. It requires an application process which is starting "soon" for CODECABIN 24. Keep an eye on <a href="https://twitter.com/codecabin">@codecabin on X</a> and other community social channels around that time and get your applications in (applications usually close just after CodeGarden).</p>

<h3>How do I get my boss to pay for all this?!</h3>

<p>Although people do pay for themselves to attend CODECABIN, I find it's always worth a shot persuading your boss! It's great fun, but honestly, it <em>is</em> work. Just like I'd ask for other conference tickets, CODECABIN has reignited my passion for open source and taught me many lessons pertinent to upcoming and future projects. The event gives you time to network with people you might not otherwise meet but also <em>code with</em> Umbraco MVPs and HQ employees like you never would at a regular conference.</p>

<p>Considering it's a fraction of the cost of sending someone to CodeGarden (let alone a more expensive conference!) and includes all food and accommodation and just how much more I got out of it, CODECABIN is a <em>bargain</em> for employers.</p>

<h2>Read more</h2>

<p>More blog posts by other attendees:</p>

<ul>
<li><a href="https://www.linkedin.com/pulse/codecabin-2023-alina-magdalena-tincas">Alina</a></li>
<li><a href="https://www.linkedin.com/pulse/codecabin-23-andy-boot/">Andy</a></li>
<li><a href="https://www.rachelbreeze.dev/blogs/codecabin-2023-the-umbraco-unconference-experience/">Rachel</a></li>
</ul>

<hr />

<p><small>Hero photo by <a href="https://github.com/jacksorjacksor">Richard Jackson</a></small></p>

<p><small>"Nerds in the Woods" as dubbed by <a href="https://x.com/therealrockerby">Richard Ockerby</a>'s wife.</small></p>                ]]></content:encoded>
            </item>
            <item>
                <title>URI property utility</title>
<link>https://joe.gl/ombek/blog/uri/</link>                <pubDate>Thu, 12 Jan 2023 12:28:04 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/uri/</guid>
                <description>It can&#39;t be just me that finds all the properties on a .NET URI object confusing! This tool takes a URI input and outputs all the properties without the need to debug.</description>
                <content:encoded><![CDATA[
                    <p>It can't be just me that finds all the properties on a .NET URI object confusing! This tool takes a URI input and outputs all the properties without the need to debug.</p>
<p>I built this little tool back in 2013 but still use it regularly to this day. Sometimes it's just so much simpler than looking at the docs!</p>

<iframe src="https://utils.joe.gl/uri" style="width: 100%; height: 50vh"></iframe>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbraco AngularJS filter cheat sheet (≤ v13)</title>
<link>https://joe.gl/ombek/blog/umbraco-angularjs-filter-cheat-sheet/</link>                <pubDate>Wed, 24 Aug 2022 02:22:26 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/umbraco-angularjs-filter-cheat-sheet/</guid>
                <enclosure url="https://joe.gl/media/omanlvll/prettyblocklist.png" length="16008" type="image/png" />
                <description>In Umbraco v13 and below, the Block editors (list and grid), Nested Content and custom list views allow you to customise how the summarized/collapsed items appear using AngularJS templates and filters - you&#39;re not just stuck with the default!</description>
                <content:encoded><![CDATA[
                    <p>In Umbraco v13 and below, the Block editors (list and grid), Nested Content and custom list views allow you to customise how the summarized/collapsed items appear using AngularJS templates and filters - you're not just stuck with the default!</p>
<div class="notification is-warning is-light">
<p>⚠️ Since these filters use native AngularJS functionality, they do not apply to v14+. <a href="/ombek/blog/ufm">Replacement functionality exists in v14.1 and up.</a></p>
</div>

<p>When you're configuring a block type in a block list or block grid, you're provided with a "Label" field. Similarly, custom columns in your list views have a "Template". You can just type some plain text in here, but using Angular templates is where it really shines.</p>

<p><img src="/media/30uj1gjw/labelconfig.png" alt="Screenshot of the block configuration panel in Umbraco showing the label field." /></p>

<p>For example, if you want to pull up the value of a field called "Heading" (alias <code>heading</code>) from the block list item into the name, you can do that by simply wrapping the alias of the property in double-curly braces: <code>{{ heading }}</code>. You can even include fixed text around it: <code>A text block with the heading '{{heading}}'</code> (but maybe less wordy, eh?)</p>

<p>As for custom list views, the only additional thing you need to know is that instead of using the property alias in the template, you use the <code>value</code> variable e.g. <code>{{ value.join(", ") }}</code></p>

<p>For properties that aren't as simple as a textbox, we can do that too with the help of these filters:</p>

<h2>Node picker / Media Picker v2</h2>

<p><code>ncNodeName</code> will pull back the name for a node selected in a node picker (although this can be a little buggy)</p>

<p>e.g. <code>Call to Action: {{ page | ncNodeName }}</code> becomes <code>Call to Action: Contact us</code></p>

<h2>Media Picker v3 (even in single mode)</h2>

<p>For Media Picker v2, this is the same as above, the <code>ncNodeName</code> filter works just fine.</p>

<p>However, for v3 we have a couple of alternatives.</p>

<p>In v11+, use the <code>mediaItemResolver</code> to get the media object using the key:</p>

<p>e.g. <code>{{(image[0].mediaKey | mediaItemResolver).name}}</code></p>

<p>Or (for older versions, or if you just like it better) we can use the <code>ncNodeName</code> filter if we construct our own UDI:</p>

<p>e.g. <code>{{ "umb://media/" + image[0].mediaKey | ncNodeName }}</code></p>

<h2>Rich Text editor</h2>

<p><code>ncRichText</code> strips out the HTML markup and just renders the inner text. This is useful for rendering text from a Rich text editor.</p>

<p>e.g.<code>Text module: {{ bodyText | ncRichText }}</code> becomes <code>Text module: This is some text from a rich text editor.</code></p>

<p>In v13, you'll also need to reference the <code>markup</code> property to access the contents:</p>

<p>e.g. <code>Text module: {{ bodyText.markup | ncRichText }}</code></p>

<h2>Multi-Url Picker (even in single mode)</h2>

<p>Simply navigate to the child properties like so: <code>link[0].name</code></p>

<p>e.g. <code>Call to Action: {{ link[0].name }}</code> becomes <code>Call to Action: Make an enquiry</code></p>

<h2>Yes/No</h2>

<p>Checkboxes store their value as a numeric, so we can use the value to render some text: <code>checkboxPickerAlias == 1 ? 'Yes' : 'No'</code></p>

<p>e.g. <code>Text with image ({{ flipped == 1 ? "Image right" : "Image left" }})</code> becomes <code>Text with image (Image right)</code></p>

<h2>Too long?</h2>

<p>We've got a few options for truncating text.</p>

<p>Angular's default <code>limitTo:n</code> (where <code>n</code> is the number of characters) will simply cut the text short after so many characters.</p>

<p>If we want to get fancy, we can use Umbraco's <code>truncate</code> function. It takes 3 parameters: wordwise (don't truncate in the middle of a word), the number of characters to truncate and finally an optional string to append to any truncated text (e.g. <code>…</code>, which is the default).</p>

<p>e.g. <code>Text module: {{ bodyText.markup | ncRichText | truncate:true:35 }}</code>* becomes <code>Text module: This is some text from a rich text…</code></p>

<p><strong>These last two parameters are swapped in v7</strong> (<code>truncate:35:true</code>).</p>

<p>We can also limit to a number of words using <code>umbWordLimit</code>, but without all the functionality of <code>truncate</code>.</p>

<p>e.g. <code>Text module: {{ bodyText.markup | ncRichText | umbWordLimit:4 }}</code>* becomes <code>Text module: This is some text</code></p>

<p><small>* See Rich Text editor section</small></p>

<h2>What about settings?</h2>

<p>Block list settings are made available using the <code>$settings</code> property.</p>

<p>e.g. <code>Text with image ({{ $settings.flipped == 1 ? "Image right" : "Image left" }})</code> becomes <code>Text with image (Image right)</code></p>

<h2>Messing with the default</h2>

<p>By default, the block displays the name of the block type. If we want to use that in our custom templates we can use <code>$contentTypeName</code>.</p>

<p>e.g. <code>{{$contentTypeName}} ({{ $settings.flipped == 1 ? "Image right" : "Image left" }})</code> becomes <code>Text with image block (Image right)</code></p>

<h2>How do I look?</h2>

<p>In the Block Grid, we can also access the column span and row span of the block using the <code>$layout</code> property and it's values <code>$layout.rowSpan</code> and <code>$layout.columnSpan</code>.</p>

<p>e.g. <code>{{$contentTypeName}} ({{$layout.columnSpan == 12 ? "full width" : "half width"}})</code> might become <code>Heading block (full-width)</code></p>

<h2>Arrays</h2>

<p>Umbraco provides a filter for joining arrays of strings or objects.</p>

<p>e.g. <code>Things: {{ myArray | umbCmsJoinArray:', ' }}</code> becomes <code>Things: Thing 1, Thing 2, Cat (in hat)</code></p>

<p>When applying to an array of objects, a second parameter can be provided to specify which property to used for joining.</p>

<p>e.g. <code>Things: {{ repeatableTextStrings | umbCmsJoinArray:', ':'value' }}</code> becomes <code>Things: Thing 1, Thing 2, Cat (in hat)</code></p>

<h2>Nesting block lists?</h2>

<p>Block lists within your block can be accessed by using the name of the block list property followed by <code>contentData</code>.</p>

<p>e.g. <code>Tab panel ({{ tabs.contentData.length }} tabs)</code> becomes <code>Tab panel (5 tabs)</code></p>

<p>Since this is just an array, we can also take advantage of the <code>umbCmsJoinArray</code> filter here!</p>

<p><code>Things: {{ things.contentData | umbCmsJoinArray:', ':'title' }}</code> becomes <code>Things: Thing 1, Thing 2, Cat (in hat)</code></p>

<h2>$index</h2>

<p>The $index field can be used in block lists and nested content. Perhaps confusingly, this is a 1-based index.</p>

<p>e.g. <code>{{ $index }}. {{ heading }}</code> becomes <code>1. Thing</code></p>

<h2>Regular ol' JS</h2>

<p>Some basic regular JS can be used too. But bear in mind you can't do complex things like writing functions (not even arrow functions, sorry!) This will require writing your own filters (see below).</p>

<p>e.g. <code>Things: {{ repeatableTextStrings.join(", ") }}</code> becomes <code>Things: Thing 1, Thing 2, Cat (in hat)</code></p>

<p>Although this particular example would be best solved using the existing <code>umbCmsJoinArray</code> filter (see above).</p>

<p>See also "Fallbacks" and "Messing with the default".</p>

<h2>Fallbacks</h2>

<p>Since we can run regular JS, we can use the truthiness of a statement to fall back to another value.</p>

<p>e.g. <code>{{ heading || tabs.contentData | umbCmsJoinArray:', ':'heading'}}</code> will be the value of the optional <code>heading</code> field, otherwise it will list all the headings of the child <code>tabs</code>.</p>

<h2>Writing your own</h2>

<p>Writing your own is possible too. Simply create a plugin folder in <code>App_Plugins</code> with a JS file for the new filter.</p>

<p>Here's an example taking the <code>umbCmsJoinArray</code> even further:</p>

<pre><code>angular.module("umbraco.filters").filter("blockSummary", function () {
  return function (input, fieldAlias, maxItems, showMore, separator) {
    var items = input.contentData;
    var more = "";

    if (maxItems &gt; 0) {
      var total = items.length;
      items = items.slice(0, maxItems);
      var moreCount = total - maxItems;

      if (showMore !== false &amp;&amp; moreCount &gt; 0) {
        more = ` + ${moreCount} more`;
      }
    }

    return items.map(i =&gt; i[fieldAlias]).join(separator || ", ") + more;
  }
});
</code></pre>

<p>You'll also need to create a <code>package.manifest</code> file:</p>

<pre><code>    {
        "name": "MyFilters",
        "version": "1.0.0",
        "allowPackageTelemetry": false,
        "javascript": [
            "/App_Plugins/MyFilters/blockSummary.filter.js"
        ]
    }
</code></pre>

<p>It's used as below:</p>

<p><code>Things: {{ things | blockSummary:'title' }}</code> becomes <code>Things: Thing 1, Thing 2, Cat (in hat)</code></p>

<p>Although this particular example would be best solved using the existing <code>umbCmsJoinArray</code> filter (see above), the following take better advantage of this custom method:</p>

<p><code>Tabs: {{ tabs | blockSummary:'heading':0:false:' / ' }}</code> becomes <code>Tabs: Umbraco development / .NET development / Front-end builds</code></p>

<p><code>FAQs: {{ items | blockSummary:'question':1 }}</code> becomes <code>FAQs: How did I get here? + 2 more</code></p>

<p><img src="/media/omanlvll/prettyblocklist.png" alt="Examples of these filters being used in a block list" /></p>

<h2>Further reading</h2>

<p>Some of this is covered in the <a href="https://docs.umbraco.com/umbraco-cms/13.latest/fundamentals/backoffice/property-editors/built-in-umbraco-property-editors/block-editor/label-property-configuration">Block Editor Label Properties documentation</a> (this is targeted at v9+ which may differ from functionality in older versions, docs for these are covered in: <a href="https://our.umbraco.com/documentation/Fundamentals/Backoffice/Property-Editors/Built-in-Property-Editors/Block-Editor/">Block Editors for &lt; v9</a> and <a href="https://our.umbraco.com/Documentation/Fundamentals/Backoffice/property-editors/built-in-property-editors/Nested-Content/">Nested Content for &lt; v9</a>) and the <a href="https://github.com/umbraco/Umbraco-CMS/tree/v13/main/src/Umbraco.Web.UI.Client/src/common/filters">source code for the Umbraco filters can be found on GitHub</a>.</p>

<p><a href="https://www.youtube.com/watch?v=lqWoNf27hyc&amp;list=PL90L_HquhD-81xTOCLLJZLl1roU6hXPhp&amp;index=9">Paul Seal covers using Block List editors in his Umbraco 10 series from episode 9 onwards.</a></p>

<hr />

<p><em><small>Thanks to <a href="https://github.com/bjarnef">Bjarne Fyrstenborg</a>, <a href="https://twitter.com/callumbwhyte/status/1639994629431910402">Callum Whyte</a>, <a href="https://twitter.com/deanleigh/status/1640005226651369473">Dean Leigh</a>, <a href="https://umbracocommunity.social/@jacksorjacksor">Richard Jackson</a>, <a href="https://umbracocommunity.social/@erica">Erica Quessenberry</a> and <a href="https://umbracocommunity.social/@lee">Lee Kelleher</a> for keeping me up to date on this one.<small></em></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Umbracadabra! Defence Against the Dark Arts of Magic Strings in Umbraco</title>
<link>https://24days.in/umbraco-cms/2021/magic-strings/</link>                <pubDate>Mon, 06 Dec 2021 09:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://24days.in/umbraco-cms/2021/magic-strings/</guid>
                <enclosure url="https://joe.gl/media/awonlrab/julius-drost-c8wlyf8ubbo-unsplash.jpg" length="71584" type="image/jpg" />
                <description>I&#39;m sure many of us have been told at some point in our careers that &quot;magic strings are bad&quot; but why exactly is that and what could go wrong? And what alternatives are there to improve our code?</description>
                <content:encoded><![CDATA[
                    <p>I'm sure many of us have been told at some point in our careers that "magic strings are bad" but why exactly is that and what could go wrong? And what alternatives are there to improve our code?</p>
<h2>Why are magic strings so bad?</h2>

<p>We use the phrase "magic strings" (and, to a lesser extent, "magic numbers") to refer to a constant string used directly in your code. An example of this that we, as Umbraco developers, may be all too familiar with is  <code>Model.GetPropertyValue("bodyText")</code>. Here, <code>"bodyText"</code> is the magic string. The text can be used in multiple places (maybe in the meta tags as well as the page body, or on different templates), has to be in this exact format (I can't swap it out with <code>"Can I have the text for the body please?"</code>, as polite as that is) and refers to an external entity (the Umbraco database via the Umbraco APIs in this case). These things make it a particularly code-smelly example of a magic string that needs replacing.</p>

<p>Magic strings is a term used to cover multiple scenarios of when a string is used in place of a more robust method.</p>

<p>Typos: the arch enemy of magic strings. If we're having a particularly productive, Hollywood-hacker-style afternoon, smacking away at those keys, you'd be excused for mis-hitting the odd one.</p>

<figure>
<video src="/media/1w3dqwe0/ncis-hacking.mp4" controls autoplay loop muted>
<img src="/media/tjymsoqs/hacking.gif" alt="Clip from NCIS showing two people typing manically on one keyboard" />
</video>
<figcaption>Clip from NCIS showing two people typing manically on one keyboard</figcaption>
</figure>

<p>They can be particularly difficult to spot too: depending on your font, <code>"bodyIext"</code> could easily go unnoticed. The folks that write our IDE's know this, and will correct us where possible. A string, however, cannot be autocorrected because <em>any</em> value of string <em>is a valid string</em>. As far as the IDE knows, you meant to type <code>"bossyTRex"</code>. These cause runtime errors rather than the easier-to-spot compile-time issues.</p>

<p>Fat-fingered-ness aside, magic strings can be exclusive to those who speak a different first language or people who have difficulty reading and writing. Even if you speak the same language issues can arise when trying to agree how to spell "colour"!</p>

<p>Or how about a piece of logic that sets <code>this.ValidationMethod = "none"</code>. We then later decide we need to change the validation method, but it's not obvious what the other possible values are. Magic strings aren't self-documenting and so we rely on reference documents or "there's a comment that explains how to use it somewhere" or "didn't I put that in the README file". This is far from ideal.</p>

<p>And if all that wasn't enough, magic strings make it more difficult to replace a value, test your code or change logic on your test environments compared to production. We can do better!</p>

<h3>Is there such thing as a muggle string?</h3>

<p>Are there cases when a string is non-magic? Or perhaps when a magic string is ok? Yes, not all strings are magic strings and not all magic strings are nescessarily bad.</p>

<p>If you're typing a string value in code, the blog <a href="http://www.douevencode.com/articles/2017-11/magic-strings-not-always-bad/">do you even code, bro?</a> has boiled it down to 4 questions you need to ask yourself:</p>

<blockquote>
  <ul>
  <li>Can you reuse this constant?</li>
  <li>Can you easily change a few values at once?</li>
  <li>Does it improve readability?</li>
  <li>Is it part of configuration?</li>
  </ul>
</blockquote>

<p>This won't cover all scenarios, but it's a good place to get started. If any of your answers to these questions is "yes", it's time to look into alternatives.</p>

<h3>Did you just casually drop the term "magic numbers" too? I've only just got to grips with magic strings!</h3>

<p>Ah, yes. Sorry about that!</p>

<p>Yes, magic numbers are the lesser talked about sibling of magic strings. But they can cause the same sorts of issues.</p>

<p>Take this code as an example:</p>

<pre><code>public void PrepareForCheckout() {
    if(Customer.Country == "United Kingdom") {
        FinalValue = Value * 1.175;
    }
}
</code></pre>

<p>It's not immediately obvious what this 1.175 is. You might have picked up that it's the old tax rate in the UK of 17.5%, but it could easily be missed, especially by a non-British developer.</p>

<p>Now that we know this is the VAT rate, we also know it's wrong. The UK changed their VAT rate to 20% a few years back. Correcting this value could be difficult too - we need to replace all instances, but if it's written as <code>value = value + value * (17.5/100)</code> (someone has gone about code clarity in an odd way!), a simple find-and-replace won't suffice.</p>

<p>If, for example, we had a constant of this value somewhere that we reference from everywhere, it would be both clearer what the code was doing <em>and</em> easier to update.</p>

<pre><code>public void PrepareForCheckout() {
    if(Customer.Country == "United Kingdom") {
        FinalValue = Value * UkVatRate;
    }
}
</code></pre>

<p>Now, there are many other tweaks we could make to this code, but I've just solved the one we've been talking about.</p>

<h2>ModelsBuilder to the rescue</h2>

<p>The first example I gave in this article was <code>Model.GetPropertyValue("bodyText")</code>, which, back in the Umbraco 7-days of 2013, <a href="https://twitter.com/zpqrtbnk">Stephan Gay  aka ZpqrtBnk </a> identified as a problem and developed the <a href="https://github.com/modelsbuilder/ModelsBuilder.Original">ModelsBuilder</a> package. ModelsBuilder is now a part of Umbraco and if you're not using already, you <em>really</em> should be. It removes the need for magic strings when interacting with IPublishedContent across our Umbraco solutions by generating models for each document type. Dave Woestenborghs wrote an article on <a href="https://24days.in/umbraco-cms/2016/getting-started-with-modelsbuilder/">Getting started with Umbraco Modelsbuilder</a> back in 2016 which still holds up well.</p>

<h2>A <em>constant</em> problem</h2>

<p>I mentioned creating a constant just now. And that's probably one of the simplest methods of avoiding magic strings. Depending on the size and complexity of your project, it can be as simple as pulling your strings up to constant declarations in your class.</p>

<pre><code>public class Cart {
    private const string BodyTextKey = "bodyText";
    private const decimal UkVatRate = 1.175;

    //...
}
</code></pre>

<p>In .NET, constants (<code>const</code> declarations) are actually pretty efficient - at compile-time, they're not allocated any memory like variables are, so it's equivalent performance-wise to directly using the string in each location. It does, however, help us out a lot pre-compilation by removing the magic strings.</p>

<p>To make constants reusable across your project you might find it useful to create a <code>Constants.cs</code> file in your project and stick all your relevant strings in there. This can get pretty cluttered on bigger projects, so you may need to categorise your constants further, grouping them into logical classes and namespaces or adding your constants to related services like so:</p>

<pre><code>public static class VatHelper {
    public const decimal UkVatRate = 1.175;

    //...
}
</code></pre>

<p>In fact, with a helper, we could go even further in case we need to apply VAT to other countries in the future:</p>

<pre><code>public static class VatHelper {
    public decimal GetVatRate(string country) {
        switch (country) {
            case Countries.UnitedKingdom: 
                return 1.175;
            default:
                return 1;       
        }
    }
}

//...

public class Cart {
    private const string BodyTextKey = "bodyText";
    private const decimal UkVatRate = 1.175;

    //...

    public void PrepareForCheckout() {
        FinalValue = FinalValue * VatHelper.GetVatRate(Customer.Country);
    }

    //...
}
</code></pre>

<p>(OK, so in most real-world examples the country and it's respective VAT rate would probably both live in a database somewhere, but it works as an example!)</p>

<h2><em>Enum</em>-ber of other solutions</h2>

<p>Constants are great and all, but they can get a little bit repetitive. Enumerated types (that's enums to you and me) are useful for a finite number of constants that won't change.</p>

<p>Enums are a great solution to our <code>ValidationMethod</code> setting we were talking about earlier. We could create an enum with all the possible values and use this enum in place of our strings:</p>

<pre><code>public enum ValidationMethod {
    None,
    Required,
    EmailAddress,
    PhoneNumber,
    Numeric
}

//...

this.ValidationMethod = ValidationMethod.None;

//...

switch (field.ValidationMethod) {
    case ValidationMethod.Required:
        return !string.IsNullOrEmpty(field.Value);
        // ...
}
</code></pre>

<p>Enums also work well for statuses:</p>

<pre><code>public enum OrderStatus {
    Draft,
    Paid,
    Pending,
    Shipped,
    RecievedByCustomer
}

//...

Order.Status = OrderStatus.Paid;

//...

if(Order.Status == OrderStatus.Paid) {
    ProcessOrder();
}
</code></pre>

<h3>Magic numbers and enums</h3>

<p>Under the covers, enums actually store integers for each value. In the case of our <code>OrderStatus</code> enum, <code>Draft</code> is equivalent to <code>0</code>, <code>Paid</code> is <code>1</code>, <code>Pending</code> is <code>2</code>, etc. You can even explicitly set the number applied to each value which can be useful if you need to map a readable name to a number, or if you need to add a value to an existing enum.</p>

<pre><code>public enum OrderStatus {

    /// We want to add a new status at the top,
    /// but also to maintain the mapping of the original enum
    /// so we specify the values

    Cancelled           = -1,
    Draft               =  0,
    Paid                =  1,
    Pending             =  2,
    Shipped             =  3,
    RecievedByCustomer  =  4
}

//...

/// Readable definitions for all possible
/// response codes from the ACME API docs
public enum AcmeApiResponseCodes {
    Ok              = 200,
    BadRequest      = 400,
    Unauthorized    = 401,
    InvalidClientId = 490,
    ExpiredClient   = 491
}
</code></pre>

<h3>JSON and enums</h3>

<p>As a side effect of the underlying value being an integer, you might notice that by default if you return an enum as a JSON result, you'll end up returning the number rather than the pretty enum.</p>

<pre><code>{
    "orderNumber": 51138461315,
    "status": 2,

    "...": "..."
}
</code></pre>

<p>JSON (and JavaScript too, without some workarounds) don't support enums. So we have two choices here: return the number, or return the string. You've got a few options if you want to convert the enum to and from a string if you'd rather.</p>

<p>You can set the converter for your individual property with a simple attribute:</p>

<pre><code>public class Order {

    public long OrderNumber { get; set; }

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public OrderStatus Status { get; set; }

    //...
}
</code></pre>

<p>Alternatively, set the attribute it on the enum definition if you want it to be serialized to a string every time you use it. While we're here, you'll notice you can also customise how the enum is rendered as a string with the <code>EnumMember</code> attribute.</p>

<pre><code>[JsonConverter(typeof(JsonStringEnumConverter))]  
public enum OrderStatus {
    Draft,
    Paid,

    //...

    [EnumMember(Value = "Recieved by customer")]
    RecievedByCustomer
}
</code></pre>

<p>Or finally, you can set the behaviour globally for all enums by modifying the <code>ConfigureServices</code> method in <code>Startup.cs</code>. We need to add <code>AddMvcAndRazor</code> to get ahold of the MVC config before adjusting the default JSON options.</p>

<pre><code>public void ConfigureServices(IServiceCollection services)
{
    services
        .AddUmbraco(_env, _config)

        // Here's the good stuff

        .AddMvcAndRazor(mvc =&gt;
        {
            mvc.AddJsonOptions(json =&gt;
            {
                json.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
            });
        })

        // That's all, folks!

        .AddBackOffice()
        .AddWebsite()
        .AddComposers()
        .Build();
}
</code></pre>

<p>Your configuration may differ slightly if you're using Umbraco 8 or below (with .NET Framework) or if you're using <code>Newtonsoft.Json</code> (rather than <code>System.Text.Json</code>) in your Umbraco 9+/.NET 5+ app, but the principles are the same.</p>

<h3>Flagged enums</h3>

<p>Simple enums work fine for things like statuses, where only one can be true at any one time. But how about where multiple values make sense?</p>

<p>Our <code>ValidationMethod</code> example from earlier applies here. It may be possible to have a field that needs validating as <code>Required</code> but also <code>EmailAddress</code>. With strings, we could have done this as an array and we can do the same with enums if we want...</p>

<pre><code>// How we might have allowed multiple values with magic strings
field.ValidationMethods = new string[] { "email address", "required" };

// We can do the same when using enums too
field.ValidationMethods = new ValidationMethod[] { ValidationMethod.EmailAddress, ValidationMethod.Required };
</code></pre>

<p>But we can also do one better with flagged enums to allow an enum to have multiple values.</p>

<pre><code>[Flags]
public enum ValidationMethod {
    None            = 0,
    Required        = 1,
    EmailAddress    = 2,
    PhoneNumber     = 4,
    Numeric         = 8
}

//...

this.ValidationMethods = ValidationMethod.EmailAddress | ValidationMethod.Required;

//...

if(this.ValidationMethods.HasFlag(ValidationMethod.Required)) {
        return !string.IsNullOrEmpty(field.Value);
}
</code></pre>

<p>In this example, you can see we've added the <code>[Flags]</code> attribute to the enum and assigned each enum a value - this bit is important. We can then assign multiple values using the "bitwise OR operator" or pipe character (<code>|</code>) it's the same we use two of in an or statement ( <code>this || that</code>).</p>

<p>To check if an enum has a flag, we can use the <code>HasFlag</code> on the enum value. I've had to refactor our switch statement from earlier to use an if statement for each value we want to check for.</p>

<p>You might notice the integer I've assigned to each number isn't in order. Well, it <em>is</em> in order, but using a doubling sequence: 1, 2, 4, 8... etc. Each number is double the previous value. This is important because a flagged enum still only stores one integer!</p>

<p>It works by using bitwise operations (there's that word again) which, simply put, is looking at the individual "bits" (and I mean that in the technical term!) of a binary number and applying an "OR" operation - if any of the bits in that column is 1, it returns a 1, otherwise it returns a 0.</p>

<p>For those curious, let's look at the numbers in the doubling sequence in binary. (<a href="#skip-binary">Skip ahead if you don't want to think about binary too much!</a>)</p>

<pre>
0000 (0)
0001 (1)
0010 (2<a href="#footnote">*</a>)
0100 (4)
1000 (8)

...etc
</pre>

<p>You'll notice, that because they're all powers of 2, each column only ever contains a single 1. Therefore, no matter what combination we make of these in a bitwise OR, we can work out which initial enum values went into creating it.</p>

<pre><code>var a = ValidationMethod.EmailAddress | ValidationMethod.Required;
/// That's 1 and 2
///
///    0001
/// OR 0010
/// -------
///    0011  (the last two columns have a 1 in the first number OR second number)


var b = ValidationMethod.Required | ValidationMethod.EmailAddress | ValidationMethod.PhoneNumber| ValidationMethod.Numeric;
/// That's 1 and 2
///
///    0001
///    0010
///    0100
/// OR 1000
/// -------
///    1111
</code></pre>

<p>In both examples above, to check the value has the flag <code>Required</code>, which is <code>0001</code>, we simply need to check for a <code>1</code> in the last column - the <code>HasFlag</code> method is actually using the bitwise AND flag (<code>&amp;</code>) under the covers which does this check.</p>

<pre><code>this.ValidationMethods.HasFlag(ValidationMethod.Required);

// is the same as

(this.ValidationMethods &amp; ValidationMethod.Required) == ValidationMethod.Required;

// is the same as

// Binary literals in C# are prefixed with 0b (C# 7+)
0b0000011 &amp; 0b0000001 == 0b0000001;

///     0011
/// AND 0001
/// --------
///     0001 (the last column has a 1 in the first number AND second number)
</code></pre>

<p>Because of this behaviour, we can also add a value for common combinations or all into our enum by adding all the values of the combining values together (adding and bitwise operations provide the same result in this case because they're all powers of two, but not normally!)</p>

<pre><code>[Flags]
public enum ValidationMethod {
    // Regular items
    None            =  0,
    Required        =  1,
    EmailAddress    =  2,
    PhoneNumber     =  4,
    Numeric         =  8,

    //Combinations
    RequiredEmail   =  3 // 0001 | 0010 = 0011 or cheat by doing 1 + 2 = 3
    All             = 15 // 0001 | 0010 | 0100 | 1000 = 1111 or cheat by doing 1 + 2 + 3 + 4 + 8 = 15
}
</code></pre>

<p>Not that an "All" value makes any sense in this case, but it's good to know!</p>

<p><a id="skip-binary"></a></p>

<h2>Config your way out of it</h2>

<p>One of the questions we asked at the beginning was "is it part of configuration?" This is a good question to ask because it may well change how we deal with it. What is configuration? I like to think of anything that can alter an application's behaviour. This value will have been flagged (in a specification or by yourself) as something that's likely to change - be that in the future or per environment. A URL to an API endpoint is a good example of a configuration variable:</p>

<ul>
<li>it sits outside the control of your application and could conceivably change</li>
<li>or you may want to point your staging site at a sandbox version of the API.</li>
</ul>

<p>Now we're in the land of .NET 5, we can even get rid of some of the magic strings we have historically used to pull values out of config by mapping whole configuration sections to C# objects. I've also used a constant to get the configuration section name, to avoid that as a magic string.</p>

<pre><code>public class AcmeConfig
{
    public const string Section = "ACME";

    public string ApiBaseUrl { get; set; }

    //...
}

//...

public class AcmeClient {
    public AcmeClient(IConfiguration configuration) {
        var config = configuration.GetSection(AcmeConfig.Section).Get&lt;AcmeConfig&gt;();

        //...
    }

    //...
}
</code></pre>

<h2>Umbracadabra! Magic-less code</h2>

<p>Hopefully I've been able to explain a little of why avoiding these magic strings might be necessary and how we can improve upon them. Don't go crazy - there's no need to replace every variable in your code, but its worth thinking about in the future each time you open those double-quotes!</p>

<p>Happy <code>const</code>-ing.. and <code>enum</code>-ing... and Model Building!</p>

<p><a id="footnote"></a>*<a href="https://en.wikipedia.org/wiki/Mathematical_joke#Jokes_with_numeral_bases">There are 10 types of people in the world: those who understand binary and those who don't.</a></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Back to REBASE-ics</title>
<link>https://joe.gl/ombek/blog/rebase/</link>                <pubDate>Wed, 24 Nov 2021 04:21:53 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/rebase/</guid>
                <enclosure url="https://joe.gl/media/aofd43wf/screenshot-2021-11-24-155107.png" length="117489" type="image/png" />
                <description>My favourite method of merging feature branches has to be &quot;rebase and merge&quot; (that&#39;s a &quot;semi-linear merge&quot; if you&#39;re using DevOps) - it allows us to avoid losing data while keeping the tree clean and tidy.

But what is a rebase and how do they work?</description>
                <content:encoded><![CDATA[
                    <p>My favourite method of merging feature branches has to be "rebase and merge" (that's a <a href="https://devblogs.microsoft.com/devops/pull-requests-with-rebase/#semi-linear-merge">"semi-linear merge"</a> if you're using DevOps) - it allows us to avoid losing data while keeping the tree clean and tidy.</p>

<p>But what is a rebase and how do they work?</p>
<p>I've taken an example git repository so we can walk through how we'd do this with rebase and merge.</p>

<p><img src="/media/lvkksjoa/rebasemergept1.svg" alt="A git tree with three feature branches. The feature/another-talk branch is already merged, while two remain unmerged." /></p>

<p>We can actually see the merge of the <code>feature/another-talk</code> branch is already complete since it's cleanly merged into <code>main</code> with no intermediate commits to main - there was no need to rebase and merge, so it's just the other two branches we need to worry about.</p>

<p><img src="/media/axcnh2vd/rebasemergept2.svg" alt="The &quot;Write talk&quot; commit cherry-picked onto a new branch off of main" /></p>

<p>Now, I've created a new branch from the latest <code>main</code> and faded out our existing <code>feature/git-talk</code> branch.</p>

<p>If you've ever done a cherry-pick before to move a commit from one branch to another, a rebase is like an extreme version of that! When told to rebase, git will cherry-pick each commit in a branch in turn and add it to a new branch at the point requested (in this case, a later version of the same <code>main</code> branch).</p>

<p>Here I've "cherry-picked" the "Write talk" commit and put it onto our new branch.</p>

<p><img src="/media/dl4a3gbf/rebasemergept3.svg" alt="feature/git-talk branch fully rebased onto main" /></p>

<p>Then we repeat that for all the other commits needed (including "fix typos" in that other branch), adding them in order and fixing any merge conflicts that may come about for each one.</p>

<p>And that's actually the rebase out the way! We've "cherry picked" each commit and resolved any merges, but in a more automated way than actually doing loads of cherry-picks.</p>

<p><img src="/media/5mefc2ta/rebasemergept4.svg" alt="feature/git-talk branch fully rebased and merged" /></p>

<p>Now it's time to merge <code>feature/git-talk</code> into <code>main</code>.</p>

<p>That old branch doesn't need to be there anymore - it doesn't actually exist locally, so we can hide that.</p>

<p><img src="/media/obrfaqmi/rebasemergept5.svg" alt="feature/git-talk branch fully rebased and merged with the old un-rebased branch removed" /></p>

<p>And then all we have to do is rebase and merge our final branch, <code>feature/another-talk</code>.</p>

<p><img src="/media/lb0hy235/rebasemerge.svg" alt="Final rebased structure with branches making an uninterrupted &#39;D&#39; shape against the main branch" /></p>

<p>To give us this!</p>

<p>Each feature makes a neat "D" shape with the <code>main</code> or <code>develop</code> branch, with no crossovers. As you can see, when compared to a traditional merge process shown below this method removes the muddle of crisscrossing merges, while maintaining the whole branch structure and all the detail in every commit.</p>

<p><img src="/media/0cxdrd4g/messy-commits.svg" alt="Traditional merge method with crisscrossing merges" /></p>

<p><a href="https://skrift.io/issues/why-are-you-being-such-a-git-about-it/">For more information on why I prefer this practice and other git tips, check out my "Why are you being such a git about it?" article on Skrift.</a></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Using Gitlint with GitHub Actions</title>
<link>https://joe.gl/ombek/blog/pr-gitlint/</link>                <pubDate>Wed, 24 Nov 2021 02:21:03 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/pr-gitlint/</guid>
                <description>Linting commit messages can be useful to enforce a standard of git commit messages - but how can we run these as checks on a GitHub pull request?</description>
                <content:encoded><![CDATA[
                    <p>Linting commit messages can be useful to enforce a standard of git commit messages - but how can we run these as checks on a GitHub pull request?</p>
<p>It's possible to run checks in GitHub actions as automated checks in GitHub pull requests. These are simply GitHub actions with the <code>on: pull_request</code> trigger.</p>

<p>I'm using <a href="https://jorisroovers.com/gitlint/">gitlint by Joris Roovers</a> for this purpose as the rules are highly configurable (and come with sensible defaults). This script will automatically look for rules in your <a href="https://jorisroovers.com/gitlint/configuration/#the-gitlint-file"><code>.gitlint</code> file</a> in the root of your repository.</p>

<pre><code>name: Gitlint
on:
  pull_request:

jobs:
  gitlint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          # Check out at the last commit (pre-automated merge, we don't care about the temporary commit for linting)
          ref: ${{ github.event.pull_request.head.sha }}
          # Get all history
          fetch-depth: 0

      - name: Install gitlint
        shell: bash
        run: |
          python -m pip install gitlint

      - name: Run gitlint
        shell: bash
        run: |
          # Lint everything from the base to the latest
          gitlint --commits "${{ github.event.pull_request.base.sha }}..HEAD"
</code></pre>                ]]></content:encoded>
            </item>
            <item>
                <title>Why are you being such a git about it?</title>
<link>https://skrift.io/issues/why-are-you-being-such-a-git-about-it/</link>                <pubDate>Tue, 05 Oct 2021 11:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://skrift.io/issues/why-are-you-being-such-a-git-about-it/</guid>
                <enclosure url="https://joe.gl/media/1fohbwbf/git-about-it.jpg" length="247048" type="image/jpg" />
                <description>Are your git repos a dumping ground for code? I&#39;m really fussy about how people use git - and that&#39;s a good thing. We&#39;ll take a look at branching strategies, the importance of commit messages and how often should you commit, anyway? Let&#39;s investigate how, with 5 simple tips, we can turn a code dump into a glorious archive of software and how this can save time, frustration and money.</description>
                <content:encoded><![CDATA[
                    <p>Are your git repos a dumping ground for code? I'm really fussy about how people use git - and that's a good thing. We'll take a look at branching strategies, the importance of commit messages and how often should you commit, anyway? Let's investigate how, with 5 simple tips, we can turn a code dump into a glorious archive of software and how this can save time, frustration and money.</p>
<p>Let's start really simple: what is git? Git is the most popular Source Control Management (SCM) system. It can ensure your code is backed up, versioned and accessible to other developers. Used properly, however, it can be so much more - helping ease complex processes such as release management, feature development and multiple people working concurrently.</p>

<p>I can, though, be a bit of a git about it: I'm really fussy about how people use git - and that's a good thing. I've worked with a fair few organisations and development teams over the years, and poor git utilisation is a more common issue than you might think. People use git as a dumping ground for code rather than a well-managed archive of software. Today we'll take a look at 5 simple tips which will, with any luck, improve your git practice.</p>

<h2>0. Source <strong>Control yourselves</strong></h2>

<p><blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">Why did the developer not want to use git?<br><br>They were scared of committing to it</p>&mdash; Lou (@lovelacecoding) <a href="https://twitter.com/lovelacecoding/status/1432414619310739463?ref_src=twsrc%5Etfw">August 30, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>

<p>This isn't one of the 5, but there are seriously some organisations out there not using SCM yet. SCM is <em>not</em> the same as a backup and if you've emailed some code to a colleague, you're probably doing it wrong!</p>

<p>For all the reasons I gave previously, SCM is essential for modern day software development.</p>

<p>Mercurial (hg), Team Foundation Version Control (TFVC) and Subversion (SVN) are all examples of SCMs but Git has really taken over in the software world and is the industry standard - I'd recommend it for that on its own: there's no point teaching your team a SCM system which is (or may become) redundant.</p>

<p>As for git clients, there are so many out there. Some like to use the command line - and that's great... if you're a git pro! But why overcomplicate? And there are times when being able to visualise the commit history in tree form can be a life-saver even for confident git-users. My favourite tree visualisation (and therefore favourite overall git client) is <a href="https://www.gitkraken.com/invite/dsMBqLEr">GitKraken (this is a referral link - I want a GitKraken t-shirt!)</a>. It's free for open source projects, but you'll need to pay for private repositories. I'll be using GitKraken for examples in this article as it's the client I'm most familiar with - but you can use any client you like but I'd strongly recommend not using a stripped-back client such as Visual Studio or GitHub Desktop as you get into more complex git usage and processes. If GitKraken isn't your bag, you may want to investigate <a href="https://git-fork.com/">Fork (also paid, available for Mac and Windows)</a>, <a href="http://gitextensions.github.io/">Git Extensions</a> or, at the very least, <a href="https://www.hanselman.com/blog/my-ultimate-powershell-prompt-with-oh-my-posh-and-the-windows-terminal">Oh My Posh and Windows Terminal</a>.</p>

<h2>1. Commit <strong>Messaging is everything</strong></h2>

<p>This <a href="https://xkcd.com/1296/">xkcd comic</a> highlights the problem perfectly.</p>

<p><a href="https://xkcd.com/1296/"><img src="https://imgs.xkcd.com/comics/git_commit.png" alt="XKCD comic: &quot;As a project drags on, my git commit messages get less and less informative.&quot; &quot;loop &amp; timing control    (14 hours ago), enabled config file parsing (9 hours ago), misc bugfixes    (5 hours ago), code additions/edits (4 hours ago), more code    (4 hours ago), here have code   (4 hours ago), aaaaaaaa (3 hours ago), adkfjslkdfjsdklfj    (3 hours ago), my hands are typing words    (2 hours ago), haaaaaaaaands    (2 hours ago)&quot;" /></a></p>

<p>I'd like to say that was just a joke in a comic strip, but it can be all too real...</p>

<p><blockquote class="twitter-tweet" data-dnt="true"><p lang="en" dir="ltr">Investigating an issue and by scanning through useful commit messages in git history.. 😫 <a href="https://t.co/lVeopRoDJg">pic.twitter.com/lVeopRoDJg</a></p>&mdash; Dennis Adolfi (@dadolfi) <a href="https://twitter.com/dadolfi/status/1405424992121131010?ref_src=twsrc%5Etfw">June 17, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>

<p>Imagine being in Dennis' situation and:</p>

<ul>
<li>you've discovered a bug and you can't find the commit where it likely happened</li>
<li>you see something odd in the codebase, there's no comment so you use a blame/annotate tool to see who changed it and why... but Andy is on holiday and his commit message says "test" 😱</li>
<li>some piece of code has changed since you last saw it - why? You could revert it back to how you like it but maybe it was changed for a reason? You fire up the blame tool again, "Fixes date formatting issue in Chrome" - better leave it as it is! Saved by a good commit message!</li>
</ul>

<p>Some people will tell you that good commit messages should be written as if answering the question "what does this commit do?" and that's good advice! Starting your commit message with a verb like "Fixes ...", "Reverts ..." or "Integrates ..." is a good practice too.</p>

<p>But these alone are not enough. "Fixes bugs" or "Reverts broken changes" are still useless. Detail, detail, detail!</p>

<p>GitKraken (and many other GUI git clients) give you two fields for your commit messages - a title and a description. It's best to keep your title short (GitKraken recommends 72 characters) but you can add even more detail in the description field.</p>

<p>You can even do this on the command line: <code>git commit -m "Title" -m "Description"</code></p>

<figure>
<blockquote><p>
<strong>Fixes date formatting issue in Chrome</strong>    
    </p>
<p>
Chrome always assumes the MM/DD format no matter what locale the browser is set to*    
    </p>
    </blockquote>
<figcaption><small>*it doesn't, this is just a made up example</small></figcaption>
</figure>

<figure><blockquote>
<p><strong>Reverts new "Paytastic Checkout" checkout flow</strong></p>
<p>Customer has changed their mind and wants to revert to using LegacyCart</p>
</blockquote></figure>

<figure><blockquote>
<p><strong>Integrates SMS API</strong></p>
<p>Sending an SMS to customers when their order ships using the "Simple SMS Sender" API</p>
</blockquote></figure>

<p>If you have a ticketing system or to-do list, it sometimes helps to include the title of the item in your commit and an ID or link to the initial issue can prove very useful.</p>

<p>GitKraken (and many other GUI git clients) let you hook up your issue tracker to your git repos so you can automatically stick an issue number into the branch name (and it'll therefore be in the merge commit too!)</p>

<p><img src="/media/yvcdd2ob/image-20210906122621281.png" alt="A screenshot of the GitKraken &quot;Create a new branch for this issue&quot; menu option of the Github Issues integration" /></p>

<h2>2. Push <strong>Little and often</strong></h2>

<p>I've seen a lot of different tactics when it comes to how often people commit, but the most persuasive argument I've heard is for "little and often". Don't forget git acts as a backup of your work and progress. There's no point <em>only</em> committing when something is feature complete: what happens if your hard drive packs up overnight? Or you're off sick tomorrow and somebody else is left to pick up your work? It's far more useful to have partially complete work committed than not. Although it's generally good practice to ensure each commit is in a buildable state - it's ok if your work-in-progress (WIP) commits don't build.</p>

<p>You can also use your tiny commits to help you revert unwanted pieces of functionality, while it can be a real pain to pull small chunks of code out of a bigger commit.</p>

<p><img src="/media/x1dhjz1f/little-and-often.svg" alt="A git commit tree with commits for &quot;Change colour-scheme to green on red&quot; followed by &quot;Reorder slides&quot; and then &quot;Reverts &#39;Change colour-scheme to green on red&#39;&quot;" /></p>

<p>Perhaps changing the colour-scheme to green text on a red background was a bit much...? It would have been all too easy to bundle "Change colour-scheme to green on red" in with "Reorder slides", but separating these out made it a lot easier to revert one of them at a later date.</p>

<h2>3. <strong>Squash</strong> rackets</h2>

<p>But don't all these "little and often" commits start to make a <em>racket</em> after a while? (See what I did there?) That's where squash and amend come in. As I mentioned, I'm a big fan of a WIP (Work -In-Progress) commit at the end of the day. But, come tomorrow morning and I've finished that small segment of work off, I don't want two commits "WIP - Restyle footer" <em>and</em> "Restyle footer". So I can amend my "WIP" commit to include all changes and rename it to "Restyle footer".</p>

<p><img src="/media/fixbfjul/image-20210906143642617.png" alt="A screenshot of the GitKraken &quot;Commit message&quot; pane showing the &quot;Amend&quot; checkbox ticked" /></p>

<p>Or in the command line <code>git commit --amend -m "Restyle footer"</code>. Atlassian has a great tutorial about <a href="https://www.atlassian.com/git/tutorials/rewriting-history">rewriting git history from the command line</a>.</p>

<p>If you want to combine multiple commits, or you've already committed your second round of changes, you can look into "squashing" the commits.</p>

<p><img src="/media/20fbcafw/image-20210906145203419.png" alt="Screenshot of GitKraken with multiple commits selected and highlighing the &quot;Squash 3 commits&quot; right-click menu option" /></p>

<p>In GitKraken, this is done by selecting the multiple commits you want to combine, right clicking and selecting "Squash". There are some scenarios when squashing won't be available - in this case you might be best just leaving the history as-is!</p>

<p>If I'm doing some DIY at home, things can get pretty messy with tools and dirt around the place. Before I go to bed, I might do a quick tidy up but the next day, once I've finished the piece of work, all the tools go away and I vacuum up the mess I made. I'll definitely have it clean and tidy before I let anybody else in the house!  Treat your branches the same way. It's OK for them to be a bit messy while you've got things in progress, but as soon as you're finished or ready to merge into a shared branch, you ought to tidy it up - nobody wants to see your messy branch!</p>

<p>One thing to note, though: don't squash your mistakes! Your mistakes and reworkings tell a story. It could provide valuable explanation to a future developer and documents your mistakes and learnings. Use amend and squash to tidy up, not to sweep under the rug!</p>

<h2>4. A branching <strong>Strategy is key</strong></h2>

<p>There are many branching strategies out there and I don't really mind which one you use. My personal favourites are <a href="https://guides.github.com/introduction/flow/">GitHub flow</a> and <a href="https://commonflow.org/">Common Flow</a> (or a combination of the two) but there are <a href="https://www.gitkraken.com/learn/git/best-practices/git-branch-strategy">many more</a>. Generally, these follow the feature-driven development pattern, with a branch for each feature and some notion of the state of a particular release (generally using branches or tags).</p>

<p>I also like to ensure only one person is committing to any single branch at one time - if two people are working on a feature, break that feature down into sub-features. This is the only way my "messy work-in-progress branch" method works!</p>

<p>GitHub flow, put very simply involves creating a branch for your feature (mapped to a GitHub issue), adding your commits to that branch and then opening a Pull Request. You discuss and review your code changes with your colleagues before deploying your changes for test. Once everyone is happy, your Pull Request is merged into the <code>main</code> branch. The <a href="https://guides.github.com/introduction/flow/">GitHub flow tutorial</a> goes into a little more detail.</p>

<p>It's worth noting that if none of these git flows make sense to you, you can take inspiration from them. I don't know of many companies who strictly follow any one particular strategy. So pick one as a baseline and adapt it for your organisation.</p>

<h2>5. <strong>Rebase and merge</strong> - the best of both worlds!</h2>

<p>Alright, so you've got your branching strategy and you've completed your feature - we're ready to merge!</p>

<p><img src="/media/dmrfh3al/messy-commits.svg" alt="A messy git tree with merges happening all over the place resulting in a tree with branches crossing over each other" /></p>

<p>But on larger projects with multiple concurrent workflows, you can end up with a real tangle of branches and merges.</p>

<p>So what other options are there for merging these features?</p>

<p><img src="/media/kypncksy/image-20210902111845141.png" alt="The &quot;Rebase and merge&quot; option shown in the merge type dropdown in a GitHub pull request" /></p>

<p>GitHub provides these 3 options on completion of a Pull Request:</p>

<ul>
<li>Create a merge commit (which creates the mess you see above)</li>
<li>Squash and merge</li>
<li>Rebase and merge</li>
</ul>

<p>Now, some people like to squash and rebase: your entire feature gets squashed into one commit and plopped straight onto the develop or main branch -it's kind of an extreme alternative to GitHub's "squash and merge". This is very neat and tidy, I'll give you that! However, you lose a lot of the documentation of when, how and why code changes were made. I'd much rather keep this history where we can.</p>

<p><img src="/media/4rgm3ott/squashrebase.svg" alt="A linear git tree with no branches and only one commit for what was previously a whole branch" /></p>

<p>Which leaves us with "Rebase and merge". This option allows us to avoid losing data while keeping the tree clean and tidy so is, of course, my favourite. (If you're using Azure DevOps, you can set this method as the merge type too - they call it a <a href="https://devblogs.microsoft.com/devops/pull-requests-with-rebase/#semi-linear-merge">"semi-linear merge"</a>).</p>

<p>Rebasing is the act of "replaying" each commit in a branch (our feature branch) on top of the target branch (<code>main</code> or <code>develop</code> in this case) as if they were happening at this newer point in time.</p>

<p><img src="/media/0dyaijo2/rebasemerge.svg" alt="A git tree with no crossing branches, each branch making a neat &#39;D&#39; shape against the main branch" /></p>

<p>As you can see, this method removes the muddle of criss-crossing merges, while maintaining the whole branch structure and all the detail in every commit. Each feature makes a neat "D" shape with the <code>main</code> or <code>develop</code> branch, with no crossovers.</p>

<p>You can also enact this process without a Pull Request utilising the individual tools <code>rebase</code> your feature onto <code>main</code> and then <code>merge</code> your feature into <code>main</code> in any git client of your choosing.</p>

<h2>A note on rewriting history</h2>

<p>Both tips 4 and 5 do something controversial in the git world: rewriting history. Not "rewriting history" as in the act of whitewashing a historical atrocity, but simply changing what's been pushed to the git repository (AKA history) after its happened (rewriting). Squashing, amending, rebasing and patching are all examples of git history being rewritten.</p>

<p>And I've just been actively advocating for 3 of these!</p>

<p>Maintaining your "true history" certainly has its merits: it can be useful for learning and code reviews. It's also impossible to mess up your history if you never tamper with it!</p>

<p>However, the way I see it is that this shouldn't at the cost of the readability of your repo. I prefer to maintain what I like to call my "<em>feature</em> history" where the order of our feature development is true, even if we're tweaking the timelines a little of when each feature was developed relative to each other.</p>

<p>I don't advocate for complete rewriting, just a quick tidy up.</p>

<p>As for that "risk" I mention when rewriting history, just make sure you've <em>pushed</em> your repository before you start messing with history. Then double check before your force push.</p>

<h2>From code dump to stunning archive of software</h2>

<p>And that's it. That's our stunning archive of software. We're source controlled with small incremental commits, each having a meaningful and detailed commit message sat comfortably in feature branches with no distracting noise and complicated merges. It reads easily from bottom to top, with no detail hidden or mistakes masked.</p>

<p>A clean history allows for features to be rolled back easily, while detailed commit messages create a better understanding of a repositories history leading to fewer mistakes and a branching strategy can clarify what code is deployed to which environment at any point in time. It's now plain to see how git discipline can provide benefits to developers, managers and the end client - saving time, frustration and money for all involved in a project.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Shaken to the Core about .NET 5?</title>
<link>https://joe.gl/ombek/blog/shaken-dotnet-5/</link>                <pubDate>Thu, 10 Jun 2021 04:15:18 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/shaken-dotnet-5/</guid>
                <enclosure url="https://joe.gl/media/jafb0fqn/camila-quintero-franco-mc852jack1g-unsplash-cropped.jpg" length="726874" type="image/jpg" />
                <description>It’s a dark and stormy night and you wake up with a start: is that the looming feeling of .NET 5 and Umbraco 9 on the horizon?!
Upskilling yourself (and your team) can be a daunting prospect for developers, but I’ll guide you through which skills and processes need to change and which you can put on the back-burner. From development environments and .NET 5 concepts to configuration, deployment and hosting - it’s not as big a change as it might sound!</description>
                <content:encoded><![CDATA[
                    <p>It’s a dark and stormy night and you wake up with a start: is that the looming feeling of .NET 5 and Umbraco 9 on the horizon?!
Upskilling yourself (and your team) can be a daunting prospect for developers, but I’ll guide you through which skills and processes need to change and which you can put on the back-burner. From development environments and .NET 5 concepts to configuration, deployment and hosting - it’s not as big a change as it might sound!</p>
<p><a href="/ombek/blog/shaken-dotnet-5-video/">A recorded talk containing the same content as this article is also available.</a></p>

<p>In case you missed the memo, Umbraco 9 won't be running on .NET Framework. All the buzz about Umbraco 9, UniCore, .NET Core, .NET 5 all boils down to this: Umbraco will be running on the brand new .NET 5.</p>

<p>For many people I've spoken to, this is a terrifying prospect. The way we've been working for years or even decades is about to change in a big way - and it's hard to find the time to upskill ourselves and our teams while working full time in earlier Umbraco versions.</p>

<p>.NET 5 is the next step for both .NET Framework and .NET Core and takes over from .NET Standard and Mono too. It opens up a lot of opportunities for us as developers and words like Linux, Docker, containerisation, Visual Studio Code, dotnet templates are popping up all over the place in the Umbraco world. But that's a lot of change to all happen at once - so what's important and what can we put off for a rainy day?</p>

<h2>File > New</h2>

<p>You might have heard about dotnet templates in the command line and using Visual Studio Code - which you can do, and they're brilliant. But one change at a time, eh?</p>

<p>So for now, lets stick with Visual Studio and create a new Umbraco site using the New Project dialog.</p>

<p>You'll need to enable .NET Core templates in Visual Studio settings first (but for me this was already enabled), and then it's simple as searching for "Umbraco" in the "New Project" window. <a href="https://our.umbraco.com/documentation/UmbracoNetCoreUpdates#prerequisites">(You'll also need to add a new Nuget repository if you're working with the beta, but this won't be required once Umbraco 9 launches)</a>.</p>

<p><img src="/media/3pnfguh5/image-20210602113744941.png" alt="You&#39;ll need to enable Options&gt; Environment &gt; Preview Features &quot;Show all .NET Core templates in the New project Dialog&quot;" /></p>

<p><img src="/media/1foasam5/image-20210602143732522.png" alt="Create a new project dialog with &quot;Umbraco&quot; entered in the search field." /></p>

<p>You'll actually see some improvements from what you're used to in Umbraco 8 thanks to the unattended install process.</p>

<p>You'll be presented with options inside the New Project window to configure your site without having to run through the Umbraco installer.</p>

<p><strong>If you want to use SQL CE (create a database file locally) you'll need to check the "Use SQL Compact Edition" option (or the <code>-ce</code> flag on the <code>dotnet new umbraco</code> statement).</strong> Umbraco ships without this my default because it's Windows-only. You won't get the option of a SQL CE connection string if you don't set this flag.</p>

<p>You can also configure a user at this step. But at the moment, you can't use unattended install <em>with</em> a SQL CE database, so pick one or the other.</p>

<p><img src="/media/0donm20a/image-20210602144527027.png" alt="The Additional Information screen when creating an Umbraco project with the SQL CE flag and unattended install options" /></p>

<p>After creating the project we'll be presented with a new Umbraco site. It may look a little different, but we'll come onto that later.</p>

<p>Hit the Debug in IIS Express button (no need to use <code>dotnet serve</code> if you don't want to) and you'll see the installer we all know (or, if you used the unattended install, you'll be flung straight into an empty Umbraco site).</p>

<h2>Solution structure</h2>

<p>Things look a little different in the Solution Explorer. But it's nothing much to be afraid of. </p>

<p>Your views, controllers and other pieces of code, they'll all live in the usual folders.</p>

<p>The main difference you need to be aware of is the <code>wwwroot</code> folder. This is where your static assets are going to live: compiled JS, CSS and related assets. Anything outside this folder will not be served to the browser (except via routing of course). It's a nice peace of mind and saves security rules in your web.config, but takes a little getting used to.</p>

<p>You'll notice the old <code>umbraco</code> folder is now split between <code>~/umbraco</code> and <code>~/wwwroot/umbraco</code>, depending on the use case.</p>

<p>Another change you'll notice is the lack of a <code>Web.config</code> file (or any <code>.config</code> file for that matter) and the addition of an <code>appsettings.json</code>...</p>

<h2>Configuration</h2>

<p>One of the biggest changes you'll see in .NET 5 is with config: everything is an AppSetting. You'll also find some things have moved, but almost everything that was in a <code>.config</code> file will now be in appsettings.json.</p>

<p>Helpfully in VS Code, you get Intellisense for all the property names in <code>appsettings.json</code> - even those specific to Umbraco! Unfortunately, Visual Studio doesn't support the schema ([and I wouldn't hold your breath](<a href="https://developercommunity.visualstudio.com/t/support-json-schema-draft-06-draft-07/796216">Support json schema draft-06 &amp; draft-07 - Visual Studio Feedback</a>)). It's still perfectly usable and no worse than in .NET 4, but it's worth noting this plus for using VS Code.</p>

<p>Because it's JSON rather than key value pairs in config files, settings can now be hierarchical. This means you can find all Umbraco related settings under "Umbraco", logging settings under "Serilog" and uSync settings under "uSync". If you're using an appsetting source that only allows key value pairs (Azure Web Apps or environment variables, perhaps) you can replicate this structure with a double underscore, such as <code>Umbraco__CMS__Hosting__Debug</code>.</p>

<p>I love the new hierarchy and standardisation of all settings this provides. You can also customise the config sources and pull different values from different places in different environments... basically I'm in love with the new configuration. It's what we should've had all along.</p>

<h2>C# changes</h2>

<figure>
<blockquote>
"[Using .NET 5] is a bit like being in someone else's kitchen. You know that there's stuff that should be there, it might just be in a slightly different place, [...] there might be new things and some of the old things might not be there any more."
</blockquote>
<figcaption>
Laura Weatherhead, <cite>Candid Contributions S2:E9</cite>
</figcaption>
</figure>

<p>This is undoubtedly the biggest challenge when moving to .NET 5: libraries have changed and code you're used to writing won't work any more. It can feel a little discombobulating.</p>

<p>That all said, the concepts are generally the same - it's still a "kitchen". It may be a different container, but it's still dependency injection. It may have a slightly different name, but it's still the same method. It might be a different library you need, but it'll still fit the same purpose.</p>

<figure>
<blockquote>
"I know there's a cutlery drawer here somewhere, I just need to find it."
</blockquote>
<figcaption>
Laura Weatherhead, <cite>Candid Contributions S2:E9</cite>
</figcaption>
</figure>

<p>If you're anything like me and are a heavy user of Intellisense when you're coding, you should be fine. And any time you'd usually be using a Nuget package, you might just need to do a quick Google search first.</p>

<p><img src="/media/ypkj52ca/image-20210605101519387.png" alt="A Google search for &quot;swashbuckler .NET 5&quot;" /></p>

<h2>Packages</h2>

<p>There are some pretty drastic changes to the codebase to make Umbraco work in .NET 5 and in a ".NET 5 way". This means package developers have had to do a fair chunk of work to get their packages ready for Umbraco 9.</p>

<p>However, available packages are big factor when deciding whether to upgrade to v9 or not. So let's have a rundown of some of the big ones.</p>

<p>All packages that are front-end only (no C#) should run fine in v9 - the backoffice hasn't changed.</p>

<h3>Umbraco Forms &amp; Deploy</h3>

<p>HQ have promised that the official Umbraco packages will be in place by the time v9 ships.</p>

<h3>uSync</h3>

<p>Kevin has already <a href="https://blog.jumoo.co.uk/2021/uSync-9-beta/">released a beta of uSync</a>! No releases of uSync.Complete or uSync.ContentEdition yet though.</p>

<h3>Vendr</h3>

<p>Development is underway. </p>

<h3>Doc Type Grid Editor</h3>

<p>Søren plans to have DTGE complete for v9 launch.</p>

<h3>Contentment</h3>

<p>Lee is actively working on a v9 version.</p>

<h3>Unknown</h3>

<p>I can't find any word on: Ucommerce, <a href="https://github.com/umbraco-community/UmbracoFileSystemProviders.Azure">UmbracoFileSystemProviders.Azure</a>, SEO Checker</p>

<h2>Extensibility</h2>

<p>A lot of the extension points in Umbraco 8 still exist reasonably unchanged in v9. Content Apps, dashboards and front-end addons can still be created in code or <code>package.manifest</code> files. But for C# 'custom code' there are some changes.</p>

<h3>Events/Notifications</h3>

<p>If you wanted to extend Umbraco in the past, there's a high chance you'd turn to events. But events no longer exist in v9. (There's a pun in here somewhere about "events being a thing of the past" but I can't work it out.)</p>

<p>Instead we have notifications.</p>

<p>We still create a composer, but instead of reaching for <code>ContentService.Saved +=</code>, we're going to add a notification handler.</p>

<pre><code>public class TestComposer : IUserComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.AddNotificationHandler&lt;Umbraco.Cms.Core.Notifications.ContentSavedNotification,
            MyContentSavedHandler&gt;();
    }
}
</code></pre>

<p>So in our composer, we trigger an <code>AddNotificationHandler</code> with a type parameter of the relevant notification class. In the example above, I'm using a <code>ContentSavedNotification</code> which is the replacement for <code>ContentService.Saved</code>.</p>

<p>The easiest way to work out what notification you need, is to use Intellisense. Fill in the namespace <code>Umbraco.Cms.Core.Notifications.</code> and then hit <kbd>Ctrl</kbd> + <kbd>Space</kbd>.  You'll see a whole list of them pop up.</p>

<p><img src="/media/t5wgmaa0/image-20210605153020502.png" alt="Intellisense popup with all the Notification classes" /></p>

<p>The next parameter is your notification handler class. This will implement <code>INotificationHandler</code> (makes sense) with the notification type as the type parameter. Implementing the interface's <code>Handle</code> method, gives you an object very similar to the <code>ContentSavingEventArgs</code> you're used to from previous versions.</p>

<pre><code>using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;

namespace UmbracoProject.NotificationHandlers
{
    public class MyContentSavedHandler : INotificationHandler&lt;ContentSavedNotification&gt;
    {
        public void Handle(ContentSavedNotification notification)
        {
            foreach (var entity in notification.SavedEntities)
            {
                //Do things
            }
        }
    }
}
</code></pre>

<h2>Upgrading</h2>

<p>Your content will upgrade fine. There are no database schema updates so a v8 database will work in v9.</p>

<p>However, packages, views and custom code will be your issue.</p>

<p>This is how I'd approach a conversion:</p>

<ol>
<li>Check your packages exist in v9 or, failing that, check for alternative packages.</li>
<li>Check your Nuget packages for .NET 5 versions or alternatives</li>
<li>Install a new v9 instance of Umbraco</li>
<li>Point it at a copy of a v8 database and check the content works ([you can also use uSync to bring across schema and content](<a href="https://docs.jumoo.co.uk/uSync/v9/migration/">Umbraco v8 to v9 migration with uSync - Jumoo Documentation</a>))</li>
<li>Install relevant Umbraco and Nuget packages</li>
<li>Migrate your views and tweak any code to make it compatible</li>
<li>Migrate custom logic (using composers, components and the new notifications pattern)</li>
</ol>

<h2>What's next?</h2>

<p>Once you've got your head around .NET 5 and Umbraco 9, what else can we do with it?</p>

<h3>Docker/containerisation</h3>

<p>Containerisation makes a lot more sense now Umbraco is on .NET 5. It might be worth investigating, especially if you have a complex solution with multiple applications. However, I personally expect this will be overkill for the majority.</p>

<h3>Linux hosting</h3>

<p>Linux is cheaper than Windows. There are no licence fees which means lower start-up costs for VPS hosting and lower monthly cost for cloud hosting.</p>

<p>But do bear in mind the time and cost of testing your website on a new platform (case sensitivity isn't something we've had to think about much before!) and the time implications of learning the new skills required for DevOps on Linux.</p>

<h3>Swap out that IDE</h3>

<p>Visual Studio can be a little cumbersome. Once you've got your head around .NET 5, it might be worth looking at Visual Studio Code and the <code>dotnet</code> CLI. They're fast and they're modern. But they do take a little time to get used to.</p>

<h3>Linux &amp; MacOS dev machines</h3>

<p>Prior to being a .NET dev, I was a Linux user for many years. So once you've switched to Visual Studio Code, why not swap out that OS too?!</p>

<h3>Play!</h3>

<p>I think the most important next step of all is to play. We've got some new technology available to us that's faster, leaner and more modern. Other developers are excited about .NET 5 and there are some pretty neat projects popping up. Get inspired by them, and do some inspiration yourself - let's see what Umbraco 9 and .NET 5 can do!</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Mapping our journey to accessibility: What we can learn about accessibility from maps</title>
<link>https://joe.gl/ombek/blog/maps-a11y/</link>                <pubDate>Thu, 20 May 2021 02:20:22 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/maps-a11y/</guid>
                <enclosure url="https://joe.gl/media/rp0deiop/annie-spratt-afb6s2kibuk-unsplash.jpg" length="3106826" type="image/jpg" />
                <description>Accessibility is a fairly new concept in the world of the web. The web itself is only in its early 30&#39;s (not much older than myself!) and I think it&#39;s fair to say that accessibility wasn&#39;t a priority in Web 2.0. To catch up, it makes a lot of sense to stand on the shoulders of giants - what can we learn about accessibility from the real world that we can apply to the web?</description>
                <content:encoded><![CDATA[
                    <p>Accessibility is a fairly new concept in the world of the web. The web itself is only in its early 30's (not much older than myself!) and I think it's fair to say that accessibility wasn't a priority in <em>Web 2.0</em>. To catch up, it makes a lot of sense to stand on the shoulders of giants - what can we learn about accessibility from the real world that we can apply to the web?</p>
<p>Although my day job is as a backend web developer, I take a lot of interest in the frontend world and deem myself a bit of an accessibility advocate. I'm also a real outdoors enthusiast, but we'll come on to why that's relevant later.</p>

<p>The real world contains thousands of examples of accessibility. From accessible parking spaces and accessible toilets to tactile paving, ramps and hearing loops. But it can be quite difficult to find parallels to some of the issues we come across on the web.</p>

<p>If only there was something from the analogue world that conveyed information in a visual way, more like a website...</p>

<h2>Let's talk about maps</h2>

<p>Outside the office, I'm a real outdoor enthusiast. Generally accompanied by my girlfriend Kirsty, a geographer, and my dog, Carter; we enjoy hiking, camping, canoeing and much more. Many of these activities require the use of maps for our enjoyment and safety.</p>

<p>What with me being an avid map-user and my girlfriend being a geographer, one day we started to discuss the accessibility of maps, and I realised there were many parallels we could draw with the web.</p>

<h3>Some caveats</h3>

<p>We're going to take a look at how maps are an accessible method of <strong>visually representing data</strong>. These outdoor maps I'm going to be talking about are pretty poor at highlighting accessible facilities or routes. You won't find accessible toilets, paved footpaths, footpaths without styles, etc. marked on these maps. I do, however, think that as a visual method of representing data, these are a good example. Even if they don't convey information about accessible features.</p>

<h3>Why maps?</h3>

<p>Mapping has been around for a long time and the web, well, hasn't.</p>

<p>Mappa mundi, crude religion-centred maps of the known world, were created in the early 14th century and serious mapping of English towns began in the 16th century. While Ordnance Survey- who's maps I use regularly - started their work in 1791. What have Ordnance Survey (OS) done in those past two centuries to improve their maps usability, and what can we learn from them?</p>

<h3>Maps used in this blog post</h3>

<p>I'm using Ordnance Survey maps for the purposes of this blog post. You should be able to pan around and zoom from a 1:25,000 to a 1:50,000 scale map.</p>

<div data-map="54.4303727,-2.9624777"></div>

<h3>Low barrier to entry</h3>

<p>If I gave you a map you'd never seen before and asked you to point out a lake, a river, woodland or roads I'm sure many of you would have little issue in doing so. They use logical colours, shapes and symbols which reflect the real world.</p>

<p>Take a look at the car park "P" symbol, the man and woman symbol denoting the public toilets and various icons related to boats. Also note how the water is blue and the woods are green with tree symbols.</p>

<div data-map="54.420199,-2.962026" data-map-zoom="15"></div>

<h3>...With details if you know where to look</h3>

<p>A more experienced map reader can identify hilltops, ridges, valleys, and steep cliffs all from the shapes of the thin brown contour lines. You can even tell whether a place of worship has a spire or a tower or if woodland is deciduous, coniferous or a mixture.</p>

<p>Power lines, boundaries (normally fences) and the type of footpath can also be read from OS maps, but they don't provide too much noise to distract from the basics.</p>

<div data-map="54.430647,-2.994989" data-map-zoom="15"></div>

<h3>Levels of detail</h3>

<p>In a digital map, like Google Maps or OSM, more or less detail can be rendered depending on the zoom level. Zoomed out, we see the bigger picture: town names and major roads but as we zoom in, we see minor roads and buildings beginning to appear as well as names of smaller areas and more points of interest. We may even lose sight of the bigger picture things like town or city names because they become less appropriate as we zone in.</p>

<p>Print maps do this too. Ordnance Survey release several scales of map each with different levels of detail. The 1:25,000 scale (4cm on paper is 1km in the real world) is commonly used by hikers so they can see the intricacies of tiny crossing footpaths.</p>

<p>Cyclists, however, can travel so far in a day as to render a 1:25,000 scale map useless! 1:50,000 is more common (2cm to 1km) for those on two wheels. You don't see so many little footpaths, but the main cycle tracks are clearly marked and there's enough detail to appease the faster-traveller.</p>

<h3>Several means to an end</h3>

<p>Heights are provided as spot heights, contour lines and contour numbers and a reader can use whichever method is most appropriate. An approximate height o be climbed on a hike can be taken from the spot heights at the top of peaks, but for a more detailed estimate, you can count the ups-and-downs along a route using contours.</p>

<h3>A good solid data structure</h3>

<p>Generally speaking, the more important the location the more prominent its name will appear on the map. This makes a lot of sense. If I want to find Lougrigg Fell, I'd have very little chance of finding it unless I look first for the county of Cumbria, then for Ambleside and finally for Lougrigg. The prominence of these place names, therefore, reflects that structure making small places easy to find.</p>

<h3>Low reliance on colour</h3>

<div data-map="54.4303727,-2.9624777" style="filter: grayscale(1);"></div>

<p>A good map will generally use high contrasting colours to help different features stand out. Text is often outlined in white (or carefully positioned on manually designed maps) to make it stand out further from the background.</p>

<p>Shaded areas such as woodland or grassland will usually have a tree or grass symbol, so they can be identified without relying on colour.</p>

<p>The OS and OSM maps work in monochrome (as anybody with a budget-conscious photocopier-happy secondary school geography teacher will appreciate!) which makes them colour-blind friendly.</p>

<p>Where colour is used, it's used to enhance. The text colour on these maps is contextual to help identity what a label is labelling: a river is labelled in blue while woodland is labelled in green.</p>

<h2>How about severe visual impairment?</h2>

<p>Unfortunately, paper maps don't work well for people with severe visual impairment or blindness. However, many digital mapping apps provide a reasonable navigation experience with a screen reader.</p>

<p>It's also possible to print 3D maps based on OSM data using services like <a href="https://touch-mapper.org">Touch Mapper</a>.</p>

<p>Open Street Map has several projects "to provide accessibility to the OSM services for blind and visually impaired persons". More information is available on the <a href="https://wiki.openstreetmap.org/wiki/OSM_for_the_blind">OpenStreetMap wiki</a></p>

<h2>Takeaways</h2>

<p>From this analysis, I think there are a few pieces we can take away and apply to our industry.</p>

<h3>Avoid information overload</h3>

<p>Although there's a lot of information detailed in the maps we've looked at today, they don't distract from the basic information.
On the web, we should be careful to only include the information people need, avoiding waffle and information the user is unlikely to want or need. We can summarise the content of a page early on to save users reading through all the detail if they don't need to.</p>

<h3>Provide more information for those who want it</h3>

<p>Simplifying information doesn't mean we need to trivialise it. Provide more in-depth information further down in an article or by linking to other pages which go into more detail on a specific subject.</p>

<h3>Data structure</h3>

<p>It may seem obvious to give bigger towns larger text on maps, but we also need to ensure sections are clear on our websites to allow for skimming. Use appropriate headings (<code>h1</code>-<code>h6</code>) and semantic markup. This applies to your site as a whole as well - think about site structure and menus.</p>

<h3>High contrast</h3>

<p>There's really no reason not to! Check your colours using contrast checkers and make sure any text overlaying an image will always meet the requirements! It might be worth suggesting brand guidelines are amended.</p>

<h3>Don't depend on colour</h3>

<p>But it can be very useful to provide supplemental information. If you're using colour to convey something important, also use an icon or pattern.</p>

<h3>Provide more than one way to obtain information</h3>

<p>Cross-link to related content and provide key information from a graph in text too. Make sure your users can find that important information!</p>

<h3>Look to the real world for answers to your web accessibility problems</h3>

<p>But the most important takeaway from this is that if we come across an accessibility problem we're trying to solve, we can look to the real world to find solutions - we're not limited to the comparatively inexperienced world of web accessibility.</p>

<script>
    var maps = {
        sessionKey: undefined,
        maps: [],
        draw: function() {
            var elems = document.querySelectorAll('[data-map]');
            function drawMap(elems, i) {
                var elem = elems[i];
                if(elems.length >= i+1) {
                    var data = elem.dataset.map.split(',');
                    var lat = parseFloat(data[0]);
                    var lon = parseFloat(data[1]);
                    var map = new Microsoft.Maps.Map(elems[i], {
                        credentials: maps.sessionKey || 'AnZSOI_72o2RqtxUDuLAEFMU_IRuodD1IME1M4KYWvs2h781HhkueJWDM5ig2RzC',
                        center: new Microsoft.Maps.Location(lat, lon),
                        mapTypeId: Microsoft.Maps.MapTypeId.ordnanceSurvey,
                        zoom: parseInt(elem.dataset.mapZoom) || 15,
                        minZoom: 13,
                        maxZoom: 15,
                        disablePanning: false,
                        disableZooming: false,
                        enableClickableLogo: false,
                        showDashboard: true,
                        showLocateMeButton: false,
                        showMapTypeSelector: false,
                        showScalebar: false,
                        showZoomButtons: true
                    });
                    maps.maps.push(map);

                        drawMap(elems, ++i);
                }
            }
            drawMap(elems, 0);
        }
    };
    var drawMaps = maps.draw;
</script>

<script type='text/javascript' src='//www.bing.com/api/maps/mapcontrol?mkt=en-GB&callback=drawMaps'></script>

<p><style>
    [data-map], [data-video] {
        display: block;
        position: relative;
        height: 500px;
        height: 65vh;
    }
    @media only screen and (max-width: 900px) {
        [data-map], [data-video] {
            height: 45vh;
        }
    }
    @media only screen and (max-width: 500px) {
        [data-map], [data-video] {
            height: 30vh;
        }
    }
</style></p>                ]]></content:encoded>
            </item>
            <item>
                <title>I moved my website to Umbraco Cloud, this is how I got on</title>
<link>https://joe.gl/ombek/blog/move-to-umbraco-cloud/</link>                <pubDate>Mon, 21 Dec 2020 05:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/move-to-umbraco-cloud/</guid>
                <description>One weekend. One website. One rebuild.</description>
                <content:encoded><![CDATA[
                    <p>One weekend. One website. One rebuild.</p>
<p>I have a confession to make. Up until this weekend, my website was hosted on Squarespace.</p>

<p>This may seem counter-intuitive, for a software developer to use a service like Squarespace, but it made a lot of sense at the time. I needed a website fast while I was working full-time and didn't have the time or energy to build a site from scratch.</p>

<p>I've mentioned in the past how <a href="https://joe.gl/ombek/blog/integrate-dont-imitate/">I'm not opposed to using third-party services where it makes sense</a>, and this was certainly one of those occasions.</p>

<p>Squarespace is, however, not without its faults: accessibility is poor and there aren't many ways to improve it; the drag and drop designer is a pain when you want consistency and repetition; and my CV on the Squarespace site never looked quite right on mobile. So, combined with the fact that I'm an Umbraco developer without an Umbraco website,  it was time for a change.</p>

<h2>Trial signup</h2>

<p>Investing evenings and weekends in building something I do for my day job has never really appealed to me, so I thought I might try out Umbraco Cloud. I like the idea of patch upgrades happening automatically and other upgrades possible without cloning locally. This means that going forwards I have less admin to do to keep my site up <em>and</em> up-to-date. Using Umbraco Cloud also meant that I'd have more recent practice using Cloud, so I was at least learning something from my endeavour (I haven't used Cloud since it was in preview and called UaaS!)</p>

<p>Signing up for the trial was fairly easy and promised 14 days to get my site in order before launching and paying for it. I had a repo up in no time.</p>

<h2>Developing locally</h2>

<p>Although Cloud allows fully-online development (more on that later), I'm stuck in my ways. So decided to try out developing the Umbraco site locally. Developing locally enables compiled libraries for custom helpers and extension methods and installing packages via Nuget (sorry, Umbraco Packages!) which is all good practice as far as I'm concerned.</p>

<p>Cloud is built around Git. Any changes you make are committed to Git and this automatically triggers a deployment to the Cloud site. In theory, this means working locally is as simple as cloning the URL Cloud provides and hitting run. But it's not <em>quite</em> that simple because Cloud runs ASP.NET <em>Websites</em> rather than their <em>Web Application</em> counterpart, meaning there is no Visual Studio Solution that's source controlled. Umbraco, therefore, provide UaaS.cmd to help simplify this process.</p>

<p>Foolishly, I initially only skim-read the <a href="https://docs.umbraco.com/umbraco-cloud/set-up/working-locally#adding-a-solution-file-to-your-cloud-project">documentation</a> here before diving in. But after following the official guidance, this process was actually quite simple.</p>

<p>Then, after configuring my two (that's how it works!) git repos (and also setting up a mirror for the Umbraco Cloud repo because, although not necessary, I couldn't help feeling it was better practice somehow), I hit the run button...</p>

<p>Well, that didn't work. It's at this point I'd usually document the steps I went through to get it working properly but, in all honesty, I had no idea what I did to get it happy again. It was a bit of trial and error and likely specific to my setup, so we shan't dwell on this! The important thing is that it's working now!</p>

<h2>Project setup</h2>

<p>Let's take a moment to discuss the setup when developing locally.</p>

<p>UaaS.cmd sets up two Git repositories, one at the root (that you need to source control yourself) and one in <code>*.Web</code> (where <code>*</code> is the name of your project).</p>

<p><code>*.Web</code> contains your Umbraco site as an ASP.NET <em>Website</em> (not Web Application - there is no <code>.csproj</code> file).</p>

<p>The root directory contains the solution file as well as a folder and <code>csproj</code> file called <code>*.Core</code> which, according to <a href="https://docs.umbraco.com/umbraco-cloud/set-up/working-locally#working-with-visual-studio">the docs</a>, is where controllers, models, data access and extension methods should ideally live.</p>

<h3>Models!?</h3>

<p>Using ModelsBuilder is a bit of a pain. To get Intellisense working, <a href="https://docs.umbraco.com/umbraco-cloud/set-up/working-locally/legacy-umbraco-visual-studio-setup#using-modelsbuilder-and-intellisense">the docs</a> recommend using ModelsBuilder mode <code>AppData</code> (classes are generated upon request) or <code>AppDataLive</code> (classes are generated automatically when models change) and then overriding the path to place them in the <code>App_Code</code> folder, where they'll get compiled on the fly. This is quite clever. Visual Studio can pick these up automatically as ASP.NET Websites include files automatically (some of the time... it involves a lot of hitting the "Refresh" menu option) and they're compiled dynamically meaning you can change models on Umbraco Cloud (not developing locally) and they'll update dynamically.</p>

<p>However, this means that the <code>*.Core</code> project has no access to any ModelsBuilder models, which makes writing some of my common extension methods difficult. Normally I'd create a <code>SiteSettings()</code> extension method on IPublishedContent to get the settings node but, without access to models, I resorted to a generic method <code>GetSite&lt;Settings&gt;()</code> where Settings is the model for my Settings node.</p>

<pre><code>public static T GetSite&lt;T&gt;(this IPublishedContent page) where T : class, IPublishedContent
{
    return page.Root().FirstChild&lt;T&gt;();
}
</code></pre>

<p>There are other options here but they all have their own downsides. You can the <code>*.Core</code> project altogether and put all your code in the <code>~/App_Code</code> folder, but this means you miss out on any compilation ahead of time. You can also <a href="https://our.umbraco.com/forum/umbraco-cloud/83723-modelsbuilder-generate-models-to-umbraco-cloud-core-project">set your models to be generated within the <code>*.Core</code> project</a>, but then you won't be able to utilise the ability to develop on the live site (more on that later). A third option may be (I've not yet tested it) to use the <code>Dll</code> or <code>LiveDll</code> ModelsMode (in the <a href="https://github.com/modelsbuilder/ModelsBuilder.Original">full version of ModelsBuilder</a>) and reference the generated DLL in your <code>*.Core</code> project. This allows for development on the live site, but means you'll have to either manually generate models (<code>Dll</code>) or have site restarts every time you change a document type (<code>LiveDll</code>).</p>

<h3>Website vs. Web Application</h3>

<p>As indicated above, a website is mildly annoying to have as opposed to a web application. I've always seen applications as more superior, but the only reason I think this would be is because an application can generate DLLs, while websites can only have C# in the <code>App_Code</code> folder, where it's only compiled on application startup (think of it as the opposite of <a href="https://gunnarpeipman.com/asp-net-mvc-precompiling-views/">precompiled views</a>).</p>

<h3>Being a git about Git</h3>

<p>And now, lets get onto the Git setup. This is a little weird. If you want to use Visual Studio to build your Umbraco site, the docs suggest you'll need two git repositories (although if you're feeling brave I'm sure you could use one and then use subtree or submodules and mirror to the Cloud repo... but that's a story for another day!): one for the <code>*.Web</code> folder, and one for everything else. The <code>*.Web</code> repo is hosted by Cloud (and is where it deploys from), while you'll need to create your own repo for the rest. Although I'm sure it's unnecessary, I also decided to mirror the Cloud repo because it feels like the right thing to do!</p>

<p>It's not really an issue, just a mild niggle. I'm sure someone more confident with Git than I could find a nicer way to handle this setup, but this one will do for now.</p>

<h2>Umbraco Deploy</h2>

<p>I've heard bad things about it but so far, for me, Umbraco Deploy has just worked. When Umbraco releases Deploy for non-cloud projects, I'm sure I'll have to do a full comparison between Deploy and uSync.Publisher, but for now I'm happy to use Deploy for Cloud and uSync for everything else.</p>

<h2>Ability to develop on the live site! 🤠</h2>

<p>I've always found the Settings area of Umbraco a bit odd, personally I'd rather do all my development within my IDE not partially in the browser. Working in a CI/CD world, I'd also have to disable the Settings section in any environment except locally.</p>

<p>But in Cloud, the Settings section makes so much sense for the simple reason that any changes you make on your Cloud environments and committed to the Git repo automatically. This makes my inner "<a href="https://en.wikipedia.org/wiki/Cowboy_coding">cowboy coder</a>" very happy. If I need to hotfix a live site, I can! If I want to tweak code from my phone, halfway up a mountain, I can! If I'm too lazy to open up Visual Studio to make a minor change to my personal site, I can!</p>

<p>Cloud is where Umbraco seems most at home, largely because of this.</p>

<h2>Domain configuration... AKA 9 hours of downtime</h2>

<p>I assume due to how Umbraco Latch works, domains will need to be configured to point to the Cloud site before an SSL certificate is generated. Unfortunately for existing sites, this means there will be a short moment of downtime while your domain points to a site with an invalid HTTPS certificate. However, in my case, basic authentication was not automatically disabled when I purchased my Cloud license (and by the time I realised it was too late!)</p>

<p>I decided to leave the setup overnight in the hope the problem would resolve itself with time, which it, unfortunately, did not. Support was, however, very helpful and resolved the issue quickly after I notified them of the issue.</p>

<p>Azure Web Apps allow domains to be preconfigured (with DNS text records) prior to mapping the domain, which may mean as <a href="https://umbraco.com/blog/the-future-of-umbraco-cloud/">Umbraco Cloud moves to Web Apps and uses Cloudflare</a> in place of the existing Latch process for HTTPS, we may see an improvement here.</p>

<h2>Umbraco cloud issues</h2>

<p>I don't think it's unfair to say that Umbraco Cloud has a bit of a reputation when it comes to stability... and not a good one. And so far, I'm not helping that reputation, having already detailed an issue I had with my site not coming out of trial mode.</p>

<p>I did also experience a short amount of downtime on Saturday night.</p>

<p>But that's it so far. And support has been great at fixing the issues I have had.</p>

<p>I also had an issue with a bug in Umbraco Forms, which the support team fixed without having to delve into the database myself.</p>

<h2>The price is right... sometimes</h2>

<p>So who is this for? It's a question that's asked again and again. And I wasn't sure until I tried it properly myself.</p>

<p>I think the answer is, Umbraco Cloud is great for me. My use case is that I have a small low-traffic site which I don't want to have to invest huge swathes of time keeping patched and up to date, but I do want to quickly develop and launch new features.</p>

<p>It's not ideal for complex custom setups, or sites requiring "four-nines" of uptime. It might also outprice individuals or small charities. But for those in the middle, it seems to suit quite well. I won't be recommending Cloud to every client, but I think I now know who the target market is, and will recommend to those clients going forwards.</p>

<p>It will be interesting to see how <a href="https://umbraco.com/blog/the-future-of-umbraco-cloud/">the new planned infrastructure for Cloud</a> will affect this target demographic and whether we'll see a more stable platform going forwards.</p>

<p>As for the relatively high cost vs self-hosting, I think this comes down to the quality and speed of the support team. If you consider the month price as a time-saver, it's easily worth it in my opinion.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>How to set Windows Terminal as GitKraken&#39;s default terminal</title>
<link>https://joe.gl/ombek/blog/gitkraken-and-windows-terminal/</link>                <pubDate>Fri, 11 Dec 2020 06:34:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/gitkraken-and-windows-terminal/</guid>
                <description>GitKraken allows you to open the current repository in the terminal by hitting Alt + T but by default, this won&#39;t be the new Windows Terminal.</description>
                <content:encoded><![CDATA[
                    <p>GitKraken allows you to open the current repository in the terminal by hitting <kbd>Alt</kbd> + <kbd>T</kbd> but by default, this won't be the new Windows Terminal.</p>
<p>The new Windows Terminal has many benefits over the Command Line or Powershell applications but you won't find Windows Terminal as an option for the default terminal in GitKraken's settings.</p>

<p>There is a simple fix, however. Open up GitKraken's preferences (File > Preferences) and scroll down to and check "Use Custom Terminal Command" (below "Default Terminal" dropdown). Then, in the "Custom Terminal Command" field that appears above, enter:</p>

<pre><code>wt -d %d
</code></pre>

<p><code>wt</code> is the command for Windows Terminal, to which we're passing the <code>-d</code> directory parameter. The value of <code>%d</code> is then swapped out by GitKraken for the directory of the current repository.</p>

<p>It's as simple as that!</p>

<p><del>At the time of writing, there is a bug in GitKraken which launches the terminal twice when using the File > Open Terminal option, but <kbd>Alt</kbd> + <kbd>T</kbd> works just fine.</del></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Taking Friendly to the masses</title>
<link>https://24days.in/umbraco-cms/2020/friendly-to-the-masses</link>                <pubDate>Thu, 10 Dec 2020 08:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://24days.in/umbraco-cms/2020/friendly-to-the-masses</guid>
                <enclosure url="https://joe.gl/media/8d8a66c5cd1d5f9/aidan-de-la-paz-pdtkebjixw0-unsplash.jpg" length="2376174" type="image/jpg" />
                <description>Umbraco is &quot;The Friendly CMS&quot; and, as a community, we aim to be warm and welcoming, greeting everyone we encounter with a Friendly high-five. But how can we inject some of this Friendliness into our work and make the websites we craft as Friendly as possible?</description>
                <content:encoded><![CDATA[
                    <p>Umbraco is "The Friendly CMS" and, as a community, we aim to be warm and welcoming, greeting everyone we encounter with a Friendly high-five. But how can we inject some of this Friendliness into our work and make the websites we craft as Friendly as possible?</p>
<blockquote>
  <p><strong>friendly</strong><br />
  behaving in a pleasant, kind way towards someone</p>
</blockquote>

<p>To be Friendly to everyone, we need to make sure everyone can use our websites. This simple concept is the whole premise behind, what may initially sound daunting, accessibility. Contrary to popular belief, accessibility isn't all about screen-readers, it's about the usability of your website to anyone, no matter their personal needs.</p>

<blockquote>
  <p><strong>accessibility</strong><br />
  the quality or characteristic of something that makes it possible to approach, enter, or use it</p>
</blockquote>

<p>Accessibility of the Umbraco back-office has started to take shape over recent months: in any new Umbraco release, there’s been a whole section of the release notes dedicated to accessibility improvements. This has brought the concept of accessibility to more and more community members, but to some, an accessible website still seems like a distant pipedream - at best a low priority ticket on a backlog and at worst dismissed in the planning stages.</p>

<p>But I’m here to challenge that and show you how building a more accessible site is easier than you might think, whether you're a developer, designer or working in management, sales and delivery.</p>

<h2>Accessibility as a priority</h2>

<p>Of course, time and budget dedicated to making your website as accessible as possible is the best option and there are many reasons to do so, morally and financially.</p>

<p>We think of the web as open-access and a level playing field giving everyone access to the same information. And it is. For the most part. Building accessible websites is a very simple way to open up information and services to more people, ensuring people are not unfairly restricted by their circumstances.</p>

<p>It’s also important to remember that these are customers. Just as a shopkeeper wouldn’t put a barrier across their entrance only serving those who could jump it, why should a website put up these virtual hurdles for a percentage of potential customers?</p>

<p>I’m sure you’re all aware of <a href="https://www.bbc.co.uk/news/technology-46894463">the Domino's Pizza lawsuit that hit the US Supreme Court last year</a>, but other big names like <a href="https://thenextweb.com/tech/2020/06/24/why-your-websites-lack-of-accessibility-options-is-opening-you-up-to-lawsuits/">Nike, PornHub and Beyonce also got taken to court in America</a> with other countries catching on. Web accessibility will soon be a must-have for all big corporations and any website built today should be taking this seriously.</p>

<h2>It’s just UX</h2>

<p>User experience (that’s UX to those in the know) designers pride themselves in designing great experiences for users (I guess the clue is in the name!) rather than something that simply looks good.</p>

<p>No surprises here, that this way of thinking generally includes those with access issues. As the word “accessibility” implies, this kind of UX doesn’t exclusively help those with specific needs - it’ll help everyone.</p>

<p><img src="/media/8d8a66c5cd1d5f9/aidan-de-la-paz-pdtkebjixw0-unsplash.jpg" alt="" /></p>

<p>Any user of a shiny phone screen on a bright sunny day will appreciate high contrast ratios between text and the background, while any person who’s had an exhausting day will appreciate concise language and clear actions. Not to mention the 1 in 8 men who are colourblind (myself included) and the huge proportion of us who will develop motor, sight or hearing-related issues in later life.</p>

<p>Accessibility is vital for some and useful for everyone. Accessibility is a part of UX, so let’s be sure to embrace it and hold it in as high regard as visual appearance.</p>

<p>As a designer, thinking about how a user might interact with your design is a part of the job and accessibility fits right in with the other best practices you’re already following. There’s nothing extra here except a few additional things to think about and the more accessible designs you produce, the easier it becomes.</p>

<h2>Coding is key</h2>

<p>As developers, the way we code our sites makes all the difference. Even if there's no special budget set aside or we've received a design that hasn't fully considered accessibility (or there's no design at all!), we can still make all the difference.</p>

<p>It's not that different from coding without considering accessibility: just as we support browsers by writing valid markup, we can support as many people as possible by writing valid, semantic markup. The key to accessibility for developers is that actually, most HTML we write is probably accessible already. HTML renders in an easy to read and understand way and if we’re using the right semantic markup, it goes a long way to being understood by screen-readers too. It’s good practice to use the right elements for the right things: a nav for all navigation areas; main for the primary content of your site; header, footer and section elements for splitting up the page; and by <a href="#headings">using headings correctly*</a>.</p>

<p>The trouble comes with JavaScript and CSS.</p>

<p>When styling, a good indication of breaking some bad practice is if you set a style to none or inherit. Browsers almost always have a reason for styling something a certain way, and if you’re undoing that it could make it harder to use.</p>

<h2>Accessible accessibility tips</h2>

<p>For you designers, developers and testers reading this, here are some of my top tips for starting on your accessibility journey. There is a lot more to learn out there (<a href="https://24days.in/umbraco-cms/2015/a-web-for-everyone/">Jeffrey and Jorgen's article from 2015 is a good place to look for expanding your knowledge next</a>) but this is a good place to get started.</p>

<p>I have 3 golden rules for styling accessibly.</p>

<ol>
<li><h3>Let me <em>outline</em> this for you</h3>

<p>An outline is the browsers’ default way of showing focus - by removing this, it might make navigating your site by keyboard impossible! Consider changing the outline colour rather than removing it completely, or use some other clear styling to highlight focus.</p></li>
<li><h3>Which <em>links</em> nicely onto...</h3>

<p>By removing the colour and underline from a link, you make it less clear that the text is actually a link! Even changing a link from blue to any other colour can make navigation confusing to some users.</p>

<p>If you’re trying to make your link look like a button, you might confuse people even more.</p></li>
<li><h3>Things that look like things they are not</h3>

<p>Using elements for things they are not is another red flag. Using a link as a button or button as a link means the user doesn’t get the functionality they expect from that kind of element - links navigate you to somewhere else, while a button interacts with the current page. A checkbox is a form field, not <a href="https://css-tricks.com/the-checkbox-hack/">a toggle for a menu</a>.</p>

<p>It’s not necessarily wrong to use an element for something different, but it is a warning sign. So if you are, you should be sure to try using the component with a screen-reader (I recommend using <a href="https://www.nvaccess.org/about-nvda/">NVDA</a> over Windows Narrator - it's free and very popular) and only using keyboard navigation. If something is unclear or doesn't function as expected, use ARIA attributes and JavaScript to correct any confusing behaviour.</p></li>
</ol>

<p><a href="https://codepen.io/glombek/embed/MWeMbev?default-tab=result">See the Pen</a> by glombek (<a href="https://codepen.io/glombek">@glombek</a>) on <a href="https://codepen.io">CodePen</a>.</p>

<h3>Don't <em>label</em> me a cynic</h3>

<p>Moving on from styling, another really quick tip to make your site accessible is to label things. If an input has no label, or a section has no heading, consider adding one that's “<a href="https://css-tricks.com/inclusively-hidden/">visually hidden</a>” or through aria-label. Similarly, if you have an icon with no text, it’s generally worth adding some text to help people understand its meaning (<a href="https://vanseodesign.com/web-design/hamburger-icon-debate/">it might not be as obvious as you think</a>), but if your design prohibits this, aria-label it!</p>

<p>Conversely, an icon that has no semantic meaning, can be aria-hidden so that it’s not read out by a screen-reader. An example of this would be an icon with a label next to it - the icon adds no additional meaning to the label, so a screen-reader doesn’t need to read it out.</p>

<h3><em>Alternative</em> thinking</h3>

<p>Alternative text (alt attribute) on images have a couple of really handy uses. Firstly, if your image can't be loaded for whatever reason, it is replaced by the alt text to give an idea of the context it was meant to provide. Secondly, alt text can provide an audible description of the image for blind users and more clarity to a partially sighted user.</p>

<h3>*A note on headings</h3>

<p>Although it’s valid HTML to use an h1 per section, some screen-readers (as well as search engines) struggle to understand the structure of the content like this so it’s still considered best practice to use one h1 element per page.</p>

<h2>In inconclusive conclusion</h2>

<p>These are just a few pointers to start you on your accessibility journey. I'm by no means an expert and this is by no means all there is to know but I am keen to make my sites as accessible as possible and learning more every day, which I hope I have managed to rub off on you too! Don't hesitate to <a href="https://twitter.com/JoeGlombek">reach out to me</a> if you have any accessibility queries or get involved with the <a href="https://community.umbraco.com/community-teams/the-accessibility-team/">Accessibility Team</a>, we're all quite Friendly.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Easily open uSync Sync-Pack files</title>
<link>https://joe.gl/ombek/blog/open-usync-sync-pack-files/</link>                <pubDate>Tue, 16 Jun 2020 12:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/open-usync-sync-pack-files/</guid>
                <description>uSync Sync-Pack files (with the .uSync extension) are simply ZIP files and can be viewed in any ZIP viewer, including the one built into Windows.</description>
                <content:encoded><![CDATA[
                    <p>uSync Sync-Pack files (with the .uSync extension) are simply ZIP files and can be viewed in any ZIP viewer, including the one built into Windows.</p>
<p>One option is to rename your Sync-Pack to have a .zip extension and then open it as you would any compressed folder.</p>

<p>The other option, however, which is very useful if you’re looking inside uSync files more often, is to let Windows know that the .uSync file is a ZIP file and to treat it as such.</p>

<p><img src="/media/8d890a7556cb00f/annotation-2020-06-16-123619.png" alt="Screenshot of a uSync Sync-Pack file with a ZIP file icon" /></p>

<p>There’s no point-and-click setting for this like there is for associating a filetype with a program because we’re doing something subtly different: telling Windows this file is a ZIP file.</p>

<p>All you need to do, though, is open a command prompt as administrator and run:</p>

<pre><code>assoc .uSync=CompressedFolder
</code></pre>

<p><img src="/media/8d890a75507666d/annotation-2020-06-16-123452.png" alt="Screenshot of a Command Prompt with the assoc command entered" /></p>                ]]></content:encoded>
            </item>
            <item>
                <title>Integrate, don&#39;t imitate - when an API beats rolling your own</title>
<link>https://joe.gl/ombek/blog/integrate-dont-imitate/</link>                <pubDate>Wed, 06 May 2020 12:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/integrate-dont-imitate/</guid>
                <enclosure url="https://joe.gl/media/8d890a0d9cea932/ray-berry-okppyznbsha-unsplash.jpg" length="841845" type="image/jpg" />
                <description>It wasn’t long ago that every agency and freelancer had their own custom CMS they’d use for client projects. We decided that was a bad idea. But we’ll often be seen writing custom code within that standardised CMS. Are we making the same mistakes again?</description>
                <content:encoded><![CDATA[
                    <p>It wasn’t long ago that every agency and freelancer had their own custom CMS they’d use for client projects. We decided that was a bad idea. But we’ll often be seen writing custom code within that standardised CMS. Are we making the same mistakes again?</p>
<p>This blog accompanies my talk at the <a href="https://www.meetup.com/Thames-Valley-Umbraco-User-Group">Thames Valley Umbraco User Group</a> on 30 April 2020. I also have <a href="https://www.beautiful.ai/player/-M7qx4D9k4IkrxrkBFvn">the slides for my talk</a> available.</p>

<p>Let’s start with a couple of scenarios.</p>

<h2>Scenario 1: John the roller coaster enthusiast</h2>

<p><img src="/media/8d890a0d9cea932/ray-berry-okppyznbsha-unsplash.jpg" alt="Man pointing at a rollercoaster in the distance" /></p>

<p>Meet John. He’s a rollercoaster enthusiast and he’s decided that he wants to ride a roller coaster at least once a week. He’s got a good budget and money is no object.</p>

<p>How could we solve his problem?</p>

<h3>Option 1: Build a roller coaster in John’s back garden</h3>

<p>We could recommend to John that we build a roller coaster in his back garden. This could be quite fun and he could ride the roller coaster as often as he liked, with no need to queue. But on the downside, the estimated cost of this solution is £2.5 million to £25 million (<a href="https://www.bbc.co.uk/news/technology-24553630">source: BBC</a>).</p>

<h3>Option 2: Go to a theme park</h3>

<p>Alternatively, we could just recommend that he goes to his local theme park. He’d have his choice of various rides with no build-time, planning permission, health and safety, insurance, etc.</p>

<p>As for cost, a “premium season pass” at a well-known theme park costs £85 (notice the lack of the word “million” as a suffix), plus the cost of fuel (which, no matter how far you’re travelling is negligible, again due to the lack of the “million” suffix).</p>

<p>I’ll let you decide which option to pitch to John.</p>

<h2>Scenario 2: National Bushcraft Association website</h2>

<p>Some of you may know about the time I lived in the woods for 6 months and taught wilderness survival skills to school children.  So, I've created a scenario around the fictional National Bushcraft Association, a membership organisation for bushcraft instructors.</p>

<p><img src="/media/8d890a0d851a035/lum3n-ck3hfww2oim-unsplash.jpg" alt="A bushcraft knife and an axe laying on a tree stump" /></p>

<p>Members pay an annual fee to gain benefits such as networking, client referrals, news sharing capabilities and the obligatory newsletter.  They want a Content Management System (CMS) with member management functionality to manage membership fees, expiry of unpaid memberships and mass mailing.</p>

<p>So the obvious solution?</p>

<p>Umbraco! That’s my go-to CMS, and I love working with it. Of course, simply being my favourite CMS doesn’t justify the choice on its own, Umbraco is open source with an enthusiastic community meaning it's free and actively developed. Umbraco will easily cover the requirement the Bushcraft Association has for content management. It also has a highly extensible membership management system.</p>

<p>The membership management functionality in Umbraco by default, however, is comparatively basic. So we’d need to build custom functionality to allow sending newsletters, membership payment and expiring members who haven’t paid.</p>

<p>But wait! Isn’t this the theme park scenario all over again; are we building a roller coaster? And if so, what are our other (“season pass”) options here?</p>

<p>As for membership management, there are dozens of pre-built systems available that have this down to a tee, and many of them have an API we could integrate with. A third-party system would handle the membership fees, expiry and renewal reminders in the requirements, but also take GDPR off our hands and give us extra functionality to, for example, allow members to manage their own details and upgrade their membership tier as they need to, without phoning up the office team.</p>

<p>Similarly, a newsletter sending platform would handle sending newsletters and managing subscriptions and unsubscriptions as required while also handling GDPR and providing the Bushcraft Association with analytics.</p>

<p>In this scenario, I think building a basic CMS site and integrating with some third party services is the clear winner.</p>

<h2>The problem solver mindset?</h2>

<p>But where does this “backyard roller coaster” attitude in developers come from?</p>

<p>Personally, I think it’s due to the problem solver in us. We’re in this career because we like to solve problems and when someone gives us a brief we like to find an “ideal” solution, built exactly the way we think it should work.</p>

<p>As well as this, as a premium service provider, we might feel it’s our duty to build everything bespoke rather than “cheating” by using other people’s work.</p>

<p>It seems to take some maturity as a developer to realise we’re not taking the easy way out by using other people’s work but standing on the shoulders of giants.</p>

<p>We’re all too keen to use open source packages to speed up development and provide our users with a better experience, so why not complete services too?</p>

<h2>Benefits of integration</h2>

<p>For us developers, integrating makes life less repetitive, I get to spend more time writing new code rather than making poor imitations. Being honest, I just find building integrations more fun; I don't get too excited about another document type or building another news page. Writing more in-depth C# is more interesting to me!</p>

<p>But how do we sell an integration to higher-ups, project managers or the client?</p>

<h3>Over-delivery and extra features</h3>

<p>Integrating with more complete systems can result in over-delivery. Third-party systems likely have more features built into them over the years than you could do with the budget, the client’s going to get an industry-standard application, rather than the bare minimum.</p>

<p>Not to mention the fact that the client may get extra features “for free” as the third-party developer continues to improve their application over time.</p>

<h3>Less scope creep</h3>

<p>Since a client is buying into an existing system, chances are they can trial that system before they commit to using it. Since the client knows exactly what they’re getting and has already taken part of the system for a test drive, the requirements are less likely to change, reducing scope creep.</p>

<h3>Less support</h3>

<p>Since you’re building less, it means you handle less of the support after launch. There can be positives to this for the client too such as a reduced retainer cost and perhaps a better response rate than your team could provide. This also means that the client is less reliant on you or your company in the future, which could go a long way towards easing their minds!</p>

<h3>A solid platform</h3>

<p>By buying into an existing ecosystem, some of your application has been built and tested already.</p>

<h2>A balancing act</h2>

<p>Of course, it’s not always as simple as integrating. If nobody built their own roller coasters, there wouldn't be a theme park to buy an annual pass for!</p>

<p>It wasn’t long ago that every agency and freelancer had their own custom CMS they’d use for client projects. We decided that was a bad idea. But equally, not every website should be built with Wix, Squarespace or Shopify. There’s often a trade-off between bespoke and prebuilt. Every project needs an evaluation. Integration may not always be the answer, but it should always be considered.</p>                ]]></content:encoded>
            </item>
            <item>
                <title>Image Baselines &amp; Line Heights</title>
<link>https://joe.gl/ombek/blog/image-baselines-line-heights/</link>                <pubDate>Tue, 08 Jan 2013 12:00:00 +0000</pubDate>
                <dc:creator><![CDATA[Joe Glombek]]></dc:creator>
                <guid isPermaLink="true">https://joe.gl/ombek/blog/image-baselines-line-heights/</guid>
                <enclosure url="https://joe.gl/media/owifnhpi/caligraphy-grid.jpg" length="139822" type="image/jpg" />
                <description>Have you ever noticed what seems like a margin beneath an image that you can&#39;t get rid of? Baselines and line heights could be your problem.</description>
                <content:encoded><![CDATA[
                    <p>Have you ever noticed what seems like a margin beneath an image that you can't get rid of? Baselines and line heights could be your problem.</p>
<h2>The Problem</h2>

<p>Have you ever noticed what seems like a margin beneath an image that you can't get rid of?</p>

<pre><code>&lt;div style="background-color: red; margin: 0; padding: 0;"&gt;
    &lt;img style="margin: 0; padding: 0;" src="/media/t5jphaup/favicon.png" /&gt;
    Abc Xyz
&lt;/div&gt;
</code></pre>

<p>Renders as:</p>

<div style="background-color: red; margin: 0; padding: 0;"><img style="margin: 0; padding: 0; vertical-align: baseline;" src="/media/t5jphaup/favicon.png" /> Abc Xyz</div>

<p>See that red strip at the bottom? It shouldn't be there, right? I explicitly stated I didn't want a margin or any padding on <strong>both</strong> elements!</p>

<p>Well, after some research, the culprit is <code>line-height</code>.</p>

<p>Everything has a <code>line-height</code>. On most elements, however, its normally the same height or smaller than the element you want to display. In this case, the image is bigger than the line height (around 1.5em in most browsers, and the font on this webpage is probably 12px so is line-height can be approximated to be 18px (12 * 1.5 = 18)) and yet the line height is still having an effect. I can prove it is the line-height by doing this:</p>

<pre><code>&lt;div style="background-color: red; line-height: 0;"&gt;
    &lt;img src="/media/t5jphaup/favicon.png" /&gt;
    Abc Xyz
&lt;/div&gt;
</code></pre>

<p>Renders as:</p>

<div style="background-color: red; line-height: 0;"><img style="vertical-align: baseline;" src="/media/t5jphaup/favicon.png" /> Abc Xyz</div>

<p>See? No red line!</p>

<p>However, what if I wanted to render a string after the element? Making <code>line-height: 0px</code> is not ideal - not to mention how horribly hacky it is. This was the solution I saw posted on numerous forums. But I thought there must be another way... why is the image being rendered half way up the "line"?</p>

<p>That's when it hit me!</p>

<p>Images have <code>vertical-align: baseline</code> as their default - of course!</p>

<h2>Time for a Calligraphy Lesson!</h2>

<p>The <strong>base line</strong> is the line that most letters sit on (<u>a, b, c, d, e</u>). There are letters that sit below this line (<u>g, j, p, q, y</u>). These letters touch the <strong>descending line</strong>. Above the base line, there are various other lines but we don't care about them right now, but the top one is the <strong>ascending line</strong>.</p>

<p><img src="/media/owifnhpi/caligraphy-grid.jpg" alt="Calligraphy lines diagram" /></p>

<p>Image from <a title="Calligraphy for Beginners" href="https://web.archive.org/web/20180425063921/http://calligraphyforbeginners.com/" target="_blank" rel="noopener">Calligraphy for Beginners (Archive.org link)</a>. Seems like a good website if you want to know more!</p>

<p>The line-height in HTML defaults to the distance between the ascending and descending lines. The image, however, is sitting on the base line. So, this red line is the distance between the base line and the descending line!</p>

<h2>Fixing the issue</h2>

<p>Using the following CSS fixes my little issue and is more useful in most circumstances.</p>

<pre><code>img {
    vertical-align: bottom;
}
</code></pre>

<p>So, when used in the example:</p>

<pre><code>&lt;div style="background-color: red;"&gt;
    &lt;img style="vertical-align: bottom;" src="/media/t5jphaup/favicon.png" /&gt;
    Abc Xyz
&lt;/div&gt;
</code></pre>

<p>Renders as:</p>

<div style="background-color: red;"><img style="vertical-align: bottom;" src="/media/t5jphaup/favicon.png" /> Abc Xyz</div>                ]]></content:encoded>
            </item>
    </channel>
</rss>