<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Luke Lowrey]]></title><description><![CDATA[💻 Web development, code, product and leadership. CTO and co-founder at Endpoint IQ.]]></description><link>https://lukelowrey.com/</link><image><url>https://lukelowrey.com/favicon.png</url><title>Luke Lowrey</title><link>https://lukelowrey.com/</link></image><generator>Ghost 5.79</generator><lastBuildDate>Tue, 20 Feb 2024 09:19:44 GMT</lastBuildDate><atom:link href="https://lukelowrey.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[14 .NET packages I always recommend]]></title><description><![CDATA[Save time and make your applications better. Check out this list of open source .NET libraries I love to use and recommend.]]></description><link>https://lukelowrey.com/recommended-dotnet-libraries/</link><guid isPermaLink="false">60299147fe1a670045f2b0b7</guid><category><![CDATA[Code]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sat, 20 Mar 2021 02:26:20 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2021/03/nuget-cover-min.png" medium="image"/><content:encoded><![CDATA[<img src="https://lukelowrey.com/content/images/2021/03/nuget-cover-min.png" alt="14 .NET packages I always recommend"><p>This post lists open source libraries available on <a href="http://nuget.org/?ref=lukelowrey.com">Nuget.org</a> that I regularly use and recommend. These packages will save you time and make your applications better. They have good documentation and a great developer experience.</p><h3 id="mediatr">MediatR</h3><p><a href="https://github.com/jbogard/MediatR?ref=lukelowrey.com">MediatR </a>is a simple in-process implementation of the <a href="https://en.wikipedia.org/wiki/Mediator_pattern?ref=lukelowrey.com">mediator pattern</a>. It helps developer write clean, decoupled and extendable code. Mediator is configured using dependency injection with out of the box support for .NET Core, Autofac and others. The <a href="https://github.com/jbogard/MediatR/wiki?ref=lukelowrey.com">documentation on Github</a> is the best place to get started.</p><p>I like to use MediatR to write simple command query type requests using the <em>IRequest </em>interface. <em>IRequest </em>handle messages sent to a single handler. Examples of types of <em>IRequests </em>in a standard web application might be &quot;<em>GetUserQuery</em>&quot; or &quot;<em>UpdateCartCommand</em>&quot;. The example below uses an internal nested class to keep the query model and query handler together in one class.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">//a simple &quot;query&quot; type request
public class GetUserQuery : IRequest&lt;User&gt;
{
    public int UserID { get; set; }

    internal class GetUserQueryHandler : IRequestHandler&lt;UserGetQuery, UserDto&gt;
    {
        private readonly UserService _userService;

        public UserGetQueryHandler(UserService userService)
        {
            _userService = userService;
        }

        public async Task&lt;UserDto&gt; Handle(GetUserQuery request, CancellationToken cancellationToken)
        {
            //normlly something more extensive here...
            return await _userService.GetUser(request.UserID);
        }
    }
}

//calling code
var user = await mediator.Send(new GetUserQuery(1));</code></pre><figcaption>A simple &quot;query&quot; type mediatr request</figcaption></figure><p>Mediatr also supports notification messages which can have multiple handlers through INotification. Notifications work in a similar way to requests but don&apos;t return values. They are great for exposing a &quot;extension point&quot; in your application that you may need to build on later.</p><blockquote>As a side note: everything <a href="https://jimmybogard.com/?ref=lukelowrey.com">Jimmy Bogard</a> writes code or otherwise is great.</blockquote><h3 id="serilog">Serilog</h3><p><a href="https://serilog.net/?ref=lukelowrey.com">Serilog </a>provides <em>structured</em> logging which can target output to just about anywhere. Structured logs capture your log statements and objects as json. They give much more context into what is happening in your application and with the right tools they can be queried and analysed in more detail than standard text logs.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">var position = new { Latitude = 25, Longitude = 134 };
var elapsedMs = 34;

log.Information(&quot;Processed {@Position} in {Elapsed:000} ms.&quot;, position, elapsedMs);

//log output
//{&quot;Position&quot;: {&quot;Latitude&quot;: 25, &quot;Longitude&quot;: 134}, &quot;Elapsed&quot;: 34}</code></pre><figcaption>Example from Serilog website</figcaption></figure><p>One of the best things about Serilog is the community has written sinks - which write logs to an provider for just about every service you can think of. The <a href="https://github.com/serilog/serilog/wiki/Provided-Sinks?ref=lukelowrey.com">provided sinks</a> Github page has a full list. A couple I recommend checking out:</p><ul><li><a href="https://datalust.co/seq?ref=lukelowrey.com">Seq</a> - self hosted structured log viewer</li><li><a href="https://sentry.io/?ref=lukelowrey.com">Sentry </a>- exception reporting service that plugs in automatically to <em>error</em> log events</li></ul><h3 id="hangfire">Hangfire</h3><p><a href="https://www.hangfire.io/?ref=lukelowrey.com">Hangfire </a>is an easy way to perform scheduled or fire-and forget background jobs for .NET Core and Framework applications. I love how easy it is to get up an running as the jobs can run in your app&apos;s main process without requiring a dedicated service.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">//Fire and forget
BackgroundJob.Enqueue(() =&gt; {
	Console.WriteLine(&quot;This will run once, in the background&quot;);
});

//Recurring job
RecurringJob.AddOrUpdate(
    () =&gt; {
    	Console.WriteLine(&quot;This will run daily&quot;);
    },
    Cron.Daily
);

//Calling a dependency injection job
BackgroundJob.Enqueue&lt;MyService&gt;(myService =&gt; myService.Execute());</code></pre><figcaption>Example Hangfire jobs</figcaption></figure><p>Hangfire has a great built in dashboard, dependency injection support and storage options for SQL server, PostgreSQL, Redis and more.</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2021/03/image.png" class="kg-image" alt="14 .NET packages I always recommend" loading="lazy" width="2782" height="1650"><figcaption>Hangfire dashboard</figcaption></figure><h3 id="fluentemail">FluentEmail</h3><p><a href="https://github.com/lukencode/FluentEmail?ref=lukelowrey.com">FluentEmail </a>(written by me) helps you implement complete email sending functionality in your app in less than 10 minutes. It features built in providers for the most popular email senders including SendGrid and Mailgun along with Razor templates out of the box. I recently wrote a <a href="https://lukelowrey.com/dotnet-email-guide-2021/">full guide to .NET email using FluentEmail</a> that will get you started.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">var email = await Email
    .From(&quot;bill.gates@microsoft.com&quot;)
    .To(&quot;luke.lowrey@example.com&quot;, &quot;Luke&quot;)
    .Subject(&quot;Hi Luke!&quot;)
    .Body(&quot;Fluent email looks great!&quot;)
    .SendAsync();</code></pre><figcaption>Send an email with FluentEmail</figcaption></figure><h3 id="lazycache">LazyCache</h3><p><a href="https://github.com/alastairtree/LazyCache?ref=lukelowrey.com">LazyCache</a> is an easy to use, thread safe and developer friendly wrapper for in-memory caching in .NET Core. I&apos;ve <a href="https://lukelowrey.com/caching-in-aspnet-core-with-lazycache/">written a bit about LazyCache</a> in the past and I still reach for it every time I need a cache provider.</p><p>LazyCache uses a &quot;GetOrAdd&quot; pattern for caching where you request an item from the cache and at the same time provide the function to add that item to the cache if it is missing.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">var model = await cache.GetOrAddAsync(&quot;HomePageData&quot;, 
   async () =&gt;
   {
       return homePageService.GetData(); //function to get cachable data
   }
   , new TimeSpan(12, 0, 0) //12 hour cache expiry
); </code></pre><figcaption>Example cache GetOrAdd call</figcaption></figure><p>LazyCache is easy to extend so if you need to move beyond a simple in memory cache to something distributed the step up should be pretty seamless.</p><h3 id="dapper">Dapper</h3><p>Sometimes SQL data access with Entity Framework is overkill or just <em>too slow</em>. When you want to run raw SQL but still get some nice object mapping <a href="https://github.com/StackExchange/Dapper?ref=lukelowrey.com">Dapper </a>is the way to go.</p><blockquote>Dapper is a <a href="https://www.nuget.org/packages/Dapper?ref=lukelowrey.com" rel="nofollow">NuGet library</a> that you can add in to your project that will extend your <code>IDbConnection</code> interface.</blockquote><p>Dapper takes your existing database connection and adds extension methods to run raw SQL and map it back to C# classes.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">public class Dog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

var dogs = connection.Query&lt;Dog&gt;(
    &quot;select * from Dogs where Age &gt; @Id&quot;, //sql query, @ parameters
    new { Age = 5 } //anonymous object to map parameters
);
</code></pre><figcaption>Example dapper query</figcaption></figure><h3 id="miniprofiler">MiniProfiler</h3><p><a href="https://miniprofiler.com/?ref=lukelowrey.com">MiniProfiler</a> is a simple performance profiler for .NET Core and framework (also Ruby, Go and Node) web applications. It attaches automatically to the key parts of your application - mainly database access. MiniProfiler will then render an awesome little timing result directly on your app. It is great for running during development to pick up rogue LINQ queries.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2021/02/image-6.png" class="kg-image" alt="14 .NET packages I always recommend" loading="lazy" width="1432" height="866"><figcaption>MiniProfiler results</figcaption></figure><p>MiniProfiler has <a href="https://miniprofiler.com/dotnet/AspDotNet?ref=lukelowrey.com">great setup documentation</a> with a heap of options. For security reasons, I normally only have it enabled during development.</p><h3 id="linqkit">LinqKit</h3><p><a href="https://github.com/scottksmith95/LINQKit?ref=lukelowrey.com">LINQKit</a> is a set of extensions for querying Entity Framework with Linq. LINQKit allows you to build linq queries and conditions as predicates.</p><p>My most common use for LINQKit is simplifying queries with lots of criteria (think grids with filters) using the PredicateBuilder.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">//base linq query
var users = from u in context.Users
            select u;

//add conditions as required
var predicate = PredicateBuilder.New&lt;User&gt;(true);

if (!string.IsNullOrWhiteSpace(filters.Name))
    predicate.And(u =&gt; u.Name.Contains(filters.Name));

if (!string.IsNullOrWhiteSpace(filters.Email))
    predicate.And(u =&gt; u.Email.Contains(filters.Email));

//apply conditions to query
var filteredUsers = users.AsExpandable().Where(predicate);</code></pre><figcaption>Example LINQKit predicate usage to add optional query conditions</figcaption></figure><h3 id="fluentmigrator-or-dbup">FluentMigrator or DbUp</h3><p>If you are looking for an alternative to running EntityFramework code first migrations both FluentMigrator and DbUp will do the job.</p><p><a href="https://dbup.github.io/?ref=lukelowrey.com">DbUp</a> works best if you like raw SQL and simple scripts. You simply create a console application and add SQL files as embedded resources. Some simple code in Program.cs will run your migration scripts in order - easy to integrate into a devops pipeline.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2021/02/image-7.png" class="kg-image" alt="14 .NET packages I always recommend" loading="lazy" width="670" height="295"><figcaption>Embedded scripts</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-cs">static int Main(string[] args)
{
    var connectionString = args.FirstOrDefault(); //or some configuration

    var upgrader =
        DeployChanges.To
            .SqlDatabase(connectionString)
            .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
            .LogToConsole()
            .Build();

    var result = upgrader.PerformUpgrade();
    //omitted error handling...
    return 0;
}</code></pre><figcaption>Program.cs</figcaption></figure><p><a href="https://fluentmigrator.github.io/?ref=lukelowrey.com">FluentMigrator</a> takes a similar concept with a more code-based approach. Instead of adding raw scripts you create small classes for each migration. This approach gives you more flexibility in building migrations (and saves you remembering the create foreign key sql syntax) and allows for more complete up/down migration scenarios.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">using FluentMigrator;

namespace Migrations
{
    [Migration(20180430121800)]
    public class AddLogTable : Migration
    {
        public override void Up()
        {
            Create.Table(&quot;Log&quot;)
                .WithColumn(&quot;Id&quot;).AsInt64().PrimaryKey().Identity()
                .WithColumn(&quot;Text&quot;).AsString();
        }

        public override void Down()
        {
            Delete.Table(&quot;Log&quot;);
        }
    }
}
</code></pre><figcaption>Example migration creating a Log table</figcaption></figure><h3 id="csvhelper">CsvHelper</h3><p><a href="https://joshclose.github.io/CsvHelper/?ref=lukelowrey.com">CsvHelper </a>is my go to package for reading and writing csv files. &#xA0;The library is flexible enough that you can creating custom mapping classes and rules for your C# classes or use the low level row and column methods directly.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">//import csv to a class
using (var reader = new StreamReader(&quot;importfile.csv&quot;))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
   csv.Context.RegisterClassMap&lt;YourClassMap&gt;(); //optionally use a mapping class
   var records = csv.GetRecords&lt;YourClass&gt;();
}

//use a mapping class
public class YourClass
{
    public int Id { get; set; }
    public string Name { get set; }
}

public sealed class YourClassMap : ClassMap&lt;YourClass&gt;
{
    public FooMap()
    {
        Map(m =&gt; m.Id).Name(&quot;Identifier&quot;); //overrider column names and more
        Map(m =&gt; m.Name).Name(&quot;TheName&quot;);
    }
}

//or access columns directly
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    csv.ReadHeader();
    while (csv.Read())
    {
        var id csv.GetField&lt;int&gt;(&quot;Id&quot;),
        var name = csv.GetField(&quot;Name&quot;);
    }
}</code></pre><figcaption>Import csv file examples</figcaption></figure><p>There are similar options for exporting csv files either with direct column writes or using classes and mapping.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">using (var writer = new StreamWriter(&quot;path\\to\\file.csv&quot;))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
   csv.WriteRecords(records);
}</code></pre><figcaption>Simple export csv example</figcaption></figure><h3 id="hashids">Hashids</h3><p><a href="https://hashids.org/net/?ref=lukelowrey.com">Hashids for .NET </a>generates short, unique string identifiers from integers - similar to YouTube video ids. It is great for turning one or more integers into a single short, non sequential hashes to use in urls and sharing. Just don&apos;t use it in place of security.</p><pre><code class="language-cs">var hashids = new Hashids(&quot;example salt&quot;);

//creates &quot;R9tGSE&quot; hash
var id = hashids.Encode(1, 2, 3);

//decodes &quot;R9tGSE&quot; back into int[] {1, 2, 3}
var numbers = hashids.Decode(id);</code></pre><h3 id="humanizer">Humanizer</h3><p><a href="https://github.com/Humanizr/Humanizer?ref=lukelowrey.com">Humanizer</a> is for turning dates, numbers, enums and more in human friendly readable strings. It supports lots of data types and can be used with multiple languages.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">//These are some methods I use all the time, check out the documentation for the full list

//dates
DateTime.UtcNow.AddHours(-25).Humanize() =&gt; &quot;yesterday&quot;
DateTime.UtcNow.AddHours(4).Humanize() =&gt; &quot;4 hours from now&quot;

//timespan
TimeSpan.FromDays(16).Humanize() =&gt; &quot;2 weeks&quot;

//Pluralize and singularize
&quot;Dog&quot;.Pluralize() =&gt; &quot;Dogs&quot;
&quot;Dogs&quot;.Singularize() =&gt; &quot;Dog&quot;

//truncate
&quot;Long text to truncate&quot;.Truncate(10) =&gt; &quot;Long text&#x2026;&quot;</code></pre><figcaption>Common Humanizer methods</figcaption></figure><h3 id="bogus">Bogus</h3><p><a href="https://github.com/bchavez/Bogus?ref=lukelowrey.com">Bogus</a> is a &quot;fake&quot; data generator for .NET. I shudder at the thought of generating &quot;demo&quot; data and &quot;test&quot; data but Bogus takes a bit part of that pain away.</p><p>Bogus allow you to map you C# classes to fake data to quickly generate lots of data. It is also possible to access the underlying methods directly and generate a phone number, email address etc on demand.</p><figure class="kg-card kg-code-card"><pre><code class="language-cs">//map a class
var userIds = 0;
var testUsers = new Faker&lt;User&gt;()
	.RuleFor(b =&gt; b.Id, (f, u) =&gt; userIds++)
	.RuleFor(u =&gt; u.FirstName, (f, u) =&gt; f.Name.FirstName(u.Gender))
	.RuleFor(u =&gt; u.LastName, (f, u) =&gt; f.Name.LastName(u.Gender))
	.RuleFor(u =&gt; u.Avatar, f =&gt; f.Internet.Avatar())
	.RuleFor(u =&gt; u.UserName, (f, u) =&gt; f.Internet.UserName(u.FirstName, u.LastName))
	.RuleFor(u =&gt; u.Email, (f, u) =&gt; f.Internet.Email(u.FirstName, u.LastName));

//generate users
var user = testUsers.Generate();

//use methods directly
var faker = new Faker(&quot;en&quot;);
faker.Internet.DomainName(); //eg &quot;sylvester.com&quot;
faker.Company.Bs().Dump(); // eg &quot;enable leading-edge architectures&quot;</code></pre><figcaption>Simple Bogus data generation examples</figcaption></figure><p></p><h3 id="honourable-mentions">Honourable mentions</h3><p>I also love these libraries but this post was taking me forever to finish.</p><ul><li><a href="https://fluentvalidation.net/?ref=lukelowrey.com">FluentValidation</a></li><li><a href="https://docs.automapper.org/en/stable/?ref=lukelowrey.com">AutoMapper</a></li><li><a href="https://autofac.org/?ref=lukelowrey.com">Autofac </a>- if you are stuck on .NET framework this if your best dependency injection option</li></ul><hr><p><em>If you enjoyed the post, please share the thread on Twitter and let me know if I missed out your favourite libraries.</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">I put together this list of 14 open source <a href="https://twitter.com/dotnet?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">@dotnet</a> libraries I love to use and recommend<br><br>Libraries listed in this thread &#x1F447;<br><br>OR see the full list on my site<a href="https://t.co/Ouuyz77AB9?ref=lukelowrey.com">https://t.co/Ouuyz77AB9</a><a href="https://twitter.com/hashtag/csharp?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#csharp</a> <a href="https://twitter.com/hashtag/dotnet?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#dotnet</a></p>&#x2014; Luke Lowrey &#x1F6F4; (@lukencode) <a href="https://twitter.com/lukencode/status/1374102332225490947?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">March 22, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><p><em>Want more? for a massive community list of .NET resources check out <a href="https://github.com/quozd/awesome-dotnet?ref=lukelowrey.com">quozd/awesome-dotnet</a>.</em></p>]]></content:encoded></item><item><title><![CDATA[GhostSolo update (0.5.0) - February 2021]]></title><description><![CDATA[Membership updates, table of contents support and bug fixes for my Ghost theme "GhostSolo"]]></description><link>https://lukelowrey.com/ghostsolo-update-february-2021/</link><guid isPermaLink="false">601f481d73445b0039569809</guid><category><![CDATA[GhostSolo]]></category><category><![CDATA[Ghost]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sun, 07 Feb 2021 02:07:26 GMT</pubDate><content:encoded><![CDATA[<p>GhostSolo is free and open-source <a href="https://ghost.org/?ref=lukelowrey.com">Ghost blog</a> theme. It is currently running on this blog. Find the <a href="https://lukelowrey.com/ghostsolo/">latest information here</a> or download the theme from Github.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/lukencode/GhostSolo?ref=lukelowrey.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">lukencode/GhostSolo</div><div class="kg-bookmark-description">A Ghost theme for solo bloggers. Contribute to lukencode/GhostSolo development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">lukencode</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://repository-images.githubusercontent.com/275575297/18e1c400-cfe9-11ea-92f1-65fd2cf2adfd"></div></a></figure><h3 id="membership-changes">Membership changes</h3><p>Ghost has introduced a <a href="https://ghost.org/changelog/portal/?ref=lukelowrey.com">new way for blogs to implement membership</a> which does not depend on the theme. I feel this is a much cleaner method than previously where themes had to implement individual membership pages and blogs had to write custom routes.yaml.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2021/02/image-1.png" class="kg-image" alt loading="lazy" width="491" height="453"><figcaption>Portal signup form</figcaption></figure><p>Moving forward GhostSolo will only support using Portal for signups. This means the signup button from the header has been removed. The signup forms on the home page and footer however will stay.</p><h3 id="table-of-contents-template">Table of contents template</h3><p>I have added a new post template to enable table of contents for longer posts. It will automatically look at the headings in your post and generate a table of contents on the right of the page.</p><p>To use the template, edit the settings of your post and look for templates.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2021/02/image-2.png" class="kg-image" alt loading="lazy" width="443" height="249"><figcaption>Post settings</figcaption></figure><p>See <a href="https://lukelowrey.ghost.io/ghost/?ref=lukelowrey.com#/editor/post/5f2609c355750900451a02b5/">this post</a> for an example.</p><figure class="kg-card kg-image-card"><img src="https://lukelowrey.com/content/images/2021/02/image.png" class="kg-image" alt loading="lazy" width="1251" height="505" srcset="https://lukelowrey.com/content/images/size/w600/2021/02/image.png 600w, https://lukelowrey.com/content/images/size/w1000/2021/02/image.png 1000w, https://lukelowrey.com/content/images/2021/02/image.png 1251w" sizes="(min-width: 720px) 720px"></figure><h3 id="other-changes">Other changes</h3><ul><li>Improved display of Ghost Editor elements - bookmarks in particular see the link at the top of the page for an example</li><li>Updated <a href="https://getbootstrap.com/?ref=lukelowrey.com">Bootstrap </a><strong>v5.0.0-beta1</strong></li><li>Fixed issue where the post date did not display correctly</li><li>Fixed some display issues on smaller screens</li></ul><h3 id="future-plans">Future plans</h3><ul><li>Add search functionality</li><li>Better documentation of theme usage</li><li>Better support for social networks</li></ul><hr><p>Let me know on <a href="https://twitter.com/lukencode?ref=lukelowrey.com">twitter </a>if you are using the theme or if you have any feedback.</p>]]></content:encoded></item><item><title><![CDATA[A complete guide to send email in .NET (2021)]]></title><description><![CDATA[Use FluentEmail to send emails in .NET. Send with Smtp, Mailgun or SendGrid and use customisable Razor or Liquid templates for your content.]]></description><link>https://lukelowrey.com/dotnet-email-guide-2021/</link><guid isPermaLink="false">5f2609c355750900451a02b5</guid><category><![CDATA[Code]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[FluentEmail]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Tue, 26 Jan 2021 02:00:00 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2021/01/email3.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://lukelowrey.com/content/images/2021/01/email3.jpg" alt="A complete guide to send email in .NET (2021)"><p><a href="https://github.com/lukencode/FluentEmail?ref=lukelowrey.com">Flu<a href="https://github.com/lukencode/FluentEmail?ref=lukelowrey.com">entEmail</a></a> is an open-source .NET library (created by me but with <a href="https://github.com/lukencode/FluentEmail/graphs/contributors?ref=lukelowrey.com">lots of help</a>) that helps you implement complete email sending functionality in your dotnet app in less than 10 minutes (probably). It features built in providers for the most popular email senders including SendGrid and Mailgun along with Razor or Liquid templates out of the box.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://github.com/lukencode/FluentEmail?ref=lukelowrey.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">lukencode/FluentEmail</div><div class="kg-bookmark-description">.NET Core email sending. Contribute to lukencode/FluentEmail development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt="A complete guide to send email in .NET (2021)"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">lukencode</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://repository-images.githubusercontent.com/603405/e1bd5800-d4ab-11ea-98c4-54f2176660b5" alt="A complete guide to send email in .NET (2021)"></div></a><figcaption>FluentEmail on Github</figcaption></figure><h2 id="why-use-fluentemail">Why use FluentEmail?</h2><p>FluentEmail pieces together the components your application needs for sending email. </p><ul><li>The API is so simple that autocomplete (almost) writes your code for you! </li><li>Well tested providers for the popular email senders and template engines are available out of the box (keep reading to see examples)</li><li>Solid dependency injection keeps your code clean and allows you to easily write tests or switch out email options for different tenants/environments</li></ul><h2 id="basic-usage">Basic Usage</h2><p>FluentEmail and its providers can be installed via Nuget. The FluentEmail.Core library is all you need to get started.</p><!--kg-card-begin: markdown--><p><code>dotnet add package FluentEmail.Core</code></p>
<!--kg-card-end: markdown--><p>To construct and send an email you just keep building up the fluent methods on the IEmail class. When you are ready to send call SendAsync().</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">var email = await Email
    .From(&quot;bill.gates@microsoft.com&quot;)
    .To(&quot;luke.lowrey@example.com&quot;, &quot;Luke&quot;)
    .Subject(&quot;Hi Luke!&quot;)
    .Body(&quot;Fluent email looks great!&quot;)
    .SendAsync();
</code></pre>
<!--kg-card-end: markdown--><p>There are the most common methods available on the email object (for the full list see <a href="https://github.com/lukencode/FluentEmail/blob/master/src/FluentEmail.Core/IFluentEmail.cs?ref=lukelowrey.com">IFluentEmail.cs</a>)</p><ul><li>.<strong>To</strong>(string emailAddress) - <em>add recipients</em></li><li>.<strong>SetFrom</strong>(string emailAddress) - <em>change the sender address</em></li><li>.<strong>CC/BCC</strong>(string emailAddress) - <em>add CC or BCC</em></li><li>.<strong>Subject</strong>(string subject) -<em> set the subject</em></li><li>.<strong>Body</strong>(string body) - <em>set the message body (without templating)</em></li><li>.<strong>Attach</strong>(Attachment attachment) - <em>add attachments</em></li><li><strong>UsingTemplate</strong>(string template, T model, bool isHtml = true) - <em>set a template, keep reading for more templating information</em></li><li><strong>SendAsync</strong>() - <em>send the email using the configured sender</em></li></ul><h2 id="dependency-injection">Dependency Injection</h2><p>The best way to use FluentEmail is to configure your sender and template choices with dependency injection. There is good out of the box support for the built in .NET DependencyInjection classes.</p><!--kg-card-begin: markdown--><pre><code class="language-cs">public void ConfigureServices(IServiceCollection services)
{
    services
        .AddFluentEmail(&quot;fromemail@test.test&quot;)
        .AddRazorRenderer()
        .AddSmtpSender(&quot;localhost&quot;, 25);
}
</code></pre>
<!--kg-card-end: markdown--><p>This example sets up FluentEmail with a default SendFrom address and uses the injection helpers to add the SmtpSender and RazorRenderer. </p><ul><li><strong>ISender </strong>- this interface is the provider that will be used to send the email. In this example the AddSmtpSender() method configures an SmtpSender provider.</li><li><strong>ITemplateRenderer </strong>- this is an optional interface to provide email templating support. In the example the AddRazorRenderer() method is setting up the RazorRenderer provider.</li></ul><p>Using dependency injection will make a couple of interfaces available to your code. See the example below for sending email with the configured services.</p><!--kg-card-begin: markdown--><pre><code class="language-csharp">public class EmailExampleController : Controller
{
    public async Task&lt;IActionResult&gt; SendSingleEmail([FromServices] IFluentEmail singleEmail)
    {    
        var email = singleEmail
            .To(&quot;test@test.test&quot;)
            .Subject(&quot;Test email&quot;)
            .Body(&quot;This is a single email&quot;);

        await email.SendAsync();
        
        return Ok();
    }
    
    public async Task&lt;IActionResult&gt; SendMultipleEmail([FromServices] IFluentEmailFactory emailFactory)
    {    
        var email1 = emailFactory
            .Create()
            .To(&quot;test@test.test&quot;)
            .Subject(&quot;Test email 1&quot;)
            .Body(&quot;This is the first email&quot;);

        await email1.SendAsync();
        
        var email2 = emailFactory
            .Create()
            .To(&quot;test@test.test&quot;)
            .Subject(&quot;Test email 2&quot;)
            .Body(&quot;This is the second email&quot;);

        await email2.SendAsync();
        
        return Ok();
    }
}
</code></pre>
<!--kg-card-end: markdown--><ul><li><strong>IEmail </strong>- this is a single instance of an email to be sent as demonstrated in the basic example above. Use this to send one email (but no more!)</li><li><strong>IEmailFactory </strong>- this interface has a method .Create() which will give you an IEmail instance. Use this interface to send multiple emails in the same method or request - see the following example.</li></ul><h2 id="template-rendering">Template Rendering</h2><p>FluentEmail has core support for multiple template renderer providers. To use template rendering, include one of the providers and configure it as part of your dependency injection. </p><!--kg-card-begin: markdown--><pre><code class="language-c#">// Using Razor templating package (or set using AddRazorRenderer in services)
Email.DefaultRenderer = new RazorRenderer();

var template = &quot;Dear @Model.Name, You are totally @Model.Compliment.&quot;;
var model = new 
{
    Name = &quot;Luke Lowrey&quot;,
    Position = &quot;Developer&quot;,
    Message = &quot;Hi Luke, this is an email message&quot;
};

var email = Email
    .From(&quot;bob@hotmail.com&quot;)
    .To(&quot;somedude@gmail.com&quot;)
    .Subject(&quot;woo nuget&quot;)
    .UsingTemplate(template, model);

</code></pre>
<!--kg-card-end: markdown--><p>The UsingTemplate method accepts a string for the template and an object for the model to use. The template itself will depend on the provider you are using, this example uses the standard RazorRenderer</p><p>FluentEmail also providers helper methods to source template files from disk or as embedded resources as well as serving different templates based on culture.</p><ul><li><strong>UsingTemplateFromEmbedded</strong>(string path, T model, Assembly assembly) - pass the assembly with the embedded resource and the path eg &quot;YourApp.EmailTemplates.Welcome.txt&quot;&quot;.</li><li><strong>UsingTemplateFromFile</strong>(string filename, T model) - pass the file path for the template.</li></ul><h3 id="razor-email-templates">Razor Email Templates</h3><p>The examples above use the FluentEmail.Razor template renderer. This is the most popular option for templating. The Razor render uses the <a href="https://github.com/toddams/RazorLight?ref=lukelowrey.com">RazorLight </a>library to render Razor strings.</p><!--kg-card-begin: markdown--><p><code>dotnet add package FluentEmail.Razor</code></p>
<!--kg-card-end: markdown--><p>The RazorRenderer supports any valid Razor code. This example (taken from the test project) shows how you can use layout pages in templates.</p><!--kg-card-begin: markdown--><pre><code class="language-cs">//configure the Razor Renderer
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddFluentEmail(&quot;fromemail@test.test&quot;)
        
        //pass in the root directory for files
        .AddRazorRenderer(Directory.GetCurrentDirectory()) 
        
        //OR pass in a type in the assemble with embedded templates
        .AddRazorRenderer(typeof(Startup));
}


//In your template code include a layout file
//the template could be sourced from file/embedded if that is configured
var template = @&quot;
    @{ Layout = &quot;&quot;./Shared/_Layout.cshtml&quot;&quot;; }
    
    Hi @Model.Name here is a list @foreach(var i in Model.Numbers) { @i }
&quot;;

var model = new { Name = &quot;LUKE&quot;, Numbers = new[] { &quot;1&quot;, &quot;2&quot;, &quot;3&quot; } };

var email = new Email()
    .To(&quot;test@test.test&quot;)
    .Subject(&quot;Razor template example&quot;)    
    .UsingTemplate(template, model});

</code></pre>
<!--kg-card-end: markdown--><h3 id="liquid-templates">Liquid Templates</h3><p><a href="https://shopify.github.io/liquid/?ref=lukelowrey.com" rel="nofollow">Liquid templates</a> are a more restricted templating language created by Shopify. They are more lightweight than Razor templates as well as safer and simpler for end users to create. Properties on the model are made available as Liquid properties in the template. FluentEmail uses <a href="https://github.com/sebastienros/fluid?ref=lukelowrey.com">Fluid</a> (see <a href="https://github.com/sebastienros/fluid?ref=lukelowrey.com">samples</a>) to render the templates in C#. </p><!--kg-card-begin: markdown--><pre><code class="language-cs">//configure the Razor Renderer
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddFluentEmail(&quot;fromemail@test.test&quot;)     
        .AddLiquidRenderer(options =&gt;
        {
            // file provider is used to resolve layout files if they are in use
            options.FileProvider = new PhysicalFileProvider(Path.Combine(someRootPath, &quot;EmailTemplates&quot;)); ;
        });
}


// Liquid template which utilizes layout
var template = @&quot;
    {% layout &apos;_layout.liquid&apos; %}    
    Dear {{ Name }}, You are totally {{ Compliment }}.
&quot;;

var model = new ViewModel { Name = &quot;Luke&quot;, Compliment = &quot;Awesome&quot; };

var email = Email
    .From()
    .To(&quot;somedude@gmail.com&quot;)
    .Subject(&quot;Liquid templates&quot;)
    .UsingTemplate(template, model);
</code></pre>
<!--kg-card-end: markdown--><h2 id="email-senders">Email Senders</h2><p>FluentEmail allows you to plug in popular email sending providers (or build your own by implementing ISender). To use a sender include the provider and configure it as part of dependency injection. </p><p>The following senders are available as core libraries. The same core FluentEmail methods are used to send emails, the only difference is what sender you configure.</p><h3 id="smtp-sender">SMTP Sender</h3><p>The SMTP sender use <em>System.Net.Mail.SmtpClient</em> to send email. It is set up using dependency injection. You can provide basic host and port details or a full SmtpClient instance.</p><!--kg-card-begin: markdown--><p><code>dotnet add package FluentEmail.Smtp</code></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><pre><code class="language-cs">//configure smtp sender
public void ConfigureServices(IServiceCollection services)
{
    services
       .AddFluentEmail(&quot;fromemail@test.test&quot;)
       .AddSmtpSender(&quot;yoursmtphost.com&quot;, 25) //configure host and port
       .AddSmtpSender(new SmtpClient() { }); //OR provide your own SmtpClient instance
}


//Send email as per examples above
await new Email
    .To(&quot;test@test.test&quot;)
    .Subject(&quot;Email example&quot;)    
    .Body(&quot;Email body&quot;)
    .SendAsync(); //this will use the SmtpSender

</code></pre>
<!--kg-card-end: markdown--><h3 id="mailgun-sender">Mailgun Sender</h3><p><a href="https://www.mailgun.com/?ref=lukelowrey.com">Mailgun</a> is an API based email sender. The FluentEmail Mailgun sender supports most functionality including attachments, tags and headers.</p><!--kg-card-begin: markdown--><p><code>dotnet add package FluentEmail.Mailgun</code></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><pre><code class="language-cs">//configure Mailgun sender
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddFluentEmail(&quot;fromemail@test.test&quot;)
        .AddMailGunSender(&quot;domainname.com&quot;, &quot;yourapikey&quot;, FluentEmail.Mailgun.MailGunRegion.USA);
}


//Send email as per examples above
await new Email()
    .To(&quot;test@test.test&quot;)
    .Subject(&quot;Mailgun example&quot;)
    .Body(&quot;Email body&quot;)
    .Tag(&quot;tagname&quot;) //the Mailgun sender supports tags
    .SendAsync() //this will use the MailgunSender

</code></pre>
<!--kg-card-end: markdown--><h3 id="sendgrid-sender">SendGrid Sender</h3><p><a href="https://sendgrid.com/?ref=lukelowrey.com">SendGrid </a>is another popular API based email sender. The FluentEmail SendGrid provider can be used in &quot;live&quot; or &quot;sandbox&quot; mode.</p><!--kg-card-begin: markdown--><p><code>dotnet add package FluentEmail.SendGrid</code></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><pre><code class="language-cs">//configure SendGrid sender
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddFluentEmail(&quot;fromemail@test.test&quot;)
        .AddSendGridSender(&quot;apikey&quot;);
}

//Send emails as per examples above</code></pre>
<!--kg-card-end: markdown--><h3 id="mimekit-sender">MimeKit Sender</h3><p><a href="https://github.com/jstedfast/MimeKit?ref=lukelowrey.com">MimeKit </a>is an open source .NET email library with support for IMAP, POP3, and SMTP. If you need to do anything advanced with email protocols you need to use MimeKit. </p><p>The MimeKit sender for FluentEmail lets you to set up whatever flavour of email sending you need and include it in the FluentEmail framework.</p><!--kg-card-begin: markdown--><p><code>dotnet add package FluentEmail.MailKit</code></p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><pre><code class="language-cs">//configure MailKit sender
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddFluentEmail(&quot;fromemail@test.test&quot;)
        .AddMailKitSender(new SmtpClientOptions
        {
            Server = &quot;server&quot;,
            Port = 25,
            Password = &quot;password&quot;,
            UseSsl = true,
            User = &quot;user&quot;
        });
}

//Send emails as per examples above</code></pre>
<!--kg-card-end: markdown--><h3 id="other-senders">Other Senders</h3><p>Other people in the open source community have built and maintain senders for different email providers.</p><ul><li>Microsoft Graph API Sender - Microsoft Graph API Sender - <a href="https://www.nuget.org/packages/FluentEmail.Graph/?ref=lukelowrey.com">https://www.nuget.org/packages/FluentEmail.Graph/</a></li><li>Postmark Sender - <a href="https://www.nuget.org/packages/FluentEmail.Postmark/?ref=lukelowrey.com">https://www.nuget.org/packages/FluentEmail.Postmark/</a></li><li>MailJet Sender - <a href="https://www.nuget.org/packages/FluentEmail.Mailjet/?ref=lukelowrey.com">https://www.nuget.org/packages/FluentEmail.Mailjet/</a></li><li>MailTrap Sender - <a href="https://www.nuget.org/packages/FluentEmail.Mailtrap/?ref=lukelowrey.com">https://www.nuget.org/packages/FluentEmail.Mailtrap/</a></li></ul><h2 id="extending-fluentemail">Extending FluentEmail</h2><p>FluentEmail was built to make it easy to plug in your own providers for templating and sending. If you are working on something you think belongs in the core repository head on over to the <a href="https://github.com/lukencode/FluentEmail?ref=lukelowrey.com">Github repo </a>and let me know.</p><hr><p><em>If you found this post useful and want to help me (and others out) please share the thread on twitter.</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet" data-width="550"><p lang="en" dir="ltr">Just posted a .NET guide for sending email from your app in under 10 minutes using FluentEmail <a href="https://twitter.com/hashtag/dotnet?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#dotnet</a><a href="https://t.co/ywm371tprV?ref=lukelowrey.com">https://t.co/ywm371tprV</a></p>&#x2014; Luke Lowrey &#x1F6F4; (@lukencode) <a href="https://twitter.com/lukencode/status/1354018040673755137?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">January 26, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><blockquote>&#x1F4A1; Want more? Check out my list of <a href="https://lukelowrey.com/recommended-dotnet-libraries/">14 .NET libraries I always recommend</a>.</blockquote>]]></content:encoded></item><item><title><![CDATA[GitHub Action - Build and Run .NET Tests on new pull requests]]></title><description><![CDATA[Validate your dotnet code by building and running tests on every pull request.]]></description><link>https://lukelowrey.com/github-action-dotnet-pr-validation/</link><guid isPermaLink="false">5f4cd56e67a8400039c3ff1e</guid><category><![CDATA[DevOps]]></category><category><![CDATA[GitHub Action]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Mon, 31 Aug 2020 11:21:26 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2020/08/Annotation-2020-08-31-205922.png" medium="image"/><content:encoded><![CDATA[<img src="https://lukelowrey.com/content/images/2020/08/Annotation-2020-08-31-205922.png" alt="GitHub Action - Build and Run .NET Tests on new pull requests"><p>Pull request validation saves time and encourages better coding practices. You can easily set up a <a href="https://github.com/features/actions?ref=lukelowrey.com">GitHub action</a> to validate new code by building and running tests when a new PR is created. </p><h3 id="create-a-github-action-to-run-on-new-pull-requests">Create a GitHub Action to run on new pull requests</h3><p>Add an action to your repository using the &quot;<em>Actions&quot; tab -&gt; New workflow</em> on github.com or by creating a build-and-test.yml file in the <em>.github/workflows/</em> directory. </p><p><em>Example <a href="https://github.com/lukencode/FluentEmail/blob/master/.github/workflows/dotnet-core.yml?ref=lukelowrey.com">dotnet-core.yml</a> from FluentEmail</em></p><!--kg-card-begin: markdown--><pre><code class="language-yaml">name: Build &amp; Test

on:
  pull_request:
    branches: [ master ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.101
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --configuration Release --no-restore
    - name: Test
      run: dotnet test --no-restore --verbosity normal

</code></pre>
<!--kg-card-end: markdown--><ul><li>The action uses the <em>on: pull_request</em> trigger which accepts the branches you want to run the workflow</li><li><em>actions/checkout@v2</em> and <em>actions/setup-dotnet@v1 </em>are community actions that will get your code and setup dotnet</li><li>The <em>run</em> steps make the calls the the <a href="https://docs.microsoft.com/en-us/dotnet/core/tools/?ref=lukelowrey.com">dotnet cli</a> to restore, build and test</li><li>Be sure to run on ubuntu rather than Windows for better stability and more minutes</li></ul><h3 id="view-status-of-validation-on-pull-requests">View status of validation on pull requests</h3><p>When the action is in place you will see the validation status of all pull requests on the pull request tab. You will also see the checks inside the PR details and can prevent contributors merging pull requests with failing checks.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/08/image-5.png" class="kg-image" alt="GitHub Action - Build and Run .NET Tests on new pull requests" loading="lazy" width="1081" height="380" srcset="https://lukelowrey.com/content/images/size/w600/2020/08/image-5.png 600w, https://lukelowrey.com/content/images/size/w1000/2020/08/image-5.png 1000w, https://lukelowrey.com/content/images/2020/08/image-5.png 1081w" sizes="(min-width: 720px) 720px"><figcaption>Pull request tab status</figcaption></figure><p>Check out these other <a href="https://lukelowrey.com/tag/github-action/">GitHub Actions</a> &#x1F447; </p><ul><li><a href="https://lukelowrey.com/github-action-to-add-blog-posts-to-your-profile/">Add blog posts to your GitHub profile</a></li><li><a href="https://lukelowrey.com/use-github-actions-to-publish-nuget-packages/">Publish nuget packages to nuget.org</a></li></ul>]]></content:encoded></item><item><title><![CDATA[GitHub action to automatically add blog posts to your profile]]></title><description><![CDATA[Add blog posts to your GitHub profile automatically in 5 minutes via RSS and GitHub Actions.]]></description><link>https://lukelowrey.com/github-action-to-add-blog-posts-to-your-profile/</link><guid isPermaLink="false">5f3ee6a6d2fa8f0039f36751</guid><category><![CDATA[Open Source]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[GitHub Action]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sun, 23 Aug 2020 04:27:28 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2020/08/Annotation-2020-08-23-134326-min.png" medium="image"/><content:encoded><![CDATA[<h3 id="1-create-a-github-profile">1. Create a GitHub Profile</h3><img src="https://lukelowrey.com/content/images/2020/08/Annotation-2020-08-23-134326-min.png" alt="GitHub action to automatically add blog posts to your profile"><p>GitHub now allows you to <a href="https://docs.github.com/en/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme?ref=lukelowrey.com#about-your-profile-readme">add custom content to your profile</a> page via a special repository with a readme.md file. All you need to do is create a repository named the same as your username and add a README.md file. For example my profile repo is<em> lukencode/lukencode.</em></p><figure class="kg-card kg-image-card"><img src="https://lukelowrey.com/content/images/2020/08/image-1.png" class="kg-image" alt="GitHub action to automatically add blog posts to your profile" loading="lazy" width="759" height="81" srcset="https://lukelowrey.com/content/images/size/w600/2020/08/image-1.png 600w, https://lukelowrey.com/content/images/2020/08/image-1.png 759w" sizes="(min-width: 720px) 720px"></figure><h3 id="2-update-readme-md">2. Update README.md</h3><p>The README.md can have any valid markdown content you want. In addition to that you need a specially marked up <em>readme-section</em> that the action workflow below will use to inject content.</p><p>This example uses <em>&lt;!--START_SECTION:feed--&gt; </em>as the target. You can use multiple named sections.</p><!--kg-card-begin: markdown--><pre><code class="language-text"># &#x1F44B; Hi friends! I&apos;m Luke.

I lead tech teams and build things on the web. Currently CTO at [Endpoint IQ](https://endpointiq.com.au/&apos;). 


### &#x1F4D9; Blog Posts
&lt;!--START_SECTION:feed--&gt;
&lt;!--END_SECTION:feed--&gt;
</code></pre>
<!--kg-card-end: markdown--><p><em>Example <a href="https://github.com/lukencode/lukencode/blob/master/README.md?ref=lukelowrey.com">readme.md</a></em></p><h3 id="3-create-a-github-action-workflow">3. Create a GitHub Action workflow </h3><p>Add a <a href="https://github.com/features/actions?ref=lukelowrey.com">GitHub Action</a> to your repository using the &quot;<em>Actions&quot; tab -&gt; New workflow</em> on github.com or by creating a workflow.yml file in the <em>.github/workflows/</em> directory. The action will be setup to run on a schedule , look for new content in an RSS feed and update readme.md in the repository.</p><p>I used <a href="https://github.com/JasonEtco/rss-to-readme?ref=lukelowrey.com">JasonEtco/rss-to-readme</a> action to read the rss. All you need to provide is an RSS endpoint for it to look for.</p><!--kg-card-begin: markdown--><pre><code class="language-yaml">name: Update readme with blog posts
on: 
  schedule:
    # Once a day at 8 AM
    - cron: 0 8 * * *

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: JasonEtco/rss-to-readme@v1
        with:
          feed-url: https://lukelowrey.com/rss/
          readme-section: feed
          max: 10 # max number of items (default 5)
          template: &quot;#### [{{ title }}]({{ link }}) \n*{{ contentSnippet }}*\n\n&quot;
</code></pre>
<!--kg-card-end: markdown--><p><em>Example <a href="https://github.com/lukencode/lukencode/blob/master/.github/workflows/update-readme-rss.yml?ref=lukelowrey.com">update-readme-rss.yml</a></em></p><p>The YAML is pretty start forward. Rather than triggering on an action like pushing to branch it uses a simple schedule and cron expression to run daily. The rss-to-readme step reads RSS from the feed-url and injects it into the marked up section with the <em>feed</em> key in readme.md. </p><p>The template property is optional, if you leave it out the posts will display in a simple list. You can customise the output with a {{handlebars}} style template. Under the covers the action uses rss-parser to get the content. The <a href="https://github.com/rbren/rss-parser?ref=lukelowrey.com#output">properties available to you when using a custom template can be found here</a>.</p><p>The main options are:</p><ul><li>title</li><li>link</li><li>pubDate</li><li>content </li><li>contentSnippet</li></ul><!--kg-card-begin: markdown--><p>&#x1F449; TIP: change the on trigger to &quot;push to master&quot; for easy testing.</p>
<pre><code class="language-yaml">on:
  push:
    branches: [ master ]
</code></pre>
<!--kg-card-end: markdown--><p><br><strong>&#x1F386;Success! </strong>Like clockwork the action runs, finds the latest items in RSS and updates readme.md. Check out my profile on GitHub <a href="https://github.com/lukencode?ref=lukelowrey.com"><strong>github.com/lukencode</strong></a>.</p><figure class="kg-card kg-image-card"><img src="https://lukelowrey.com/content/images/2020/08/image.png" class="kg-image" alt="GitHub action to automatically add blog posts to your profile" loading="lazy" width="494" height="188"></figure>]]></content:encoded></item><item><title><![CDATA[Introducing GhostSolo - a free and open source Ghost Theme]]></title><description><![CDATA[GhostSolo is a free and open source Ghost theme created for solo bloggers.]]></description><link>https://lukelowrey.com/ghostsolo-a-free-and-open-source-ghost-theme/</link><guid isPermaLink="false">5f3135b9120d1000392c0dfb</guid><category><![CDATA[Open Source]]></category><category><![CDATA[Code]]></category><category><![CDATA[Frontend]]></category><category><![CDATA[GhostSolo]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Mon, 10 Aug 2020 12:22:53 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2020/08/lukelowrey.com_.png" medium="image"/><content:encoded><![CDATA[<img src="https://lukelowrey.com/content/images/2020/08/lukelowrey.com_.png" alt="Introducing GhostSolo - a free and open source Ghost Theme"><p>GhostSolo is a free and open source <a href="https://ghost.org/?ref=lukelowrey.com">Ghost </a>theme created primarily for solo bloggers.</p><p><strong><a href="https://github.com/lukencode/GhostSolo?ref=lukelowrey.com">GhostSolo source on Github</a></strong></p><p>I wanted something clean and simple that was focused around a single author writing mostly technical content. I wanted to make us of Ghost&apos;s inbuilt email newsletter and membership options. Finally, I wanted to build something flexible that others might find useful.</p><p>I have a little bit of work to go before the theme can be considered &quot;ready for public release&quot; but if you think you might like to try it out, feel free to <a href="https://lukelowrey.com/contact">contac</a>t me for help.</p><h3 id="features">Features</h3><ul><li>Clean responsive design</li><li>Dark/light theme toggle</li><li>The theme is customisable via CSS variables (see <a href="https://github.com/lukencode/GhostSolo?ref=lukelowrey.com#customise">https://github.com/lukencode/GhostSolo#customise</a>)</li><li>Ghost membership support (click <a href="https://lukelowrey.com/signup">subscribe</a> in the header for a live demo!)</li><li>Curate the homepage with pinned posts using #pinned tag</li><li>Tag pages - eg <a href="https://lukelowrey.com/tag/code/">Code</a></li></ul><h3 id="roadmap">Roadmap</h3><ul><li>Better support for Ghost editor blocks (bookmark etc)</li><li>Full paid membership support</li><li>Post template with table of contents</li><li>Some sort of &quot;clips&quot; tag option to format shorter posts. (Inspired by the awesome <a href="https://linear.app/changelog?ref=lukelowrey.com">linear.app changelog</a>)</li><li>Tag listing page</li><li>A cool animated header with the current post title</li><li>More post curation options</li></ul><h3 id="technical">Technical</h3><p>GhostSolo is built on <a href="https://v5.getbootstrap.com/?ref=lukelowrey.com">Bootstrap v5 </a>and the <a href="https://github.com/TryGhost/Starter?ref=lukelowrey.com">Ghost Starter Theme</a>.</p><p>I learned a lot about Ghost themes and modern frontend development while building the theme. These are posts (some coming soon) that I have written about it.</p><ul><li><a href="https://lukelowrey.com/css-variable-theme-switcher/">CSS variable dark mode toggle</a></li><li><em>How to build a custom Ghost theme</em></li><li><em>Use Github actions to build, create and publish a release</em></li></ul>]]></content:encoded></item><item><title><![CDATA[The simplest CSS variable dark mode theme]]></title><description><![CDATA[Use CSS variables and simple javascript to enable dark mode and themes on your website.]]></description><link>https://lukelowrey.com/css-variable-theme-switcher/</link><guid isPermaLink="false">5f1d557755750900451a01d1</guid><category><![CDATA[Code]]></category><category><![CDATA[Frontend]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sun, 26 Jul 2020 10:50:26 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2020/07/dark-mode.png" medium="image"/><content:encoded><![CDATA[<img src="https://lukelowrey.com/content/images/2020/07/dark-mode.png" alt="The simplest CSS variable dark mode theme"><p>I have been working on a custom <a href="https://lukelowrey.com/ghostsolo">Ghost theme</a> to power my blog (you are looking at an early version right now!). One thing I wanted to have a crack at was a dark/light theme switcher. It turns out with modern CSS this is pretty straight forward.</p><p>The approaches I considered were:</p><ul><li>CSS classes set on the &lt;body&gt;</li><li>Switch out style sheet entirely</li><li>CSS variables</li></ul><p>I went with CSS variables because my blog audience tends to be on the latest browser versions so I don&apos;t need to worry much about browser support (<a href="https://caniuse.com/?ref=lukelowrey.com#feat=css-variables">not that it is too bad</a>). </p><p><em>If a blog post is too much for you, I managed to condense it into 4 tweets:</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet" data-width="550"><p lang="en" dir="ltr">The easiest <a href="https://twitter.com/hashtag/html?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#html</a> / <a href="https://twitter.com/hashtag/css?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#css</a> / <a href="https://twitter.com/hashtag/javascript?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#javascript</a> dark mode in 4 tweets.<a href="https://twitter.com/hashtag/100DaysOfCode?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#100DaysOfCode</a> <a href="https://twitter.com/hashtag/coding?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#coding</a><br><br>1. &#x1F31E;Light (also default via :root selector) and &#x1F319;Dark theme css variables <a href="https://t.co/t9tQMM91rd?ref=lukelowrey.com">pic.twitter.com/t9tQMM91rd</a></p>&#x2014; Luke Lowrey &#x1F6F4; (@lukencode) <a href="https://twitter.com/lukencode/status/1289427747396177921?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">August 1, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure><h3 id="using-css-variables-for-themes">Using CSS variables for themes</h3><p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties?ref=lukelowrey.com">CSS variables</a> are properties you can set in your stylesheets that can be used in later styles. They are similar to tools such as SASS but built directly into CSS. This makes them very well suited for theming.</p><p>This is a simplified snippet from the default variables used in my theme <a href="https://lukelowrey.com/ghostsolo">GhostSolo</a>.</p><!--kg-card-begin: markdown--><pre><code class="language-css">:root {
    --background-color: #fff;
    --text-color: #121416d8;
    --link-color: #543fd7;
}

html[data-theme=&apos;light&apos;] {
    --background-color: #fff;
    --text-color: #121416d8;
    --link-color: #543fd7;
}

html[data-theme=&apos;dark&apos;] {
    --background-color: #212a2e;
    --text-color: #F7F8F8;
    --link-color: #828fff;
}
</code></pre>
<!--kg-card-end: markdown--><p>The :root selector is the default set of values. When <em>&lt;html data-theme=&apos;dark&apos;&gt;</em> is set those values are overridden by the<em> html[data-theme=&apos;dark&apos;] </em>values. It is really just a matter of applying the variables in your CSS to get the effect.</p><!--kg-card-begin: markdown--><pre><code class="language-css">body {
    background: var(--background-color);
    color: var(--text-color);
}

a {
    color: var(--link-color);
}

a:hover {
    text-decoration: underline;
    filter: brightness(80%);
}
</code></pre>
<!--kg-card-end: markdown--><p>In my theme I have variables for a couple of key style choices to allow distinct colour themes beyond dark/light. </p><!--kg-card-begin: markdown--><pre><code class="language-css">:root {
    --background-color: #fff;
    --alternate-background-color: #f7f7f9;
    --text-color: #121416d8;
    --text-color-light: #777676bb;
    --link-color: #543fd7;
    --masthead-color: #543fd7;
    --masthead-text: #fff;
    --button-color: #263238;
    --button-text: #fff;
    --bs-font-sans-serif: &quot;Inter&quot;, system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, &quot;Helvetica Neue&quot;, Arial, &quot;Noto Sans&quot;, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;
    --bs-font-serif: Georgia, serif;
    --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, &quot;Liberation Mono&quot;, &quot;Courier New&quot;, monospace;
}
</code></pre>
<!--kg-card-end: markdown--><blockquote>Note: the --bs prefixed variables are <a href="https://v5.getbootstrap.com/docs/5.0/customize/css-variables/?ref=lukelowrey.com">CSS variables bootstrap 5 supports</a>. The css variable support in Bootstrap is a bit limited and doesn&apos;t apply to things like <em>.btn-primary </em>or <em>.bg-light</em>.</blockquote><h3 id="javascript-dark-mode-toggle">JavaScript dark mode toggle</h3><p>With the CSS theming in place I needed a simple way to switch between &quot;dark&quot; and &quot;light&quot; modes.</p><!--kg-card-begin: markdown--><pre><code class="language-javascript">var toggle = document.getElementById(&quot;theme-toggle&quot;);

var storedTheme = localStorage.getItem(&apos;theme&apos;) || (window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches ? &quot;dark&quot; : &quot;light&quot;);
if (storedTheme)
    document.documentElement.setAttribute(&apos;data-theme&apos;, storedTheme)


toggle.onclick = function() {
    var currentTheme = document.documentElement.getAttribute(&quot;data-theme&quot;);
    var targetTheme = &quot;light&quot;;

    if (currentTheme === &quot;light&quot;) {
        targetTheme = &quot;dark&quot;;
    }

    document.documentElement.setAttribute(&apos;data-theme&apos;, targetTheme)
    localStorage.setItem(&apos;theme&apos;, targetTheme);
};

</code></pre>
<!--kg-card-end: markdown--><p>This simple JavaScript snippet will set the theme based off the user&apos;s system preferred setting using a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme?ref=lukelowrey.com">media query for</a><em><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme?ref=lukelowrey.com"> &quot;prefers-color-scheme: dark&quot;</a>. </em>It will also store the value in local storage so it can be persisted across each page. The selected theme is set as a data-theme attribute on the html node.</p><h3 id="other-css-theming-tricks">Other CSS theming tricks</h3><p>I couldn&apos;t quite get form inputs looking nice with a pure CSS variable approach. When using data attributes, you can always target them in your CSS the traditional way. This CSS gives the inputs a nice dark overlay against any background colour but didn&apos;t look right on the light theme.</p><!--kg-card-begin: markdown--><pre><code class="language-css">[data-theme=&apos;dark&apos;] .form-control {
    background-color: rgba(0, 0, 0, 0.6);
    border-color: rgba(0, 0, 0, 0.6);
    color: var(--text-color) !important;
}

[data-theme=&apos;dark&apos;] .form-control:focus {
    color: var(--text-color) !important;
}
</code></pre>
<!--kg-card-end: markdown--><p>I needed to show/hide my sun moon icons in the dark mode toggle button. I went with a Bootstrap inspired display utility approach to change an element display based on the theme.</p><!--kg-card-begin: markdown--><pre><code class="language-css">[data-theme=&apos;light&apos;] .d-block-light,
[data-theme=&apos;dark&apos;] .d-block-dark {
    display: block !important;
}
</code></pre>
<pre><code class="language-html">&lt;button id=&quot;theme-toggle&quot; class=&quot;btn btn-link btn-sm ml-2 small&quot; type=&quot;button&quot;&gt;
 &lt;span class=&quot;d-block-light d-none&quot;&gt;{{&gt; &quot;icons/moon&quot;}}&lt;/span&gt;
 &lt;span class=&quot;d-block-dark d-none&quot;&gt;{{&gt; &quot;icons/sun&quot;}}&lt;/span&gt;
&lt;/button&gt;
</code></pre>
<!--kg-card-end: markdown--><h3 id="see-it-in-action">See it in action</h3><p>Use the sun/moon icon in the nav bar of this page to see it in action. Or just watch the gif:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/07/theme-toggle.gif" class="kg-image" alt="The simplest CSS variable dark mode theme" loading="lazy" width="1419" height="1021"><figcaption>Dark mode toggle</figcaption></figure><h3 id="references">References</h3><ul><li><a href="https://medium.com/@mwichary/dark-theme-in-a-day-3518dde2955a?ref=lukelowrey.com">Dark theme in a day - Marcin Wichary</a> - some great advanced techniques with HSL colours and transitions</li><li><a href="https://css-tricks.com/a-complete-guide-to-dark-mode-on-the-web/?ref=lukelowrey.com">A complete guide to dark mode - CSS tricks</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Use GitHub actions to publish NuGet packages]]></title><description><![CDATA[How to automate creating and pushing nuget packages from GitHub to nuget.org and other sources.]]></description><link>https://lukelowrey.com/use-github-actions-to-publish-nuget-packages/</link><guid isPermaLink="false">5edb6f68edd3830039a37e46</guid><category><![CDATA[dotnet]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[GitHub Action]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sat, 06 Jun 2020 12:47:14 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1590595906931-81f04f0ccebb?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1590595906931-81f04f0ccebb?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Use GitHub actions to publish NuGet packages"><p>One of the most frustrating aspects of maintaining an open source .NET Core library has been publishing the nuget packages on <a href="https://www.nuget.org/?ref=lukelowrey.com">nuget.org</a>. In the world of devops automation, manually creating and uploading packages felt so old-fashioned (don&apos;t get me started on Azure Devops).</p><p>Here is how I automated pushing packages to nuget.org for <a href="https://github.com/lukencode/FluentEmail/?ref=lukelowrey.com">FluentEmail</a>.</p><ol><li>Create a Github Action that runs on push to the master branch</li><li>Use <a href="https://github.com/marketplace/actions/publish-nuget?ref=lukelowrey.com">Publish Nuget</a> action to package and publish nuget packages</li><li>Create a Nuget.org API key and set it as a GitHub secret</li><li>Nuget packages are published on push to master!</li></ol><h3 id="create-a-github-action">Create a GitHub Action</h3><p><a href="https://github.com/features/actions?ref=lukelowrey.com">GitHub actions</a> makes it easy to automatically build test and deploy code hosted on GitHub. There are heaps of community built actions that cover the whole #devops spectrum.</p><p>Creating a new workflow creates a yaml file in .github/workflows for the repository. Infrastructure as code! The best way to start is create a new workflow using the .NET Core starter action.</p><figure class="kg-card kg-image-card"><img src="https://lukelowrey.com/content/images/2020/06/image-2.png" class="kg-image" alt="Use GitHub actions to publish NuGet packages" loading="lazy"></figure><p>This action sets up a simple dotnet environment. It is a great starting point for running validation builds and tests on new pull requests. For publishing packages running the action on push to the master branch works best.</p><p>Here is the full yaml for the workflow I am using. It uses actions to setup .Net Core, restore packages, build and publish nuget.</p><!--kg-card-begin: markdown--><pre><code class="language-yaml">name: Publish Packages

on:
  push:
    branches: [ master ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Setup .NET Core
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 3.1.101
    - name: Install dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --configuration Release --no-restore
    - name: Publish FluentEmail.Core
      uses: brandedoutcast/publish-nuget@v2.5.2
      with:
          PROJECT_FILE_PATH: src/FluentEmail.Core/FluentEmail.Core.csproj
          NUGET_KEY: ${{secrets.NUGET_API_KEY}}
</code></pre>
<!--kg-card-end: markdown--><h3 id="publish-nuget-package-action">Publish nuget package action</h3><p><a href="https://github.com/brandedoutcast?ref=lukelowrey.com">brandedoutcast </a>has created a reusable GitHub Action to <a href="https://github.com/marketplace/actions/publish-nuget?ref=lukelowrey.com">Publish Nuget</a> packages. You can see it referenced in the <em>&quot;uses:&quot;</em> section in the workflow above. This action does the heavy lifting for packaging and publishing nuget packages. whenever the project version is updated. The action (by default) looks for changes to the <em>&lt;version&gt;1.0&lt;/version&gt;</em> tag in your csproj file.</p><p>There are a couple of variables you can set to get the process working just right. For a basic setup you only really need to set <em>PROJECT_FILE_PATH</em> and <em>NUGET_KEY</em>.</p><p>The full publish-nuget variable list allows you to tweak the versioning method and format as well as target package sources other than nuget.org.</p><!--kg-card-begin: markdown--><pre><code># Filepath of the project to be packaged, relative to root of repository
PROJECT_FILE_PATH: YourProject/YourProject.csproj
          
# NuGet package id, used for version detection &amp; defaults to project name
# PACKAGE_NAME: YourProject

# API key to authenticate with NuGet server
NUGET_KEY: ${{secrets.NUGET_API_KEY}}

# NuGet server uri hosting the packages, defaults to https://api.nuget.org
# NUGET_SOURCE: https://api.nuget.org
          
# Filepath with version info, relative to root of repository &amp; defaults to PROJECT_FILE_PATH
# VERSION_FILE_PATH: Directory.Build.props

# Regex pattern to extract version info in a capturing group
# VERSION_REGEX: &lt;Version&gt;(.*)&lt;\/Version&gt;
          
# Useful with external providers like Nerdbank.GitVersioning, ignores VERSION_FILE_PATH &amp; VERSION_REGEX
# VERSION_STATIC: 1.0.0

# Flag to toggle git tagging, enabled by default
# TAG_COMMIT: true

# Format of the git tag, [*] gets replaced with actual version
# TAG_FORMAT: v*


# Flag to toggle pushing symbols along with nuget package to the server, disabled by default
# INCLUDE_SYMBOLS: false</code></pre>
<!--kg-card-end: markdown--><h3></h3><h3 id="create-nuget-org-api-key-as-github-action-secret">Create Nuget.org API key as GitHub Action Secret</h3><p>The workflow above uses the nuget API to automatically push the package. You need to create an API key for your profile on nuget.org.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/06/image-3.png" class="kg-image" alt="Use GitHub actions to publish NuGet packages" loading="lazy"><figcaption>Create API Keys from your nuget.org profile</figcaption></figure><p>You can then enter the key as a<a href="https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets?ref=lukelowrey.com"> secret in your GitHub repository</a>. The key for the secret must be <em>NUGET_API_KEY</em>. The API key will then be available to the publish package action workflow.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/06/image-4.png" class="kg-image" alt="Use GitHub actions to publish NuGet packages" loading="lazy"><figcaption>Setting a GitHub secret</figcaption></figure><h3 id="results">Results</h3><p>Now when code is push to the master branch (and the project version changes) the publish packages action run and publishes the nuget package! </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/06/image-5.png" class="kg-image" alt="Use GitHub actions to publish NuGet packages" loading="lazy"><figcaption>Success!</figcaption></figure><p>A more advanced branching and action setup would allow you to publish pre-release versions from a beta branch and full release versions from master.</p><hr><p><em>If you found this useful, retweet or like the thread on twitter.</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet" data-width="550"><p lang="en" dir="ltr">How I use GitHub actions to automatically publish <a href="https://twitter.com/nuget?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">@nuget</a>  packages for a .NET core library<a href="https://t.co/xOD5IqklwC?ref=lukelowrey.com">https://t.co/xOD5IqklwC</a></p>&#x2014; Luke Lowrey (@lukencode) <a href="https://twitter.com/lukencode/status/1270101881885388801?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">June 8, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item><item><title><![CDATA[Presentation tips for a better product demo]]></title><description><![CDATA[Software demos can be stressful and overwhelming. Use these tips to deliver a confident and professional demo.]]></description><link>https://lukelowrey.com/product-demo-presentation-tips/</link><guid isPermaLink="false">5ecb5d1d8581cf0045dab612</guid><category><![CDATA[Product]]></category><category><![CDATA[Business]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Wed, 03 Jun 2020 10:42:34 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1528238646472-f2366160b6c1?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1528238646472-f2366160b6c1?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Presentation tips for a better product demo"><p>Giving product demos can be stressful and overwhelming. You need to focus on the software, your presentation and keep your potential client&apos;s needs at the front of your mind. These tips help me stay focused and deliver a confident and professional demo.</p><h3 id="be-prepared">Be prepared</h3><p>The most valuable step you can take to improve your product demos is preparation. I have written a <a href="https://lukelowrey.com/product-demo-checklist/">demo preparation guide</a> that outlines the research, planning and practice techniques that have helped me.</p><h3 id="don-t-go-alone">Don&apos;t go alone</h3><p>It is very difficult to maintain your focus and enthusiasm for any presentation longer than half an hour. In my experience it is nearly always best to bring some backup from your team. An extra person can concentrate closely on questions, get a better read on how the demo goes, and give you a chance to catch your breath every now and then.</p><ul><li>Only bring people that will be able to contribute. I have found that using two presenters works well. One person takes control of the software and the other handles the introductions and company overview, and is the first call for questions.</li><li>Each person included in the presentation should be given some space to contribute. It could be as simple as splitting the introduction and demo between you or handing off a standard technical explanation. There is nothing more awkward then sitting through an hour meeting and never having a chance to contribute.</li><li>Try to include people to cover both business and technical expertise.</li></ul><h3 id="be-friendly-but-stick-to-business">Be friendly but stick to business</h3><p>It is very useful to break the ice with your audience before you kick on into the demo. My advice is to keep it simple and brief; you don&apos;t want to appear unfriendly but you also need to ensure you have time to show your software in the best light.</p><h3 id="mostly-stick-to-your-plan">Mostly stick to your plan</h3><p>Hopefully you have come prepared with a demo of some high-level user stories and a demo script. A demo where no one says anything is not a good sign but being peppered with questions and never getting into a groove is also an issue. You need to balance answering questions and adapting to the audience&apos;s preference with telling the story of your product the way you have planned.</p><ul><li>Let the audience know up front what you are going to show them. It gives them a chance to point out what is important to them and will also give them an idea of what will be shown so they don&apos;t need to jump ahead with questions.</li><li>Always describe the software in business terms rather than technical functions. An easy way to remind yourself to do this is starting explanations with <em>&quot;As a [end user type] I can...&quot;</em></li><li>Stop and ask for questions at convenient spots in your presentation. For example, once you have completed some workflow.</li></ul><h3 id="control-your-tempo">Control your tempo</h3><p>A problem that many developers have (including me) when presenting software is moving too fast. When you know the product well and have a time limit it is easy to blow through every function of the software at a million miles an hour.</p><ul><li>Don&apos;t try to show everything. You should spend the most time on your product&apos;s strongest features; no need to open every page.</li><li>Deliberately pause at key discussion points. I normally mark these in my demo script. You want to leave on the screen interesting, strong features of the product as you pause and make your point. </li><li>When you pause, take your hands completely off the keyboard and mouse. This is your cue to look at the camera or audience and speak using hand gestures.</li><li>Go easy on the mouse. Try not to dart your mouse all over the screen. An annoying habit I need to be aware of is flicking my mouse all around the button I am talking about. Slow your mouse movement down and take your hand off completely when you talk.</li></ul><h3 id="tips-for-presenting-remotely-over-video">Tips for presenting remotely over video</h3><p></p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/demo-screen.png" class="kg-image" alt="Presentation tips for a better product demo" loading="lazy"><figcaption>Example remote demo screen setup</figcaption></figure><ul><li>Prepare your software and desktop. Get your product setup in its own window or tabs. Close down your chat app, email and anything else that could pop something embarrassing onto the screen.</li><li>Share as little as possible. If you are demoing a web application, stick to the web browser. As above you want to limit the chance of oversharing.</li><li>Keep your script on screen. If you have a big monitor and can limit your screen sharing to a single window it is very useful to keep your notes visible to the side of the main demo.</li><li>Get the right window size and zoom level. With a big widescreen monitor sharing Fullscreen is not always the best option. Play around with window size and zoom level before the demo to find a good fit.</li><li>Open by stating <em>&quot;We are looking at the homepage...&quot;</em> this should prompt someone to interrupt your screenshare is not working and is more natural than <em>&quot;can everyone see this?&quot;</em></li><li>Make sure you look at the camera. I have a habit of watching my own video feed when presenting. Because of this I tend to move my video to just below my webcam to ensure I am always facing the camera.</li><li>Try a custom virtual background. For a simple, branded background try <a href="https://www.actionablebackground.com/?ref=lukelowrey.com">actionablebackground.com</a>. If you know the audience well or have a nice office, I don&apos;t think this is necessary.</li></ul><h3 id="tips-for-presenting-in-person">Tips for presenting in person</h3><ul><li>Try to position yourself at the front of a meeting room (in front of the big screen) facing toward the audience.</li><li>Pausing is your cue to look around and make eye contact. It is also a good way to encourage questions from your audience.</li><li>Always assume the presentation equipment will be missing the cables and adaptors you need. Bring your own.</li><li>As above, have a backup internet source ready to go.</li></ul><hr><p><em>If you enjoyed the post, like or retweet the thread on twitter.</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet" data-width="550"><p lang="en" dir="ltr">My presentation tips for running software demos<a href="https://t.co/PaikSK4ZU3?ref=lukelowrey.com">https://t.co/PaikSK4ZU3</a></p>&#x2014; Luke Lowrey (@lukencode) <a href="https://twitter.com/lukencode/status/1270468261138714626?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">June 9, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item><item><title><![CDATA[How to prepare the perfect product demo]]></title><description><![CDATA[Improve the quality of your next product demo using this preparation checklist as a guide. No matter how well you know the software, you need a plan.]]></description><link>https://lukelowrey.com/product-demo-checklist/</link><guid isPermaLink="false">5ebccf213628990039b28332</guid><category><![CDATA[Product]]></category><category><![CDATA[Business]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Wed, 27 May 2020 21:36:08 GMT</pubDate><media:content url="https://lukelowrey.com/content/images/2020/07/photo-1517245386807-bb43f82c33c4.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://lukelowrey.com/content/images/2020/07/photo-1517245386807-bb43f82c33c4.jpg" alt="How to prepare the perfect product demo"><p>You don&apos;t have to be a sales person to deliver a great software demonstration. I have found the key to improving the quality and consistency of my demos is research, planning and practice. &#xA0;This demo preparation checklist helped me transform from rambling techie to educated and coherent domain expert.</p><h3 id="know-your-audience">Know your audience</h3><p>I view software demonstrations similarly to job interviews; there is an expectation that you have researched the company you are presenting to and the people who will be attending. You need solid grasp of their core business and where your product fits it. It is also useful to know in advance the roles of the attendees and a little about their background. Hopefully you can save yourself the embarrassment of over explaining a point to an expert in the field. Public company websites and LinkedIn profiles are a good source of information. </p><h3 id="ask-questions-in-advance">Ask Questions in Advance</h3><p>Send an email before the meeting to ask questions in advance. Use this information to tailor your demo. These types of questions are good options to discover the key issues:</p><ul><li>What are the roles and background of those attending?</li><li>What are your top 3 pain points with your current process?</li><li>Are you currently using any other tools in the process?</li></ul><p>What you learn here researching your audience should then be used to fine tune the demo story and script.</p><h3 id="craft-user-stories">Craft user stories</h3><p>Before you start rattling off the product features to show in your run through, take a step back and imagine the software through a user&apos;s eyes. Those in software would be familiar with a <a href="https://www.atlassian.com/agile/project-management/user-stories?ref=lukelowrey.com">user story</a>. User stories help frame software requirements from the point of view of the end user. They are useful when preparing your product demo for the same reason. </p><p>A typically agile user story is formatted like this:</p><blockquote>As a [ROLE], I want to [ACTION] (so that [BENEFIT])</blockquote><p>The format works for planning software demos as well. The key difference is while in an agile framework the user story is the smallest unit of work in a software demo it tends to have a larger scope. Depending on how your product works it may be useful to design one story for each type of user interacting with the system.</p><p>Here are some examples I have used recently.</p><blockquote>As a <strong>potential student</strong>, I want to <strong>apply for a research project</strong> so that <strong>I can start research at the university</strong><br><br>As a <strong>supervisor</strong>, I want to<strong> review and progress my student projects </strong>so that <strong>my students don&apos;t miss any milestones</strong><br><br>As a <strong>research officer</strong>, I want to <strong>ensure student projects are meeting milestones </strong>so that ....</blockquote><p>You should have a couple of key user stories for your product that you can mix and match depending on what you know about your audience. The more familiar your demo feels the easier it is for the potential client to imagine themselves using your software in production.</p><p>I tend to pick two or three user stories to base my demos around. These stories are separate but should fit together to form the overall picture of your software. Often &quot;data collection, &quot;data workflow&quot; and &quot;reporting and analytics&quot; flow well together. </p><h3 id="write-a-demo-script">Write a demo script</h3><p>After you have a clear idea of the stories you want to tell draft a script or run sheet around them. The script needs to act as a high-level guide, not detail step by step actions to complete. When presenting as you need to be flexible with audience questions and preferences but try to act within your broad script. </p><blockquote>If you don&apos;t know the product well enough to work off high level notes it should be someone else running the show.</blockquote><p>Write a section for each user story you are going to cover. You should try to simulate the process that the user would take in the product.</p><ul><li><strong>Don&apos;t hold back on the good stuff.</strong> Including some stronger features/screens/reports early on in your run through. You want people to be engaged early.</li><li><strong>Don&apos;t try to include everything.</strong> You need to spend time showing your best and most relevant features so don&apos;t feel like you need to &quot;feature dump&quot; every button and every screen in your plan. This is a trap that developers often fall into.</li><li><strong>Include &quot;actions&quot; in your script</strong>. You need to show the process of your software, not just what every screen looks like. Seeing data change state and move around your system is key to making it &quot;feel real&quot; for the audience.</li><li><strong>Avoid complex data entry.</strong> You should &quot;do things&quot; in the demo as the point above suggests but no one will enjoy watching you fumble typing for 20 minutes only to fail form validation. If it is going to take too long, have an example ready to go.</li></ul><p>My preference is for each user story to outline each function you intend to show. Within each function I add details on how it should be delivered. I tend to colour code the details along these lines:</p><ul><li>Yellow is a key talking point. This is normally a specific business benefit that I want to highlight. Yellow is my cue to slow down while presenting and make sure I get my point across.</li><li>Green is an action I need to take in the system. I normally highlight in green things like submitting forms, changing status or anything that I need to actually do (as opposed to just show) in the system to proceed.</li><li>Purple is a detour or action outside of the main system. I often end up cross selling related products during our demos. Purple is a reminder to me to open up that other product/tab/example outside of the normal demo flow.</li><li>Red is something I need to sort out before the demo. Things I highlight in red include missing data, questions to clarify about the client or (hopefully not) errors in the product. Red needs to be sorted well before demo time.</li><li>Uncoloured items are general reminders on the story flow or minor talking points.</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/image-2.png" class="kg-image" alt="How to prepare the perfect product demo" loading="lazy"><figcaption>Example run sheet</figcaption></figure><h3 id="do-i-need-demo-slides">Do I need demo slides?</h3><p>Slide decks are not my strong point and I am a firm believer in the &quot;show don&apos;t tell&quot; method. However, having some simple high-level slides ready can help structure your introductions and drive home the key benefits you are pitching.</p><ul><li>Introductions. Include the names and roles of the presenters from your company. This is a good reminder to actually do intros as it is something that can be missed and is awkward to pick up later on.</li><li>Company overview. Your company&apos;s key information and mission.</li><li>Product overview and key benefits. Include the name and logo of the product you are showing. Try to distil the top 1-3 benefits into this slide and refer to them in your script.</li><li>[Optional] Description of the user stories you want to show. Sometimes a simple process diagram can be useful here.</li><li>Conclusion. A slide to show at the end of the demo. Summarise the next steps and key contact details. </li></ul><h3 id="demo-data">Demo Data</h3><p>In the ideal world you configure your demo data to match the audience as closely as possible. For important demonstrations I have used publicly available client polices and documents to configure our demonstration site as closely as possible to how I envision the client would operate in production.</p><blockquote>The bottom line is your demo data has to be good. &quot;Test project 1&quot; and &quot;This is a comment&quot; is not good enough.</blockquote><p>Your demo data should be good enough that you not find yourself saying &quot;this is just demo data&quot;. </p><p>You should avoid &quot;smoke and mirrors&quot; in your data and functionality. If you don&apos;t have good data for a report showing a screenshot is fine as long as you aren&apos;t trying to pass it off as the real deal.</p><h3 id="know-your-weaknesses">Know your weaknesses</h3><p>Put yourself into the shoes of your future client and ask &quot;what is missing from this product?&quot;. You will know the software better than anyone so it shouldn&apos;t be too difficult to come up with some potential gaps. They key is to have a decent (and honest) answer to what may be lacking in your product.</p><p>Here are some ways to frame missing functionality when asked:</p><ul><li>Ensure you have accounted for it on your product road map. This should be something you can provide to a client if requested.</li><li>Have an explanation for an alternative method or workaround in the system.</li><li>Accept the question (if it comes) as important feedback and make an effort to acknowledge it in any follow up communications.</li><li>Be confident about what is out of scope for your product</li></ul><h3 id="practice">PRACTICE</h3><p>Like all things in life, the more you practice the better you get. It can be tricky to find an audience for a &quot;real&quot; dry run of a software demo. You can fall into the trap of not selling to them because they are not a real client.</p><p>I normally go with two types of practice. One is a &quot;simulated&quot; run through of the demo with a team member - ideally someone who is involved in the meeting. I have my script up on screen and we step through making tweaks where necessary.</p><p>The second practice is a &quot;live&quot; run of the demo. This can still work well with team members, especially those not familiar with the product. Often the best candidates (and the hardest to get hold of) are in leadership and executive positions as they best emulate the decision makers that you need to convince. </p><p>Another option is to do regular internal team &quot;functionality presentations&quot;. These work best if kept short - one user story from your plan. Internal demonstrations have the added benefit of knowledge sharing - both of the product and how to show it off effectively. </p><h3 id="presentation-and-delivery">Presentation and delivery</h3><p>I have written a separate post with my <a href="https://lukelowrey.com/product-demo-presentation-tips/">top product demo presentation tips</a>. They relate closely to the preparation outlined in this post.</p><hr><p><em>If you enjoyed the post, perhaps like or retweet the thread on twitter</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet" data-width="550"><p lang="en" dir="ltr">I&apos;ve written guide on researching and planning software demos. These tips have massively improved my demos this year.<a href="https://t.co/h9bSScewl3?ref=lukelowrey.com">https://t.co/h9bSScewl3</a></p>&#x2014; Luke Lowrey (@lukencode) <a href="https://twitter.com/lukencode/status/1265768957593571328?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">May 27, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item><item><title><![CDATA[The future of FluentEmail]]></title><description><![CDATA[FluentEmail - the open source .NET Core email library I created now has over 1000 stars on GitHub! I am looking for collaborators to help me continue development.]]></description><link>https://lukelowrey.com/fluentemail-future/</link><guid isPermaLink="false">5ecdf5a48581cf0045dab6b0</guid><category><![CDATA[Open Source]]></category><category><![CDATA[FluentEmail]]></category><category><![CDATA[dotnet]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Wed, 27 May 2020 05:19:56 GMT</pubDate><content:encoded><![CDATA[<p>FluentEmail - the open source .NET Core email library I created (with <a href="https://twitter.com/BenWhoLikesBeer?ref=lukelowrey.com">Ben Cull</a>) now has over 1000 stars on GitHub! However, I have to apologise to those using the library as I have been a fairly absent contributor over the last year (I co-founded a company and had twins in that time).</p><p>The library has come a long way in the last few years <a href="https://star-history.t9t.io/?ref=lukelowrey.com#lukencode/fluentemail">https://star-history.t9t.io/#lukencode/fluentemail</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/image-3.png" class="kg-image" alt loading="lazy"><figcaption>FluentEmail stars</figcaption></figure><p>I am planning on spending some time cleaning up the project going through outstanding pull requests and getting a new version up and running. I also want to write up a complete guide to serve as some documentation (if anyone has already written something let me know!).</p><p>I am also on the lookout for a couple of people willing to put their hand up and become a contributor. I hope they could take some responsibility for reviewing and merging pull requests so we can keep the project moving along. Please <a href="https://lukelowrey.com/contact">contact me</a> if you are interested.</p><p>I am also hoping to get access to <a href="https://help.github.com/en/github/building-a-strong-community/about-team-discussions?ref=lukelowrey.com">GitHub discussions</a> to make communication with the community easier. If you can pull any strings to get that enabled on the FluentEmail repository that would be great.</p>]]></content:encoded></item><item><title><![CDATA[Closing Austechjobs.com.au]]></title><description><![CDATA[<p>I built Austechjobs.com.au to try fill a gap I saw in the Australian developer and technology recruitment market. It was a focussed, recruiter free job board which I hoped would be the go-to place for the top developer talent in Australia.</p><p>Some months after launching we added twins</p>]]></description><link>https://lukelowrey.com/closing-austechjobs-com-au/</link><guid isPermaLink="false">5eb292f892411a0039b031f6</guid><category><![CDATA[Hiring]]></category><category><![CDATA[Technology]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sun, 17 May 2020 01:46:06 GMT</pubDate><content:encoded><![CDATA[<p>I built Austechjobs.com.au to try fill a gap I saw in the Australian developer and technology recruitment market. It was a focussed, recruiter free job board which I hoped would be the go-to place for the top developer talent in Australia.</p><p>Some months after launching we added twins to our family, then the coronavirus dried up the jobs available. I ran out of the time required to maintain the site and build momentum and made the decision to close it down.</p><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/screenshot-home.png" class="kg-image" alt loading="lazy"><figcaption>RIP Austechjobs</figcaption></figure><p>I hope I can return the space with a new project some time in my future. I would particularly like to do something in the pre-screening phase of hiring developers. Until then I recommend you check out my post on<a href="https://lukelowrey.com/find-developers-to-hire-in-australia/"> where to find developers in Australia.</a></p>]]></content:encoded></item><item><title><![CDATA[Text based stand-ups]]></title><description><![CDATA[The advantages of using text based stand-up updates in addition to (or in place of) a meeting.]]></description><link>https://lukelowrey.com/text-based-standups/</link><guid isPermaLink="false">5ebbc0a53628990039b2826a</guid><category><![CDATA[Management]]></category><category><![CDATA[Code]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sun, 17 May 2020 00:35:25 GMT</pubDate><content:encoded><![CDATA[<p>Like a lot of software companies Endpoint IQ runs a daily team stand-up. It keeps the team promotes accountability and transparency. But you don&apos;t have to go far to find a developer who loathes the morning stand-up ritual. For some it can cause anxiety while most fail to see the benefit.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/standup-cancelled.jpg" class="kg-image" alt loading="lazy"><figcaption>Stand-up cancelled</figcaption></figure><p>We used to do the traditional stand up in a circle and talk method but I have found the most effective way to keep everyone informed is switching to simple, text-based updates. </p><p>Every morning the team posts an update in our #standup chat channel with something along the lines of:</p><ul><li><strong>What I did yesterday?</strong></li><li><strong>What am I doing today?</strong></li><li><strong>Are there any blockers?</strong></li></ul><p>The format itself isn&apos;t important as long as we capture where we are up to, what we plan to do and if anything is stopping us.</p><figure class="kg-card kg-image-card kg-width-full kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/image-1.png" class="kg-image" alt loading="lazy"><figcaption>Example text stand up update</figcaption></figure><p>Writing the update as opposed to just saying it during a meeting is quick and I find it has some real advantages:</p><!--kg-card-begin: markdown--><ul>
<li>It forces me to think before we I start work and make sure my plans are aligned with what the team is trying to do.</li>
<li>Keeps me focused during the day. Having a record of what I planned to achieve and knowing it will be revisited the next day keeps met accountable.</li>
<li>Stand-ups are not dominated by loudest voices.</li>
<li>Allows us to be flexible times. We have people start from 7am - 9:30am as long as you provide your update you never really &quot;miss&quot; a stand-up</li>
</ul>
<!--kg-card-end: markdown--><p>This type of short, recurring daily update lends itself well to collaboration tools like Slack and Microsoft Teams. Endpoint IQ uses a dedicated Teams channel #standups to post the updates. Any questions or updates from the team takes place in the replies to the original post. This keeps the discussions easy to follow over the course of the week. I also encourage people to posts screenshots, videos or gifs of their work.</p><p>We tried out a couple of &quot;stand-up bots&quot; which attempt to automate the process by sending reminders and consolidating responses. The bots generally got in the way more than they helped. </p>]]></content:encoded></item><item><title><![CDATA[Where to find developers to hire in Australia]]></title><description><![CDATA[<p><em>Originally posted on the now defunct austechjobs.com.au</em></p><p>Austechjobs has started to collect the best channels to find devs to hire in Australia. The channels listed fall into a couple of broad categories:</p><h3 id="job-boards">Job boards</h3><p>This is the most common way hiring managers advertise developer positions. The usual suspects</p>]]></description><link>https://lukelowrey.com/find-developers-to-hire-in-australia/</link><guid isPermaLink="false">5eb2917292411a0039b031d7</guid><category><![CDATA[Hiring]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Tue, 01 Jan 2019 10:31:00 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally posted on the now defunct austechjobs.com.au</em></p><p>Austechjobs has started to collect the best channels to find devs to hire in Australia. The channels listed fall into a couple of broad categories:</p><h3 id="job-boards">Job boards</h3><p>This is the most common way hiring managers advertise developer positions. The usual suspects are big guns like Seek and LinkedIn but you can also find success on more focused niche job boards (like austechjobs!). There are some solid global developer focused boards such as StackOverflow Jobs and Github where you will find quality developers but you should be aware if you want to hire international visa applicants you will need to advertise locally too.</p><h3 id="meetups-and-user-groups">Meetups and User groups</h3><p>There are many high quality and active technical meetups/user groups in Australia. Most of them have a presence on <a href="http://meetup.com/?ref=lukelowrey.com" rel="noreferrer nofollow">meetup.com</a>. It is possible to reach out via the comments section of meetups you are a part of to let the members know about your position. Generally speaking, you should only post on meetups you plan to attend in person and you need to be careful not to fall foul of <a href="http://meetup.com/?ref=lukelowrey.com" rel="noreferrer nofollow">meetup.com</a> spam detection by repeatedly posting across multiple groups. There should be a couple of meetups in each major city that would be relevant for any developer position.</p><h3 id="slack-communities">Slack communities</h3><p>Developer slack groups are a great place to find talented and engaged developers. Generally speaking those who join these communities are passionate about their work. Most Slack groups will most likely have a designated channel for promoting your position. The best groups have clear rules on what you can and can&apos;t post so be sure to read through these before you post anything. You should make yourself available to answer any followup questions from the community.</p><h3 id="facebook">Facebook</h3><p>Facebook groups are less likely to be professionally orientated that Meetups and Slack but there are still some good ones out there. The groups listed below are generally focused toward startup jobs rather than enterprise or government positions.</p><p>Below is the list of channels to connect with developers in Australia. </p><figure class="kg-card kg-embed-card"><iframe class="airtable-embed" src="https://airtable.com/embed/shr4cqFL8GrgRs3bw/tblintCULGzj4AyEW?blocks=hide" frameborder="0" onmousewheel width="100%" height="533" style="background: transparent; border: 1px solid #ccc;"></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Automatically remove unused css from Bootstrap or other frameworks]]></title><description><![CDATA[Remove the bloat from your CSS with PurgeCSS and Webpack.]]></description><link>https://lukelowrey.com/automatically-removeunused-css-from-bootstrap-or-other-frameworks/</link><guid isPermaLink="false">5ec08bc156b3b10039d78072</guid><category><![CDATA[Code]]></category><dc:creator><![CDATA[Luke Lowrey]]></dc:creator><pubDate>Sun, 29 Jul 2018 00:56:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1555952494-efd681c7e3f9?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><img src="https://images.unsplash.com/photo-1555952494-efd681c7e3f9?ixlib=rb-1.2.1&amp;q=80&amp;fm=jpg&amp;crop=entropy&amp;cs=tinysrgb&amp;w=2000&amp;fit=max&amp;ixid=eyJhcHBfaWQiOjExNzczfQ" alt="Automatically remove unused css from Bootstrap or other frameworks"><p>I love bootstrap (and other css frameworks). For a developer like me who often works on web projects without any design input it is a real lifesaver.<br>
The problem is since bootstrap is a kitchen sink type framework (although you can pick and choose via sass) the css file size can get out of hand quickly.<br>
Bootstrap.min.css weighs in at 138 KB. If you add a theme and custom css it can easily bump you over 200 kb.</p>
<h2 id="whatispurgecss">What is PurgeCSS?</h2>
<p><a href="https://www.purgecss.com/?ref=lukelowrey.com">PurgeCSS</a> is a node.js package that can detect and remove unused css selectors. It will analyze your view pages - be they HTML<br>
or a templating engine and build a list of the css selectors in use. PurgeCSS will then take your stylesheets and remove any selectors that are not<br>
present in your views.</p>
<h2 id="howtousepurgecsswithwebpack">How to use PurgeCSS with Webpack</h2>
<p>I setup PurgeCSS using the <a href="https://www.purgecss.com/with-webpack?ref=lukelowrey.com">webpack plugin</a> which works in conjunction with the extract-text-webpack-plugin. My webpack config<br>
is based off the <a href="https://lukencode.com/2018/04/14/simple-webpack-config-to-build-javascript-sass-and-css-using-npm-and-aspnet-core/?ref=lukelowrey.com">Simple webpack config</a> I have written about previously.</p>
<p>The webpack config was already setup to process sass and combine css into a single file. I added the PurgecssPlugin which will run at after the initial css processing.</p>
<p><code>npm i -D purgecss-webpack-plugin</code></p>
<p><code>npm i -D extract-text-webpack-plugin</code></p>
<pre><code class="language-javascript">const path = require(&apos;path&apos;)
const glob = require(&apos;glob&apos;)
const webpack = require(&apos;webpack&apos;)
const ExtractTextPlugin = require(&apos;extract-text-webpack-plugin&apos;)
const PurgecssPlugin = require(&apos;purgecss-webpack-plugin&apos;)

const outputDir = &apos;./wwwroot/dist&apos;
const entry = &apos;./wwwroot/js/app.js&apos;
const cssOutput = &apos;site.css&apos;

module.exports = (env) =&gt; {    
    return [{
        entry: entry,
        output: {
            path: path.join(__dirname, outputDir),
            filename: &apos;[name].js&apos;,
            publicPath: &apos;/dist/&apos;
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: &apos;babel-loader&apos;
                },
                {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        use: [&apos;css-loader&apos;],
                        fallback: &apos;style-loader&apos;
                    })
                },
                {
                    test: /\.scss$/,
                    use: ExtractTextPlugin.extract({
                        use: [&apos;css-loader&apos;, &apos;sass-loader&apos; ],
                        fallback: &apos;style-loader&apos;
                    })
                }
            ]
        },
        plugins: [
            new ExtractTextPlugin(cssOutput),
            new PurgecssPlugin({
                paths: glob.sync(&apos;./Views/**/*.cshtml&apos;, { nodir: true }),
                whitelistPatterns: [ /selectize-.*/ ]
            })
        ]
    }]
}
</code></pre>
<p>The plugin accepts an array of paths to view files to search for css selectors in use. The glob package accepts a search pattern<br>
and generates a list of files. I am looking for .cshtml view files in my .net web app.</p>
<pre><code class="language-javascript">paths: glob.sync(&apos;./Views/**/*.cshtml&apos;, { nodir: true })
</code></pre>
<h2 id="whitelistselectors">Whitelist selectors</h2>
<p>The whitelistPatterns parameter allows you to exclude selectors from the purge that may not have been present in the paths.<br>
I found that the <a href="https://selectize.github.io/selectize.js/?ref=lukelowrey.com">selectize plugin</a> I am using has css classes that are added dynamically and were being removed<br>
so I added a pattern to match the prefix of its css classes. I could alternatively include the .js file for this plugin with the paths parameter.</p>
<pre><code class="language-javascript">whitelistPatterns: [ /selectize-.*/ ]
</code></pre>
<p>Running PurgeCSS on one of my (admittedly heavy) bootstrap based web sites reduced the css file size from 159KB to 60KB with essentially no effort on my end!</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://lukelowrey.com/content/images/2020/05/purgecss-results.png" class="kg-image" alt="Automatically remove unused css from Bootstrap or other frameworks" loading="lazy"><figcaption>Purge CSS Results</figcaption></figure><hr><p><em>If you found this post useful please like or retweet the thread on twitter.</em></p><figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet" data-width="550"><p lang="en" dir="ltr">Speed up your site by automatically removing unused CSS from <a href="https://twitter.com/getbootstrap?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">@getbootstrap</a> (or other frameworks)<a href="https://twitter.com/hashtag/webdevelopment?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#webdevelopment</a> <a href="https://twitter.com/hashtag/css?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#css</a> <a href="https://twitter.com/hashtag/webdev?src=hash&amp;ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">#webdev</a><a href="https://t.co/CNMecHo1gl?ref=lukelowrey.com">https://t.co/CNMecHo1gl</a></p>&#x2014; Luke Lowrey &#x1F6F4; (@lukencode) <a href="https://twitter.com/lukencode/status/1274564633697779712?ref_src=twsrc%5Etfw&amp;ref=lukelowrey.com">June 21, 2020</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>]]></content:encoded></item></channel></rss>