<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Ops Community ⚙️: Andrew Owen</title>
    <description>The latest articles on The Ops Community ⚙️ by Andrew Owen (@aowendev).</description>
    <link>https://community.ops.io/aowendev</link>
    <image>
      <url>https://community.ops.io/images/QqZniCVj7P-ZBjxn2MOZJ3bKgzf54do-CiSth-u6QKY/rs:fill:90:90/g:sm/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL3Vz/ZXIvcHJvZmlsZV9p/bWFnZS85Ny9kNDM0/ZjU5MC1iYzQ0LTQ0/ZTQtODQxYy04N2Vi/ZTA1YWUwNjkuanBn</url>
      <title>The Ops Community ⚙️: Andrew Owen</title>
      <link>https://community.ops.io/aowendev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://community.ops.io/feed/aowendev"/>
    <language>en</language>
    <item>
      <title>Getting started with GraphQL</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Sun, 06 Apr 2025 12:51:58 +0000</pubDate>
      <link>https://community.ops.io/aowendev/getting-started-with-graphql-41hk</link>
      <guid>https://community.ops.io/aowendev/getting-started-with-graphql-41hk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GraphQL is an API query and manipulation language. Created by Facebook in 2012, it was open-sourced in 2015. In 2018 it moved to the GraphQL Foundation and introduced a schema definition language (SDL). It seems to be replacing REST as the standard way to expose public APIs. With REST, you have to define the inputs and outputs for each endpoint. Whereas with GraphQL, there's only one endpoint and you define a schema. The user sends only the required data to get what they want back. And unlike SQL, you're not limited to a single data source.&lt;/p&gt;

&lt;p&gt;This year, I've had to get rapidly up-to-speed with GraphQL. I thought I'd be starting from nothing, but I'd forgotten that TinaCMS (the headless content management system that I use with this site) uses it. One of the first problems I had to solve was how to generate static documentation. My limited research led me to two possible solutions: &lt;a href="https://github.com/anvilco/spectaql" rel="noopener noreferrer"&gt;SpectaQL&lt;/a&gt;, developed from the earlier &lt;a href="https://github.com/wayfair/dociql" rel="noopener noreferrer"&gt;DociQL&lt;/a&gt;, and &lt;a href="https://github.com/magidoc-org/magidoc" rel="noopener noreferrer"&gt;Magidoc&lt;/a&gt;. The latter has built-in search, so that made my choice for me. I use Hugo as my static site generator, so the first thing I had to do was start the local version of TinaCMS from my site's Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tinacms dev - c &lt;span class="s2"&gt;"hugo server -D -p 1313"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the local server running, you can access &lt;a href="https://github.com/graphql/graphiql" rel="noopener noreferrer"&gt;GraphiQL&lt;/a&gt; at &lt;a href="http://localhost:1313/admin/#/graphql" rel="noopener noreferrer"&gt;http://localhost:1313/admin/#/graphql&lt;/a&gt;. GraphiQL is a reference implementation of the GraphQL API playground. If it's too basic for you, there's a commercial alternative called &lt;a href="https://www.apollographql.com/" rel="noopener noreferrer"&gt;Apollo&lt;/a&gt;. The TinaCMS implementation gives you three options (selected from the icons on the left):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt;: API docs in a tree structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History&lt;/strong&gt;: Previous queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries&lt;/strong&gt;: Query builder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After you've taken a look at the Docs section in GraphQL, you'll understand why I wanted to generate a static site. With Magidoc it's easy. I added this configuration file to my Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;introspection:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;url:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'http://localhost:&lt;/span&gt;&lt;span class="mi"&gt;4001&lt;/span&gt;&lt;span class="err"&gt;/graphql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;website:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;template:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'carbon-multi-page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;customStyles:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'/static/css/custom.css'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then to generate the documentation, enter: &lt;code&gt;pnpm add --global @magidoc/cli\@latest &amp;amp;&amp;amp; magidoc generate&lt;/code&gt; (you'll need pnpm installed). By default, this creates a static site in a folder called docs. However, you can't just open the index.html file. You'll need to launch the server with &lt;code&gt;magidoc preview&lt;/code&gt; and then follow the link. You may want to add the docs folder to your &lt;code&gt;.gitignore&lt;/code&gt; file. But in production, you can deploy using any &lt;a href="https://magidoc.js.org/deployment/others" rel="noopener noreferrer"&gt;web server&lt;/a&gt;. The output is based on IBM's &lt;a href="https://carbondesignsystem.com/" rel="noopener noreferrer"&gt;Carbon Design System&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now you've got some static searchable documentation, you can start exploring your schema. GraphQL schema can be written in any programming language that implements the type system. With TinaCMS that means either TypeScript or JavaScript. Here's a snippet of my Tina &lt;code&gt;config.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;schema:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;collections:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;format:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Blog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;path:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"content/blog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;defaultItem:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="err"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;draft:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;fields:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;required:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;isTitle:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;required:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"datetime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image_license"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Image License"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'tags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'Tags'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;list:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'body'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;type:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'rich-text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;isBody:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Body"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;templates:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'shortcode'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="err"&gt;label:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'shortcode'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a schema type called &lt;code&gt;Blog&lt;/code&gt; with the fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;draft&lt;/strong&gt;: &lt;code&gt;Boolean!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;title&lt;/strong&gt;: &lt;code&gt;String!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;date&lt;/strong&gt;: &lt;code&gt;String&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;description&lt;/strong&gt;: &lt;code&gt;String&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;image&lt;/strong&gt;: &lt;code&gt;String&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;image_license&lt;/strong&gt;: &lt;code&gt;String&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tags&lt;/strong&gt;: &lt;code&gt;[String]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;body&lt;/strong&gt;: &lt;code&gt;JSON&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;id&lt;/strong&gt;: &lt;code&gt;ID!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;_sys&lt;/strong&gt;: &lt;code&gt;SystemInfo!&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;_values&lt;/strong&gt;: &lt;code&gt;JSON!&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fields can be scalar (&lt;code&gt;Boolean&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;, &lt;code&gt;Integer&lt;/code&gt;, &lt;code&gt;String&lt;/code&gt; and so on) or complex (containing other data). A trailing exclamation mark ( &lt;strong&gt;!&lt;/strong&gt; ) means the field is required (non-nullable). Square brackets mean an array. If the type is &lt;code&gt;JSON&lt;/code&gt;, then a set of sub-fields is defined. You can also use previously defined types to create relationships. For example, if your blog has multiple contributors you could have a field called &lt;code&gt;author&lt;/code&gt; with the type &lt;code&gt;User!&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Queries
&lt;/h4&gt;

&lt;p&gt;Because there's only one endpoint, you have to tell it what you want. For example, to get the list of collections you'd use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;collections&lt;/code&gt; filed is the &lt;em&gt;root field&lt;/em&gt;. Everything else is the &lt;em&gt;payload&lt;/em&gt;. Because the payload only contains &lt;code&gt;name&lt;/code&gt; the query returns a list of all the &lt;code&gt;collections&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"collections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blog"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recipe"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get exactly the amount of information you ask for in the payload. You can also pass arguments. For example, if you wanted to get the &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt; and &lt;code&gt;draft&lt;/code&gt; status of this article you could use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"getting-started-with-graphql.md"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, typically you would use a &lt;a href="http://graphql.org/learn/queries/#variables" rel="noopener noreferrer"&gt;variable&lt;/a&gt; in the query, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$relativePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$relativePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Variables have a prefix ( &lt;strong&gt;$&lt;/strong&gt; ). The type (&lt;code&gt;String&lt;/code&gt; in this example) is defined in the query. The variable is passed in the &lt;code&gt;blog&lt;/code&gt; parameters.&lt;/p&gt;

&lt;h4&gt;
  
  
  Mutations
&lt;/h4&gt;

&lt;p&gt;Query is the equivalent of &lt;code&gt;GET&lt;/code&gt; in REST or &lt;code&gt;read&lt;/code&gt; in traditional data terminology. For &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; there's &lt;code&gt;mutation&lt;/code&gt;. The syntax is the same as for queries. You might have noticed the &lt;code&gt;ID&lt;/code&gt; type earlier. If you specify it when creating a record, the GraphQL server will give it a unique identifier. You can try out this example from the TinaCMS &lt;a href="https://tina.io/docs/graphql/queries/update-document/" rel="noopener noreferrer"&gt;documenation&lt;/a&gt; (sticking with the &lt;a href="https://www.imdb.com/title/tt0374900/" rel="noopener noreferrer"&gt;Napoleon Dynamite&lt;/a&gt; references):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;updatePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"voteForPedro.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vote For Napolean Instead"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"politics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"content/authors/napolean.json"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Author&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Subscriptions
&lt;/h4&gt;

&lt;p&gt;I've written previously about event-driven architectures. Subscriptions are a way to get data from the GraphQL server every time an subscribed event takes place. Not all GraphQL servers are configured to support websocket subscriptions, and that's true of my TinaCMS instance. But if it was, this is what a request to be notified when a new blog article is created would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;newBlog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Custom scalars
&lt;/h4&gt;

&lt;p&gt;Five months after I originally wrote this article, I ran into an issue with Magidoc. The doc build was failing with missing definitions for custom scalars. At first, I thought this was a bug, but it turns out that the bug was that the previous version would build the docs even if the definitions were missing. You don't need to provide definitions for the predefined scalars (&lt;code&gt;String&lt;/code&gt;, &lt;code&gt;Int&lt;/code&gt;, &lt;code&gt;Float&lt;/code&gt;, &lt;code&gt;Boolean&lt;/code&gt; and &lt;code&gt;ID&lt;/code&gt;). But if you have any custom scalars in your schema you must provide an example for each in JSON format in the Magidoc config file. These definitions go under &lt;code&gt;options&lt;/code&gt; in the &lt;code&gt;website&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;website:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;template:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'carbon-multi-page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;customStyles:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'/static/css/custom.css'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;options:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;queryGenerationFactories:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;accountID:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01234567-890a-bcde-f012-34567890abcd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;It's beyond the scope of this article to do more than cover the basics. For more information, the GraphQL &lt;a href="https://graphql.org/learn/" rel="noopener noreferrer"&gt;documenation&lt;/a&gt; is a good starting point. TinaCMS is a good application to start experimenting with its &lt;a href="https://tina.io/docs/graphql/overview/" rel="noopener noreferrer"&gt;GraphQL API&lt;/a&gt;. For the adventurous, Kingdom Orjiewuru wrote the first part of an &lt;a href="https://medium.com/software-insight/building-a-simple-document-manager-with-graphql-part-1-ccdb1707f8f" rel="noopener noreferrer"&gt;article&lt;/a&gt; on building a simple document manager with GraphQL.&lt;/p&gt;

</description>
      <category>docs</category>
      <category>tinacms</category>
      <category>graphql</category>
      <category>magidoc</category>
    </item>
    <item>
      <title>Getting started with Bitbucket Pipelines</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Sun, 06 Apr 2025 12:41:31 +0000</pubDate>
      <link>https://community.ops.io/aowendev/getting-started-with-bitbucket-pipelines-5f7d</link>
      <guid>https://community.ops.io/aowendev/getting-started-with-bitbucket-pipelines-5f7d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm a big fan of GitHub Actions. But if you're working for an enterprise software company, there's a fair chance you're using Atlassian's Bitbucket Cloud (along with Confluence and Jira). If so, then you can use Pipelines to build continuous integration and deployment workflows. If you're new to DevOps and CI/CD, I have a &lt;a href="https://community.ops.io/aowendev/using-github-actions-and-hosted-runners-2h56"&gt;TL:DR&lt;/a&gt; for you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Bitbucket, go to your repository and select &lt;strong&gt;Pipelines&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create your first pipeline&lt;/strong&gt; to scroll down to the template section.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Starter&lt;/strong&gt; pipeline. This will create a file called &lt;code&gt;bitbucket-pipelines.yml&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Previously, I wrote a GitHub Action to unpack a zip archive. So let's recreate that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;atlassian/default-image:3&lt;/span&gt;

&lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;extract&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;zip&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file'&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;changesets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;includePaths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.zip"&lt;/span&gt;
        &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rm -r uploads/extracted&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;filename=$(basename -s .zip *.zip)&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;unzip *.zip&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rm *.zip&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv $filename temp&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mv temp/out/* .&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rm -r temp&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git add .&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git commit -m "unzip"&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;git push origin main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're done, click &lt;strong&gt;Commit File&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Bitbucket Pipelines runs your builds in Docker containers. So first you need to choose an image. The default is atlassian/default-image:latest. Atlassian recommends using this until you get your pipeline working and then finding a specific image. So in this instance, we're using Ubuntu 20.04 LTS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;default&lt;/strong&gt; tells the script to run when a commit is pushed to any branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;parallel&lt;/strong&gt; enables you to run steps simultaneously, but it's not necessary here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;step&lt;/strong&gt; is an individual task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;name&lt;/strong&gt; is the display name of the step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;condition&lt;/strong&gt; limits the circumstances under which the script runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;changesets&lt;/strong&gt; specifies changes to use as a condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;includePath&lt;/strong&gt; sets the file path that will match the condition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;script&lt;/strong&gt; is the Linux CLI input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script is virtually identical to the GitHub Action I wrote, so for a more detailed explanation, you can read &lt;a href="//../using-github-actions-to-automatically-unpack-a-zip-archive/"&gt;that&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Image: original by &lt;a href="https://unsplash.com/photos/4CNNH2KEjhc" rel="noopener noreferrer"&gt;Sigmund&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>automation</category>
      <category>cicd</category>
      <category>bitbucket</category>
    </item>
    <item>
      <title>Adding languages to a Hugo site</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Thu, 16 Mar 2023 08:44:48 +0000</pubDate>
      <link>https://community.ops.io/aowendev/adding-languages-to-a-hugo-site-17n6</link>
      <guid>https://community.ops.io/aowendev/adding-languages-to-a-hugo-site-17n6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm a long-term advocate for localization, but this site has been monolingual for over a year now. It's past time I started following my own advice. So last weekend I finally got around to localizing the site for French.&lt;/p&gt;

&lt;p&gt;As with most localization tasks, I'm retrofitting it to an existing project. Fortunately, I chose &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt; as my static site generator, and it has built-in internationalization support using &lt;a href="https://github.com/nicksnyder/go-i18n" rel="noopener noreferrer"&gt;go-i18n&lt;/a&gt;. On the downside, I chose a theme that doesn't fully support localization, which meant there was some work involved. But not as much as I'd expected. So let me take you through the steps involved.&lt;/p&gt;

&lt;p&gt;First, I updated my &lt;code&gt;config.toml&lt;/code&gt; file to tell Hugo that my site is now multilingual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;defaultContentLanguageInSubdir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;DefaultContentLanguage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"en-us"&lt;/span&gt;
&lt;span class="nn"&gt;[languages]&lt;/span&gt;
  &lt;span class="nn"&gt;[languages.en-us]&lt;/span&gt;
    &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Andrew Owen | Writer | Designer"&lt;/span&gt;
    &lt;span class="py"&gt;languageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"English"&lt;/span&gt;
    &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nn"&gt;[languages.fr]&lt;/span&gt;
    &lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Andrew Owen | Écrivain | Concepteur"&lt;/span&gt;
    &lt;span class="py"&gt;languageName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Français"&lt;/span&gt;
    &lt;span class="py"&gt;weight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;defaultContentLanguageInSubdir&lt;/code&gt; leaves the language out of the URL for the default language, in my case US English. Any settings you leave out from the language definitions will fall back to the default.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;404.md&lt;/code&gt; page is already stored in a folder called &lt;code&gt;en&lt;/code&gt;. So adding a French version was just a case of creating a new folder called &lt;code&gt;fr&lt;/code&gt;, copying the file across and changing the text.&lt;/p&gt;

&lt;p&gt;The next step was to set up some a set of translatable phrases for the generated content. This is as simple as creating an &lt;code&gt;i18n&lt;/code&gt; folder in the root of the Hugo repository and adding a YAML or TOML file for each language. In my case &lt;code&gt;en-us.yaml&lt;/code&gt; and &lt;code&gt;fr.yaml&lt;/code&gt;. These phrases are included using the shortcode &lt;code&gt;{{T "phrase" | formatting}}&lt;/code&gt;. If you want to include HTML such as &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; tags, then use &lt;code&gt;safeHTML&lt;/code&gt; for the &lt;code&gt;formatting&lt;/code&gt; value. Each value should have an ID and a definition in each language file. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;January&lt;/span&gt;
  &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;janvier"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In several places in the site, titles were derived from data contained in YAML files. In each of these cases, I replaced the data-derived version with a shortcode to the translated version instead. I also had to modify some of the links to go to the correct place when viewing the site in French. In most cases, that just meant using the &lt;code&gt;.Site.Language.Lang&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;Because I'm only supporting two languages, I wanted to have a simple toggle. The nav bar was starting to get rather full, so I removed the &lt;code&gt;Home&lt;/code&gt; link, because the logo already takes you there. I had to change the RSS link to make it work in French, so I also replaced it with the icon. I figure anyone who still cares about RSS should recognize it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ if eq .Site.Language.Lang "en-us" }}
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/blog/index.xml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;i&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;'fa fa-rss'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  {{ else }}
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/fr/blog/index.xml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;i&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;'fa fa-rss'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  {{ end }}
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last thing to do was to translate the individual articles. Hugo gives you two ways of doing this. You can set up a folder for each language, and if you have three or more languages, you should do this. But by default, Hugo will treat anything ending in &lt;code&gt;.md&lt;/code&gt; as the default language, and you can add other languages by adding the language code to the extension, for example: &lt;code&gt;.fr.md&lt;/code&gt;. If you're using folders, then you add the folder details to the language definitions in your TOML file. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[languages]&lt;/span&gt;
  &lt;span class="nn"&gt;[languages.en]&lt;/span&gt;
    &lt;span class="py"&gt;contentDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"content/english"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While I was editing the site in VScode, I also took the opportunity to reorganize the images. They were getting a bit tricky to navigate with TinaCMS, so I moved the blog images into folders by year. I also did some cleanup on the top nav bar. The last thing to do was change the article date string from &lt;code&gt;{{ .PublishDate.Format “2 January 2006” }}&lt;/code&gt; to &lt;code&gt;{{.Date.Day}} {{i18n .Date.Month}} {{.Date.Year}}&lt;/code&gt; after setting up a set of translatable date strings in the YAML files in the &lt;code&gt;i18n&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Of course, just when I thought I'd caught everything, I noticed that tag links were broken. I fixed this by making the URL relative so that it would pick up the current language. I'm pleased to say that I didn't have to do anything at all to localize search.&lt;/p&gt;

&lt;p&gt;With the technical piece done, all that's left to do is the translation. There are ways to automate the process, but there's still no substitute for a human reviewer. I'm using a combination of &lt;a href="https://www.deepl.com/translator" rel="noopener noreferrer"&gt;DeepL&lt;/a&gt;, &lt;a href="https://languagetool.org/" rel="noopener noreferrer"&gt;LanguageTool&lt;/a&gt; and my own modest language ability. To begin with, I'm only translating the core content. But eventually I intend to have a fully translated site. If you are a native French user, and you spot any egregious mistakes, please drop me an email.&lt;/p&gt;

</description>
      <category>hugo</category>
      <category>localization</category>
      <category>l10n</category>
      <category>i18n</category>
    </item>
    <item>
      <title>Setting up a website with GitHub, Hugo, Netlify and TinaCMS</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Thu, 02 Mar 2023 09:23:42 +0000</pubDate>
      <link>https://community.ops.io/aowendev/setting-up-a-website-with-github-hugo-netlify-and-tinacms-2hk9</link>
      <guid>https://community.ops.io/aowendev/setting-up-a-website-with-github-hugo-netlify-and-tinacms-2hk9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My first blog post of 2022 was on setting up a free personal website with &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;, &lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt; and Forestry. But Forestry is due to be discontinued at the end of this month. So this is a rework of that post using Forestry's successor &lt;a href="https://tina.io/" rel="noopener noreferrer"&gt;TinaCMS&lt;/a&gt;. If you need to migrate, a tool and guide are available. Or you can do it the hard way and follow my &lt;a href="https://www.andrewowen.net/blog/migrating-a-hugo-site-from-forestry-to-tina" rel="noopener noreferrer"&gt;post&lt;/a&gt; from earlier this year.&lt;/p&gt;

&lt;p&gt;I spent most of last year as a developer advocate, and I felt that my personal website should be a more modern solution than WordPress. At a previous job, I'd built a developer portal with Hugo and spoken to people at Netlify and Forestry. I also already had a GitHub account. What made that a particularly attractive solution was that, aside from the domain name fees, it doesn't cost anything. Hugo is open source. And GitHub, Netlify and TinaCMS all have a free tier for personal websites.&lt;/p&gt;

&lt;p&gt;Next, I needed to find a portfolio style Hugo theme that would integrate with all these services. After some searching, I settled on using &lt;a href="https://github.com/themefisher/kross-hugo/" rel="noopener noreferrer"&gt;Kross&lt;/a&gt;, a free MIT-licensed theme from &lt;a href="https://themefisher.com/" rel="noopener noreferrer"&gt;Themefisher&lt;/a&gt;. It's built on &lt;a href="https://getbootstrap.com/" rel="noopener noreferrer"&gt;Bootstrap&lt;/a&gt;, HTML5 and CSS3. I recommend finding a theme hosted on GitHub with the option to deploy to Netlify. You can fork the repository (create a copy in your own repository) and deploy it automatically. Then you just need to add TinaCMS support.&lt;/p&gt;

&lt;p&gt;TinaCMS has come a long way since it came out of beta in November 2022. As more users adopt it, it's become more mature. And that's also reflected in the documentation. I was expecting to have to write a guide to setting up Hugo on Tina, but it's already been &lt;a href="https://tina.io/docs/frameworks/hugo/" rel="noopener noreferrer"&gt;done&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Modify your theme
&lt;/h4&gt;

&lt;p&gt;You'll have the site up and running very quickly, but you'll want to change the placeholder text and images. I found the easiest way to replace the images was directly through the GitHub web interface. When replacing the illustration SVGs, I had to manually edit the files in VS Code to set the correct display size. For the text, save yourself the pain of YAML induced build errors and do it all in Tina Cloud.&lt;/p&gt;

&lt;p&gt;The contact form seems to have a dependency on a non-free service, so I've switched it off for now and provided a mailto link instead. There also seems to be a bug in the Portfolio section where, with tags enabled, some content wasn't rendering, so for now I've removed the tags.&lt;/p&gt;

&lt;p&gt;I wasn't completely happy with the typography, but originally the entire Kross theme is inherited as a submodule, and I was only overriding the illustrations. That way, if the theme got updated, my site would automatically get updated too. However, there were changes I needed to make and after about a week I replaced the submodule with a local copy of the theme.&lt;/p&gt;

&lt;p&gt;With most of the static content now in place, you can concentrate on writing your blog in Tina Cloud. If you need to make developer changes to the site, you can do it locally in TinaCMS, which for a personal site means you can do without staging and deploy to your main repository in GitHub.&lt;/p&gt;

&lt;p&gt;I had an issue with the SSL/TLS certificate. I resolved it by making andrewowen.net the primary domain and *.andrewowen.net the redirect domain in the Netlify settings.&lt;/p&gt;

&lt;p&gt;One tool I find invaluable when working in web applications like Tina Cloud is &lt;a href="https://languagetool.org/" rel="noopener noreferrer"&gt;LanguageTool&lt;/a&gt;. It's a multilingual grammar, style and spell checker. The one caveat is that it doesn't seem to check text on TinaCMS within blockquotes. So if you use it, make sure you do the checking before applying the blockquotes style.&lt;/p&gt;

&lt;h4&gt;
  
  
  Check your styles
&lt;/h4&gt;

&lt;p&gt;The other thing I learned post-launch is that I should have checked to see how the theme renders all the styles. It was doing some odd things with emphasis and code snippets, so that required some changes to the theme. Fortunately, I only had to edit the &lt;code&gt;style.css&lt;/code&gt; file in the theme.&lt;/p&gt;

&lt;p&gt;If you're using the Kross theme, I would suggest making the following changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;ol&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="nl"&gt;list-style-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;list-style-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;disc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.content&lt;/span&gt; &lt;span class="nt"&gt;strong&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4c4c4c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Roboto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sans&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, numbers and bullets are missing from lists and the &lt;strong&gt;bold&lt;/strong&gt; (strong) style uses the heading font. I also decided to replace all the fonts with &lt;a href="https://www.ibm.com/plex/" rel="noopener noreferrer"&gt;IBM Plex,&lt;/a&gt; a free alternative to Helvetica. After some user feedback, I also set the base font size to 17 pixels.&lt;/p&gt;

&lt;p&gt;I made a change to &lt;code&gt;layouts/index.html&lt;/code&gt; to increase the size of the social media icons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- social icon --&amp;gt;
&amp;lt;ul class="list-unstyled ml-5 mt-3 position-relative zindex-1"&amp;gt;
{{ range .Site.Params.social }}
&amp;lt;li style="font-size:32px" class="mb-3"&amp;gt;&amp;lt;a class="text-white" href="{{.URL | safeURL }}"&amp;gt;&amp;lt;i class="{{.icon}}"&amp;gt;&amp;lt;/i&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
{{ end }}
&amp;lt;/ul&amp;gt;
&amp;lt;!-- /social icon --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also removed the email / phone number / address content from the footer. I would expect most people to contact me using one of the social media links on the front page. I've made the icons bigger and added &lt;a href="https://fontawesome.com/" rel="noopener noreferrer"&gt;Font Awesome&lt;/a&gt; support. Syntax highlighting is supported automatically in Hugo with &lt;a href="https://gohugo.io/content-management/syntax-highlighting/" rel="noopener noreferrer"&gt;Chroma&lt;/a&gt;, but you have to enable it. You no-longer need to use a shortcode, and TinaCMS enables you to select the tag when you enter the Markdown to start a code block.&lt;/p&gt;

&lt;p&gt;After I got the site up and running, I made some tweaks to the theme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing the heading font sizes.&lt;/li&gt;
&lt;li&gt;Removing H3 headings from the blog entries.&lt;/li&gt;
&lt;li&gt;Reducing the top background on the home page.&lt;/li&gt;
&lt;li&gt;Removing the &lt;code&gt;All&lt;/code&gt; button from the portfolio (tagging didn't work out of the box).&lt;/li&gt;
&lt;li&gt;Removing the address footer.&lt;/li&gt;
&lt;li&gt;Making all the hard-coded paths relative.&lt;/li&gt;
&lt;li&gt;Adding a fav icon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  RSS support
&lt;/h4&gt;

&lt;p&gt;Hugo has built-in RSS support. Just add &lt;code&gt;/index.xml&lt;/code&gt; at whatever level you want the feed to be generated from. For example: &lt;a href="//../index.xml"&gt;https://andrewowen.net/blog/index.xml&lt;/a&gt;. However, it won't work if you have &lt;code&gt;baseurl="/"&lt;/code&gt; in your &lt;code&gt;config.toml&lt;/code&gt; file. You need to set it to your actual base URL.&lt;/p&gt;

&lt;h4&gt;
  
  
  Site-wide search
&lt;/h4&gt;

&lt;p&gt;There are lots of options for adding search, but I think Victoria Drake's solution using &lt;a href="https://lunrjs.com/" rel="noopener noreferrer"&gt;Lunr&lt;/a&gt; is the best for a personal site: &lt;a href="https://victoria.dev/blog/add-search-to-hugo-static-sites-with-lunr/" rel="noopener noreferrer"&gt;https://victoria.dev/blog/add-search-to-hugo-static-sites-with-lunr/&lt;/a&gt;. The only extra thing I had to do beyond the instructions was add the search form to the nav bar and style the results page to match the rest of the site.&lt;/p&gt;

&lt;h4&gt;
  
  
  Analytics
&lt;/h4&gt;

&lt;p&gt;It's good to know who your audience is and which blog subjects attract the most interest. I recommend adding &lt;a href="https://gohugo.io/templates/internal/#google-analytics" rel="noopener noreferrer"&gt;Google Analytics&lt;/a&gt;. After you've signed up for an account, all you need to do is provide your tracking ID in your config file and add a short code to the &lt;code&gt;head.html&lt;/code&gt; partial. Or you can add it as a &lt;a href="https://medium.com/@sin9270/how-to-add-google-analytics-to-your-website-on-netlify-a6fbbd92ae79" rel="noopener noreferrer"&gt;snippet&lt;/a&gt; to Netlify (I've found this to be the more reliable approach).&lt;/p&gt;

&lt;h4&gt;
  
  
  Tags
&lt;/h4&gt;

&lt;p&gt;Hugo supports taxonomies out of the box. Without doing anything, I can view a &lt;code&gt;tags.html&lt;/code&gt; page. But I hadn't enabled tags on posts. You can add them as front matter in your TinaCMS collection (as a list). Adding tags to posts populates the tags page, but it doesn't display the tags on the posts themselves. To do that, you need to add this code to your default single.html page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tags-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      {{- with .Params.tags -}}
        {{- if ge (len .) 1 -}}
          {{- range . -}}
            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ $.Site.BaseURL }}tags/{{ . | urlize }}/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;#{{ . }}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
          {{ end -}}
        {{- end -}}
      {{- end -}}
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Custom 404 page
&lt;/h4&gt;

&lt;p&gt;The last thing I added was a &lt;a href="https://answers.netlify.com/t/custom-404-error-page-with-hugo/47250/7" rel="noopener noreferrer"&gt;custom 404 page&lt;/a&gt;. After that, you're all set.&lt;/p&gt;

&lt;p&gt;Image: Original by &lt;a href="https://unsplash.com/photos/142_ztTZH_o" rel="noopener noreferrer"&gt;Toa Heftiba&lt;/a&gt;. I used one of Toa's images that came with the Kross theme on the original post, so I wanted to highlight her work again. The previous image wasn't exactly a forest. This isn't exactly an alpaca.&lt;/p&gt;

</description>
      <category>githubn</category>
      <category>hugo</category>
      <category>netlify</category>
      <category>tinacms</category>
    </item>
    <item>
      <title>Migrating a Hugo site from Forestry to Tina</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Thu, 05 Jan 2023 08:26:21 +0000</pubDate>
      <link>https://community.ops.io/aowendev/migrating-a-hugo-site-from-forestry-to-tina-c99</link>
      <guid>https://community.ops.io/aowendev/migrating-a-hugo-site-from-forestry-to-tina-c99</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I launched the current version of my website a year ago. Having become a developer advocate in 2021, I didn't think a WordPress site that hadn't been updated in a decade would cut it any more. I wanted to do something a bit more modern. At my previous company, I'd built a developer portal on &lt;a href="https://gohugo.io/" rel="noopener noreferrer"&gt;Hugo&lt;/a&gt;. The company ended up hosting the site itself, but I'd had discussions with &lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt; and &lt;a href="https://forestry.io/" rel="noopener noreferrer"&gt;Forestry&lt;/a&gt; at the time. And I'd been using &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for my big open source projects for a long time. I picked a free Hugo starter theme from &lt;a href="https://themefisher.com/" rel="noopener noreferrer"&gt;Themefisher&lt;/a&gt; that had built-in support for Netlify and Forestry. I spent a weekend on it: setting up the site structure, customizing the theme and adding content. I didn't have all the features at first (search, tags and RSS came later), but it was a huge step up from my old site.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://jamstack.org/" rel="noopener noreferrer"&gt;Jamstack&lt;/a&gt;, Vercel's &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; with its &lt;a href="https://reactjs.org/" rel="noopener noreferrer"&gt;React&lt;/a&gt; templates has overtaken Hugo in popularity. And I'm not surprised. By most measures, JavaScript has never been out of the top 10 programming languages over the last two decades. And of 347 static site generators listed, 130 are written in JavaScript, 51 are written in Python and 26 are written in PHP. Hugo, and 16 others, are written in &lt;a href="https://go.dev/" rel="noopener noreferrer"&gt;Go&lt;/a&gt;. But Hugo claims to be the fastest (you can check out those claims with &lt;a href="https://pagespeed.web.dev/" rel="noopener noreferrer"&gt;PageSpeed&lt;/a&gt;) so I'm sticking with it. However, since 2019 the team behind Forestry have been developing the next iteration called &lt;a href="https://tina.io/" rel="noopener noreferrer"&gt;TinaCMS&lt;/a&gt;. And on November 8, 2022 it came out of beta. Forestry is scheduled to be discontinued in late March 2023. Existing users will be offered a migration path to &lt;a href="https://tina.io/" rel="noopener noreferrer"&gt;TinaCMS&lt;/a&gt;, a next generation headless CMS from the creators of Forestry. There are plans to share the migration tool in mid-January, but I decided to go ahead and do a manual migration.&lt;/p&gt;

&lt;p&gt;I decided to take the opportunity to do some clean up and add a Hugo shortcode for audio (thanks to John Arrroyo for information on &lt;a href="https://www.johnarroyo.com/2021/02/adding-audio-to-hugo/" rel="noopener noreferrer"&gt;how to do that&lt;/a&gt;). I also finally got around to adding a &lt;a href="https://answers.netlify.com/t/custom-404-error-page-with-hugo/47250/7" rel="noopener noreferrer"&gt;custom 404 page&lt;/a&gt;. I created a new empty GitHub repository and connected it to a new staging site on Netlify. I checked out a local copy and brought in the content from the old version of the site. You can run TinaCMS locally, so after installation I was able to make changes before pushing to staging. I removed the Forestry config (although it wouldn't have done any harm to leave it in place). But I'll assume you want to simply add TinaCMS support to your existing repo.&lt;/p&gt;

&lt;h4&gt;
  
  
  Create the project
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://app.tina.io/register" rel="noopener noreferrer"&gt;Register&lt;/a&gt; for a Tina account and log in.&lt;/li&gt;
&lt;li&gt;From the Dashboard, navigate to Projects and click New Project.&lt;/li&gt;
&lt;li&gt;Click Import Your Site, then click Authenticate with GitHub. If you're not already logged in to GitHub, do it now.&lt;/li&gt;
&lt;li&gt;Select the repository for your Hugo site and enter the site URLs (your live site's URL and localhost with the port you want to use when you're doing local editing).&lt;/li&gt;
&lt;li&gt;Click Create Project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Set up your site schema
&lt;/h4&gt;

&lt;p&gt;You'll need &lt;strong&gt;npm&lt;/strong&gt;. If it's not already installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On macOS with &lt;a href="//../blog/managing-packages-on-macos-with-homebrew/"&gt;Homebrew&lt;/a&gt;: brew install npm.&lt;/li&gt;
&lt;li&gt;On Ubuntu: sudo apt install npm.&lt;/li&gt;
&lt;li&gt;On Windows with &lt;a href="//../blog/managing-packages-on-windows-with-scoop/"&gt;Scoop&lt;/a&gt;: scoop install npm.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also need &lt;strong&gt;hugo&lt;/strong&gt;. You can install it the same way you installed &lt;strong&gt;npm&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check out a local copy of your repo from GitHub.&lt;/li&gt;
&lt;li&gt;In the root of the repo folder: &lt;code&gt;npx @tinacms/cli@latest init&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When prompted to choose your package manager, select NPM.&lt;/li&gt;
&lt;li&gt;Choose if you want to use Typescript.&lt;/li&gt;
&lt;li&gt;When prompted for the public assets' storage folder name, enter static.&lt;/li&gt;
&lt;li&gt;Start TinaCMS: &lt;code&gt;npx tinacms dev -c "hugo server -D -p 3003"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to the admin page: &lt;a href="http://localhost:3003/admin" rel="noopener noreferrer"&gt;http://localhost:3003/admin&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because I used VScode, I created a &lt;code&gt;tasks.json&lt;/code&gt; file in the &lt;code&gt;.vscode&lt;/code&gt; folder to automate deploying TinaCMS locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start TinaCMS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shell"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx tinacms dev -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hugo server -D&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"isDefault"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, TinaCMS expects to find images in a &lt;code&gt;media&lt;/code&gt; folder. Edit your TinaCMS config file to point to &lt;code&gt;/static/images/&lt;/code&gt; or wherever you keep your images. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;tina&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;mediaRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;publicFolder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
      &lt;span class="pi"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Model your content
&lt;/h4&gt;

&lt;p&gt;Before you can edit your content, you need to model it, based on the metadata you're using in the headers of your markdown files. In my case I'm using &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;draft&lt;/code&gt; status, &lt;code&gt;image&lt;/code&gt;, a &lt;code&gt;tags&lt;/code&gt; list and a &lt;code&gt;title&lt;/code&gt;. Besides the metadata, you also need to define the &lt;code&gt;body&lt;/code&gt; text. My schema looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;collections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
        &lt;span class="pi"&gt;{&lt;/span&gt;
          &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blog"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;md"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blog"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content/blog"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
          &lt;span class="nv"&gt;defaultItem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;() =&amp;gt;&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;return&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;},&lt;/span&gt;
          &lt;span class="nv"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boolean"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have a single collection called &lt;code&gt;Blog&lt;/code&gt; that corresponds to the folder where my posts go (&lt;code&gt;content/blog&lt;/code&gt;). The &lt;code&gt;draft&lt;/code&gt; field is a Boolean that determines if the post is displayed. You can set a default value for new posts in the &lt;code&gt;defaultItem&lt;/code&gt; list so that new posts are all created as drafts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;isTitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datetime"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Date"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Image"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;image&lt;/code&gt; are all fairly self-explanatory. You can set the &lt;code&gt;required&lt;/code&gt; value to &lt;code&gt;true&lt;/code&gt; to prevent saving a post that is missing a required field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tags'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;string'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Tags'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rich-text'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;isBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;templates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
                &lt;span class="pi"&gt;{&lt;/span&gt;
                  &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shortcode'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                  &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;shortcode'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                  &lt;span class="nv"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                    &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                  &lt;span class="pi"&gt;},&lt;/span&gt;
                  &lt;span class="nv"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;
                    &lt;span class="pi"&gt;{&lt;/span&gt;
                      &lt;span class="nv"&gt;// Be sure to call this field `text`&lt;/span&gt;
                      &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                      &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Text'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                      &lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;string'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                      &lt;span class="nv"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                      &lt;span class="nv"&gt;isTitle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                      &lt;span class="nv"&gt;ui&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
                        &lt;span class="nv"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;textarea'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
                      &lt;span class="pi"&gt;},},],},],}&lt;/span&gt;&lt;span class="err"&gt;,],},],},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tags&lt;/code&gt; are a set of text strings. For items like this, set the &lt;code&gt;list&lt;/code&gt; value to &lt;code&gt;true&lt;/code&gt;. Setting the &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;rich-text&lt;/code&gt; enables the GUI editor for the body text. To be able to include Hugo shortcodes, you need to include the above template for them. In practice, I found the need to include the opening and closing angle brackets in the &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; items so that no space would be inserted between the curly brackets and the angle bracket (because a space kills the audio shortcode).&lt;/p&gt;

&lt;h4&gt;
  
  
  Fix your Markdown
&lt;/h4&gt;

&lt;p&gt;This part is a headache, but until there's a Forestry to Tina migration tool, there's no getting around it. The big problem I encountered was that all my Markdown front matter was in TOML and, at the time of writing, TinaCMS only supports YAML. I used search and replace in VScode. But there is a &lt;a href="https://discourse.gohugo.io/t/howto-convert-your-front-matter-from-toml-to-yaml/332" rel="noopener noreferrer"&gt;better way&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Enable Tina Cloud in TinaCMS
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Log into &lt;a href="https://tina.io" rel="noopener noreferrer"&gt;Tina Cloud&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Overview&lt;/strong&gt; and get a copy of your &lt;strong&gt;clientID&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Tokens&lt;/strong&gt; and click &lt;strong&gt;New Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give the token a name. For example: &lt;code&gt;Production Content Token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter the Git branches the token has access to. For example: &lt;code&gt;main&lt;/code&gt;. Then click &lt;strong&gt;Create Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Tokens&lt;/strong&gt; and get a copy of the &lt;code&gt;token&lt;/code&gt; you just created.&lt;/li&gt;
&lt;li&gt;Add your &lt;code&gt;clientID&lt;/code&gt; and &lt;code&gt;token&lt;/code&gt; to your config: 
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="s"&gt;export default defineConfig({&lt;/span&gt;
    &lt;span class="s"&gt;branch,&lt;/span&gt;
    &lt;span class="s"&gt;clientId&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;   &lt;span class="s"&gt;// Get this from tina.io&lt;/span&gt;
    &lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;      &lt;span class="s"&gt;// Get this from tina.io&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;netlify.toml&lt;/code&gt; file, add TinaCMS to your build command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[build]
publish = "public"
command = "yarn tinacms build &amp;amp;&amp;amp; hugo"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can set this directly in your build settings on Netlify, but whatever is in the file will override whatever is on Netlify.&lt;/p&gt;

&lt;p&gt;Now you can push your changes to GitHub. After Netlify deploys your build, you'll be able to work on your site in Tina Cloud.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sync your media
&lt;/h4&gt;

&lt;p&gt;Before you start editing. Go to Media Manager, click &lt;strong&gt;Sync&lt;/strong&gt; and then click &lt;strong&gt;Sync Media&lt;/strong&gt;. This copies media assets from the &lt;code&gt;images&lt;/code&gt; folder in your designated branch (typically main) in your git repository to Tina Cloud's asset service. Now you can use those assets in your site with Tina Cloud.&lt;/p&gt;

&lt;h4&gt;
  
  
  Afterthought
&lt;/h4&gt;

&lt;p&gt;I wrote this before TinaCMS published its Forestry migration tool and &lt;a href="https://community.ops.ioForestry%20Migration%20Guide"&gt;guide&lt;/a&gt;. I am indebted to Forestry CEO and co-founder Scott Gallant for sharing a draft of that guide with me when I got stuck. He was also super helpful on the &lt;a href="https://discord.com/invite/zumN63Ybpf" rel="noopener noreferrer"&gt;TinaCMS Discord&lt;/a&gt;. Fun fact: Tina is named after the llama in “&lt;a href="https://www.imdb.com/title/tt0374900/" rel="noopener noreferrer"&gt;Napoleon Dynamite&lt;/a&gt;”.&lt;/p&gt;

</description>
      <category>sitemigration</category>
      <category>hugo</category>
      <category>forestry</category>
      <category>tinacms</category>
    </item>
    <item>
      <title>Bulk updating documents with XSLT</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 19 Dec 2022 19:47:23 +0000</pubDate>
      <link>https://community.ops.io/aowendev/bulk-updating-documents-with-xslt-248i</link>
      <guid>https://community.ops.io/aowendev/bulk-updating-documents-with-xslt-248i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;XSLT (Extensible Stylesheet Language Transformations) is a language for transforming XML documents into other documents. I've mentioned it before in my post on creating release notes from Jira.&lt;/p&gt;

&lt;p&gt;Now that every document under the sun is either stored in XML or can easily be converted to XML, XSLT provides a great way to perform batch processing on those documents.&lt;/p&gt;

&lt;p&gt;To use XSLT, you need an XSLT processor. If you stick to version 1.0 of the specification, there are lots of choices. But if you want support for the latest version (3.0 at time of writing) your choices are RaptorXML (integrated with Altova's &lt;a href="https://www.altova.com/xmlspy-xml-editor" rel="noopener noreferrer"&gt;XMLSpy&lt;/a&gt; XML editor), or &lt;a href="https://www.saxonica.com/products/products.xml" rel="noopener noreferrer"&gt;Saxon&lt;/a&gt;. Because my preferred XML editor is XMLmind's XML Editor (&lt;a href="https://www.xmlmind.com/xmleditor/" rel="noopener noreferrer"&gt;XXE&lt;/a&gt;), I use Saxon. Saxon and XXE both have free editions for personal use and open source projects.&lt;/p&gt;

&lt;p&gt;As an aside, I've been using XXE for over a decade. If you have to write docs in an XML format like DocBook or DITA, it's the only editor I would recommend. It has a WYSIWYG interface, keyboard shortcuts for everything and you rarely have to deal with tags.&lt;/p&gt;

&lt;p&gt;When you use the processor to apply your XSLT declarations to a file, it creates a new output, leaving the original files unchanged. Although it's a special purpose language, XSLT is &lt;a href="https://en.wikipedia.org/wiki/Turing-complete" rel="noopener noreferrer"&gt;Turing-complete&lt;/a&gt;, so you can do any kind of computation you might need with it.&lt;/p&gt;

&lt;p&gt;Here's an example I created to convert &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;span class="BodyWord"&amp;gt;&lt;/code&gt; tags to &lt;code&gt;&amp;lt;span class="Emphasis"&amp;gt;&lt;/code&gt; tags in MadCap &lt;a href="https://www.madcapsoftware.com/products/flare/" rel="noopener noreferrer"&gt;Flare&lt;/a&gt;. To use it, save it as &lt;code&gt;emphasis.xsl&lt;/code&gt; in the path with your files and then from the command line, enter:\&lt;br&gt;
&lt;code&gt;saxon -it:main -xsl:emphasis.xsl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8" ?&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:transform&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsl=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/XSL/Transform"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:output&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"xml"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt; &lt;span class="na"&gt;encoding=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="na"&gt;indent=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:strip-space&lt;/span&gt; &lt;span class="na"&gt;elements=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"b"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"span"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"class"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Emphasis&lt;span class="nt"&gt;&amp;lt;/xsl:attribute&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"@*|node()"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:element&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"@class\[.='BodyWord'\]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:attribute&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"class"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Emphasis&lt;span class="nt"&gt;&amp;lt;/xsl:attribute&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"@*|node()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:copy&amp;gt;&amp;lt;xsl:apply-templates&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"@*|node()"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&amp;lt;/xsl:copy&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:for-each&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"collection('.?select=*.htm;recurse=yes')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:result-document&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"output/{tokenize(document-uri(.))}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:apply-templates&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:result-document&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:for-each&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/xsl:transform&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's a slightly more complex example that converts the contents of headings (&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;) to sentence case in MadCap Flare. To use it, save it as &lt;code&gt;sentence.xsl&lt;/code&gt; in the path with your files and then from the command line, enter:\&lt;br&gt;
&lt;code&gt;saxon -it:main -xsl:sentence.xsl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8" ?&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:transform&lt;/span&gt; &lt;span class="na"&gt;xmlns:xsl=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/XSL/Transform"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"2.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:output&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"xml"&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt; &lt;span class="na"&gt;encoding=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="na"&gt;indent=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:strip-space&lt;/span&gt; &lt;span class="na"&gt;elements=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"h1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:element&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"h1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"substring(upper-case(.),1,1)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"substring(lower-case(.),2)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:element&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"@*|node()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:copy&amp;gt;&amp;lt;xsl:apply-templates&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"@*|node()"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&amp;lt;/xsl:copy&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:for-each&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"collection('.?select=*.htm;recurse=yes')"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:result-document&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"output/{tokenize(document-uri(.))}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:apply-templates&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:result-document&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:for-each&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/xsl:transform&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can learn more about XSLT at &lt;a href="https://www.w3schools.com/xml/xsl_intro.asp" rel="noopener noreferrer"&gt;w3schools.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>xslt</category>
      <category>xml</category>
      <category>devops</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>Converting images with Image Magick</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 19 Dec 2022 19:46:29 +0000</pubDate>
      <link>https://community.ops.io/aowendev/converting-images-with-image-magick-566b</link>
      <guid>https://community.ops.io/aowendev/converting-images-with-image-magick-566b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Photoshop is old. Really old. Well, in computer terms anyway. As of writing, it's currently on version 24.0. It was originally developed for the Mac in 1987 by Thomas Knoll, a PhD student at the University of Michigan, and his brother John, who worked for Industrial Light &amp;amp; Magic. Photoshop is a raster graphic (pixel image) editor. And because of its age, it has features for rendering 24-bit images (16 million colors) down to limited palettes, (which were a common feature of computers in the 1980s), that are missing from, or hard to use in, other image manipulation programs.&lt;/p&gt;

&lt;p&gt;Another piece of software dating from 1987 is Image Magick. It was created by John Cristy at DuPont to convert 24-bit images to 8-bit images (256 colors). In 1990 Adobe became the publisher of Photoshop, and DuPont assigned the copyright in its tool to  ImageMagick Studios LLC, a non-profit organization “dedicated to making software imaging solutions freely available”. So, if you can't afford over $200 a year for a Creative Cloud subscription just to convert 24-bit images to low color images, Image Magick is the obvious solution.&lt;/p&gt;

&lt;p&gt;Some hardware that definitely doesn't date from 1987, but is designed as if it did, is the Chloe 280SE (the retro computer project that I will talk endlessly about if given the opportunity). The project is still in development, and at present its highest resolution graphics mode is incredibly complicated. It has a non-linear bitmap of 256×192 pixels. This is overlaid with a non-linear attribute map of 32×192 cells (each 8×1 pixels in size) that determine the foreground and background color. Each 8 pixel cell is rendered by paring a bitmap byte with an attribute byte. The attribute byte is divided into three parts. The highest two bits select one of four color look up tables (CLUTs). The middle three bits set one of 8 background colors, and the lowest three bits set one of 8 foreground colors. Background and foreground colors are defined independently. This gives 16 colors per CLUT, for a maximum of 64 colors on screen. However, it is not possible to combine colors from different CLUTs. The colors are chosen from a fixed palette of 256 colors based on half the 9-bit RGB palette (3-bits are used for red and green, but only two for blue). These are stored in GRB (MSB) order. A complete image consists of a 6K bitmap, 6K attribute map and 64 byte palette.&lt;/p&gt;

&lt;p&gt;I said it was complicated. Fortunately, Edward Cree wrote an image conversion tool for Linux (scrplus) and Klaus Jahn made a Windows version (Image2ULAplus). These can both be used on a Mac with a suitable virtualization solution. However, the results can vary widely. This is the pre-conversion process if you have access to any version of Photoshop.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scale the image you want to convert to 256×192 pixels.&lt;/li&gt;
&lt;li&gt;Pattern dither the image to a uniform 128 colors.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Typically, Floyd-Steinberg or diffusion dither will give better results than pattern dither when reducing color depth. However, pattern dither seems to be the best method to reduce the visibility of the attribute cells. The palette has 8 levels for red and green, but only 4 for blue. Reducing the range to a uniform 128 colors translates to giving each color 5 levels in the input image. I don't know why, but that seems to give the best conversion results.&lt;/p&gt;

&lt;p&gt;The conversion process after opening the input image in Image2ULAplus is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch off:

&lt;ul&gt;
&lt;li&gt;Reduce colors to 8-bit.&lt;/li&gt;
&lt;li&gt;Use dithering&lt;/li&gt;
&lt;li&gt;Create HAM256 screen&lt;/li&gt;
&lt;li&gt;Stretch to SCREEN$ size.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Switch on:

&lt;ul&gt;
&lt;li&gt;Calculate palette&lt;/li&gt;
&lt;li&gt;CLUTs: 0, 1, 2, 3&lt;/li&gt;
&lt;li&gt;Create Timex screen&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set maximum colors to 64.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Render&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Save the image in &lt;code&gt;.SCR&lt;/code&gt; format.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've got a licensed copy of Photoshop CS6 on a 2010 MacBook Air. But I don't really want to have to get that out of storage every time I need to convert an image. So here's how to use Image Magick instead. First, you have to install it (unless it's pre-installed in your Linux distro): On Ubuntu, you'd use &lt;code&gt;sudo apt install imagemagick&lt;/code&gt; and invoke it with the &lt;code&gt;convert&lt;/code&gt; command. On macOS, the easiest way to install it is using &lt;a href="//../blog/managing-packages-on-macos-with-homebrew/"&gt;Homebrew&lt;/a&gt; with &lt;code&gt;brew install imagemagick&lt;/code&gt;. On Windows, you can download an &lt;a href="https://imagemagick.org/script/download.php#windows" rel="noopener noreferrer"&gt;executable&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With experimentation, I've found that the best setting for conversion to the Chloe 280SE is: &lt;code&gt;magick input.jpg -resize 256x192 -ordered-dither o8x8,5,5,4 output.gif&lt;/code&gt;. This scales the image to 256×192 and then performs an ordered (pattern) dither on the image using an 8×8 pattern (which works well with 8×1 attributes). It applies a uniform palette with 5 levels of red and green, and 4 levels of blue (128 uniform colors in Photoshop gives approximately 5 levels for each color). The results vary, but for the test image that I've used in all my tutorials, Image Magick actually does a better job than Photoshop.&lt;/p&gt;

&lt;p&gt;Now that's just one use case. Back in my days as a tech writer, I mainly used it for making &lt;code&gt;.PNG&lt;/code&gt; images as small as possible for inclusion in online help files. And that's barely scratching the surface of what's possible with Image Magick. Fortunately, there's good &lt;a href="https://imagemagick.org/script/command-line-processing.php" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; and an active &lt;a href="https://github.com/ImageMagick/ImageMagick/discussions" rel="noopener noreferrer"&gt;community&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Image: Detail from cover of “Nubia Coronation Special” by David Mack. Copyright © &lt;a href="//dc.com"&gt;DC&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>imageconversion</category>
      <category>devops</category>
      <category>automation</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>Fostering security awareness</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 19 Dec 2022 19:44:25 +0000</pubDate>
      <link>https://community.ops.io/aowendev/fostering-security-awareness-goi</link>
      <guid>https://community.ops.io/aowendev/fostering-security-awareness-goi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today's post is based on a presentation I gave at a security conference in the 2010s. It's a bit longer than what I would normally share, but I think it's still relevant, possibly more so than when it was originally written.&lt;/p&gt;

&lt;h4&gt;
  
  
  Introduction
&lt;/h4&gt;

&lt;p&gt;This article is aimed at a non-technical audience. The aim is to raise awareness of the threats that often get overlooked when hardening software and hardware. In practice, you can only ever mitigate against security threats. For example, Bryan Dye once told the Wall Street Journal that Symantec, then the biggest antivirus vendor, was getting out of the antivirus business because the software stopped at most around 45 per cent of viruses. He said the money was no longer in “protect”, but instead in “detect and respond”.&lt;/p&gt;

&lt;p&gt;Or consider the compromising of RSA’s SecurID. The theory at the time was that a nation state was trying to get access to secrets at a military aerospace vendor, but was blocked by the vendor’s use of SecurID. So instead they sent targeted email to RSA employees, which enabled them to breach the SecurID security and get what they were really after. RSA took a lot of criticism for how it responded to this attack. As an aside, that’s why it’s important to train your staff to recognize phishing emails.&lt;/p&gt;

&lt;p&gt;Say you don’t have a disaster recovery plan. When the worst happens, even if you changed all your passwords, you know you’ll have to do it again after all the services you use have got their new keys in place. And you’ll still wonder if anyone managed to leave any snooping software on those services while the keys were compromised. The heartbleed OpenSSL vulnerability is an example of the worst happening without a disaster recovery plan.&lt;/p&gt;

&lt;p&gt;As IT professionals, when we talk about security we’re mostly talking about confidentiality, integrity, and availability (CIA) of data. We don’t want confidential data leaving the organization, so we enforce a trusted device policy to ensure all BYO devices have their data encrypted and can be remotely wiped. We block the use of file sharing applications like DropBox that can lead to confidential data being stored in the public cloud. And we provide users with alternatives that keep the data within the corporate network, because users really like DropBox.&lt;/p&gt;

&lt;p&gt;We lock down all the USB ports, because corporate spies have started sending out free mice with hidden malware to employees (I’m not making this up). And we use access controls to ensure people only have access to the information they need to do their job. We look after data integrity by making regular backups, and we do periodic restores to make sure those backups are working. And we make sure the data is available by doing system maintenance while the west coast of America is asleep. Ok, so outside of California your mileage may vary. So assuming you’ve done everything you should to secure your software and hardware, what have you missed? Well, I’ll get to that later.&lt;/p&gt;

&lt;h4&gt;
  
  
  Case study: the payment card industry
&lt;/h4&gt;

&lt;p&gt;I’ve been interested in security since the late 1980s when I got my copy of Hugo Cornwall’s &lt;em&gt;Hacker’s Handbook&lt;/em&gt;, where I discovered the existence of the Internet, or ARPAnet as it was known back then. Prior to joining the security business, I worked for a retail software company, where I discovered all sorts of frightening things about how card payments are processed. For instance, did you know that when chip and PIN payment was originally introduced in the UK that there was no encryption between the mobile radio units and the base stations? Thankfully, that’s now been resolved.&lt;/p&gt;

&lt;p&gt;Or did you know that all the card payment transactions in high street stores used to be stored and sent unencrypted to the banks? Now the reason for this was that, as I’m sure you can imagine, there are a very large numbers of transactions throughout the day’s trading. Traditionally, these were sent to the bank at the end of the day for overnight processing. You’ll be glad to know that these were sent over a dedicated line rather than the public Internet.&lt;/p&gt;

&lt;p&gt;But even so, they were sitting on the host system without any encryption. And the reason for that was that the overhead added by decrypting each transaction. Because they would all have to be individually encrypted and decrypted to work with the batch processing system at the banks. It would have added just enough delay to ensure that eventually the system wouldn’t be able to keep up with the number of transactions. Payments would be going into the queue faster than they could be processed.&lt;/p&gt;

&lt;p&gt;Now, you may have heard of PCI DSS (the Payment Card Industry Data Security Standard). And what, among other things, that standard says, is that organizations have to restrict who has access to the folder with the card payments in it. And so already we’ve gone beyond the software and hardware, and we’ve got a security policy, the PCI DSS, and that policy is based at least in part on trust. If you want to read more about trust, I recommend Bruce Schneier’s book &lt;em&gt;Liars &amp;amp; Outliers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;What I want to get across here, is that software and hardware are just part of the security solution. All retailers in the UK are supposed to be audited for compliance with PCI DSS. But according to Financial Fraud Action UK, card fraud losses in the UK for 2013 totaled £450.4 million. Now that sounds bad, but it to put it another way it’s equal to 7.4 pence for every £100 spent. And the things we have to consider here are the risk and, the cost of mitigating that risk.&lt;/p&gt;

&lt;p&gt;The payment card industry wants to keep fraud down, but if putting in place a solution that eliminates fraud costs more than the cost of the fraud itself, then it will look for a cheaper solution. So actually, even before you secure the box, you really need a security policy. Because if there’s nothing of value in the box, then you don’t really need it to be that secure. But if what’s in the box is the most valuable thing you have, then you really need to be able to deal with a situation where all of your security measures failed.&lt;/p&gt;

&lt;h4&gt;
  
  
  The importance of a security policy
&lt;/h4&gt;

&lt;p&gt;So, although that was a bit of a roundabout way to get to my point, what I’m advocating is that organizations need a security policy. And vendors of security solutions, need to help their customers to think about security in this way. So what makes a good security policy? Well first of all you need to have someone with the responsibility for the policy, the chief security officer (CSO). And one of their most important responsibilities is to keep the policy under review, because the environment is changing all the time, and a static policy can’t address that.&lt;/p&gt;

&lt;p&gt;So how do you come up with a good security policy? Well, there are various things you need to take into account. But primarily it’s about working out the risk: How likely is it that someone will walk out of this facility with all this government data on a USB pen drive? And the cost: what will be the effect if this confidential information about everyone we’re spying on gets into the public domain?&lt;/p&gt;

&lt;p&gt;So for each risk, you work out the associated cost, and then you come up with a solution proportionate to the risk. Let’s go back to the early days of hacking. I’m not sure anyone ever calculated the risk of hackers going dumpster diving for telephone engineer manuals. But I’m reasonably confident that the cost of shredding all those manuals set against the risk of someone typing the whole thing into a computer and uploading it to a bulletin board system was fairly high. Now this is in the days before cheap scanners, good optical character recognition and widespread access to the Internet, which is why everyone now securely disposes of confidential documents, don’t they?&lt;/p&gt;

&lt;p&gt;Now, in the Snowden case there were a couple of things that surprised me. First, that the NSA wasn’t using mandatory access control (MAC). Or in other words, they weren’t using a trusted computing solution. They were using the same operating systems as the rest of us. I think partly that can be explained by the fact that it’s expensive to get support for trusted operating systems, because almost no-one besides governments use them. And often the applications that governments want to run aren’t available on those platforms, so the cost of using them may exceed their benefit in mitigating risk. But the other thing that surprised me is the practice of password sharing.&lt;/p&gt;

&lt;p&gt;And that brings me to the main vulnerability you face if your hardware and software are secure. Your users. Kevin Mitnick, I’m assuming you’ve heard of him, if not look him up. He asserts, and I don’t disagree with him, that humans are the weakest link in security. In fact, I recommend his book &lt;em&gt;The Art of Deception&lt;/em&gt; if you want to know exactly how predictable and easy to manipulate people are.&lt;/p&gt;

&lt;p&gt;So let’s look at the password sharing issue. If you put up a big enough road block for your users to getting work done, they will find a detour around it. Is it easier to tell someone your password than jump through hoops to get that one file they need? Many companies have a policy that passwords need to contain at least eight alphanumeric characters, both upper and lower case letters, at least one number, and at least one special character. It also can’t be one of the previous three passwords. So what do users do? They pick dictionary words with substitutions.&lt;/p&gt;

&lt;p&gt;And then users have to change their password every six months, or quarterly if it’s an administrative password. This leads to one of two things. They write the passwords down. Or they repeatedly change their password until they cycle back to their original password. It’s pretty easy to get a valid corporate username. They’re in all of our email addresses. If you can actually get on to a corporate site and physically connect to the network, you can just keep trying to connect until you brute force the password.&lt;/p&gt;

&lt;p&gt;So how do you get on site? Well, this touches on the other main vulnerability, physical security. Many companies use employee badges for building access, and various areas are restricted to specific groups of employees. They have a policy of not holding the door open for people employees don’t recognize. Unfortunately, it is in most people’s nature to be helpful. If I smile at someone as they go through a door, and I’m dressed appropriately, they’re less likely to question if they should have just let me follow them.&lt;/p&gt;

&lt;p&gt;Mitnick’s book is full of these kinds of social engineering techniques. But actually the easiest way to get on site at some companies is to sign up for a training course. You might have read in the news earlier this year about the gang of crooks who stole £1.25 million by going into bank branches and attaching KVM (that’s keyboard/video/mouse) switches. Reports haven’t detailed how they got into the building, but it’s safe to assume it was low tech, and they didn’t break in.&lt;/p&gt;

&lt;p&gt;So you need to educate staff about threats. Phishing email, social engineering, not picking up USB pen drives that you find lying around and connecting them to your corporate PC. I'm not even going to cover BYOD. That’s “Bring Your Own Device”, although some have called it “Bring Your Own Disaster” because of the additional risks and management headaches it entails. I will say that the mitigation is to require BYO devices to meet a minimum level of protection: a secure password, encrypted storage, the ability to do a remote wipe. But basically, the message is that it’s all very well having a security policy, but it isn’t much use if your staff don’t know about it.&lt;/p&gt;

&lt;p&gt;Once you’ve got a policy in place, then you need to stress test it. This is where the “red team” comes in. This can be an internal group, or an externally hired group, whose job is to attempt to penetrate your security, for instance by leaving USB pen drives lying around or sending test phishing emails. Penetration testing needs to be conducted on a regular basis, the frequency of which will depend on the risk and cost analysis, and the security policy updated following the findings.&lt;/p&gt;

&lt;p&gt;But let’s come back to physical security. In the aftermath of hurricane Sandy, it seems fairly obvious to state that if you’re doing offsite backup to multiple data centers that at the very least you don’t want them co-located in the same flood plain. Of course since then everyone has looked at where their critical services are and ensured sufficient redundancy to deal with a major disaster. Haven’t they?&lt;/p&gt;

&lt;p&gt;Assuming you’ve got the location sorted out, and you’re outside the 500-year flood plain, you’re going to want to consider alternate power sources, given the increasing demands being placed on the power grid. And when you’ve got your failover power supply in place, it helps to test that it actually works. Your backups are only as good as your ability to recover from those backups, so it’s important to perform regular testing to make sure that’s the case. Physical access can be controlled by physical barriers, locks, guards, but it can also be monitored by video cameras. Servers get hot, so you need to consider fire suppression systems. Ideally, ones that will leave the data in a recoverable state.&lt;/p&gt;

&lt;h4&gt;
  
  
  Summary
&lt;/h4&gt;

&lt;p&gt;I’ve barely scratched the surface, but hopefully I’ve given you some things to think about. So to sum up: you want a security policy that is under continual review and covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Human nature&lt;/li&gt;
&lt;li&gt;Disaster recovery&lt;/li&gt;
&lt;li&gt;Physical location&lt;/li&gt;
&lt;li&gt;Penetration testing&lt;/li&gt;
&lt;li&gt;Social engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And really the most important thing is to raise security awareness.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>hacking</category>
      <category>socialengineering</category>
    </item>
    <item>
      <title>Managing packages on macOS with Homebrew</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 19 Dec 2022 19:43:09 +0000</pubDate>
      <link>https://community.ops.io/aowendev/managing-packages-on-macos-with-homebrew-4b82</link>
      <guid>https://community.ops.io/aowendev/managing-packages-on-macos-with-homebrew-4b82</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're a Linux user, or you read my post on Scoop, you'll be familiar with package managers. They aim to simplify installing, upgrading, configuring and removing software. A key feature is dependency management: if a package requires software that isn't already installed, the package manager can install it.&lt;/p&gt;

&lt;p&gt;Apple's solution to managing installed software on macOS, and its other operating systems, is the App Store application. Typically, end-user applications don't have dependencies. However, command line tools often do, particularly if they originated on Linux. Another issue is that macOS typically ships with older versions of common tools, if it includes them at all.&lt;/p&gt;

&lt;p&gt;The most popular third-party package manager on macOS used to be &lt;a href="https://www.macports.org/" rel="noopener noreferrer"&gt;MacPorts&lt;/a&gt;, and it's still a valid choice. But it has been replaced in popularity by &lt;a href="https://brew.sh/" rel="noopener noreferrer"&gt;Homebrew&lt;/a&gt;, which is based on Git and Ruby.&lt;/p&gt;

&lt;p&gt;If you haven't already installed the Xcode command line tools, you might want to do that first. From the command line, enter: &lt;code&gt;xcode-select --install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To install Homebrew, open the &lt;strong&gt;Terminal&lt;/strong&gt; and enter &lt;code&gt;/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To update the package list, enter &lt;code&gt;brew update&lt;/code&gt;. To upgrade all packages, enter &lt;code&gt;brew upgrade&lt;/code&gt;. Now you're ready to add whatever packages you like.&lt;/p&gt;

&lt;p&gt;Homebrew includes an extension called Cask. You install command line apps with &lt;code&gt;brew install &amp;lt;&lt;/code&gt;&lt;em&gt;&lt;code&gt;app name&lt;/code&gt;&lt;/em&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;. For graphical apps use &lt;code&gt;brew casks install &amp;lt;&lt;/code&gt;&lt;em&gt;&lt;code&gt;app name&lt;/code&gt;&lt;/em&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Having been using an M1 (ARM CPU) Mac for over a year now, I'm pleased to report that I haven't run into any problems with Homebrew.&lt;/p&gt;

</description>
      <category>apple</category>
      <category>homebrew</category>
      <category>devops</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>Managing packages on Windows with Scoop</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 19 Dec 2022 19:41:48 +0000</pubDate>
      <link>https://community.ops.io/aowendev/managing-packages-on-windows-with-scoop-411d</link>
      <guid>https://community.ops.io/aowendev/managing-packages-on-windows-with-scoop-411d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have even a passing familiarity with Linux, you're probably aware of the concept of package management. The goal is to simplify the installing, upgrading, configuring and removing software. One of the key features is dependency management: if your software depends on some other software that isn't already installed, the package manager will give you the option to install it.&lt;/p&gt;

&lt;p&gt;Microsoft provides a &lt;a href="https://en.wikipedia.org/wiki/Windows_Package_Manager" rel="noopener noreferrer"&gt;Windows Package Manager&lt;/a&gt;, known as &lt;em&gt;winget&lt;/em&gt;. However, at the time of writing it's only been around for a couple of years and I got a hash error when I tried to use it to install Google Chrome.&lt;/p&gt;

&lt;p&gt;A popular third-party option is &lt;a href="https://chocolatey.org/" rel="noopener noreferrer"&gt;Chocolatey&lt;/a&gt;, but it depends on &lt;a href="https://www.nuget.org/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; which is a package manager in its own right. I prefer to use &lt;a href="https://scoop.sh/" rel="noopener noreferrer"&gt;Scoop&lt;/a&gt;. The latest install instructions are on the website, but I'll go through how I installed it on Windows 11.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open PowerShell.&lt;/li&gt;
&lt;li&gt;[Optional] Enter &lt;code&gt;Set-ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/code&gt;. This enables a remote script to run the first time.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;Invoke-WebRequest get.scoop.sh | Invoke-Expression&lt;/code&gt;. This installs Scoop.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;scoop bucket add extras&lt;/code&gt;. The extras bucket includes software that doesn't match the &lt;a href="https://github.com/ScoopInstaller/Scoop/wiki/Criteria-for-including-apps-in-the-main-bucket" rel="noopener noreferrer"&gt;main criteria&lt;/a&gt; for inclusion.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;scoop bucket add nonportable&lt;/code&gt;. The nonportable bucket includes software that must be installed to a specific location.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;scoop install git&lt;/code&gt;. Even if you're not using Git, Scoop uses it to perform updates.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;scoop update&lt;/code&gt;. This will update the buckets you added.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you're ready to add whatever packages you like. Here are some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker: &lt;code&gt;scoop install docker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Desktop: &lt;code&gt;scoop install github&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Google Chrome: &lt;code&gt;scoop install googlechrome&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Helm: &lt;code&gt;scoop install helm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Notepad++: &lt;code&gt;scoop install notepadplusplus&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SQL Server Management Studio: &lt;code&gt;scoop install sql-server-management-studio-np&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Visual Studio Code: &lt;code&gt;scoop install vscode&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify the software has been correctly installed by viewing the &lt;code&gt;C:\Users\&lt;/code&gt;&lt;em&gt;&lt;code&gt;&amp;lt;user&amp;gt;&lt;/code&gt;&lt;/em&gt;&lt;code&gt;\scoop\apps&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;To update all your installed packages to the latest version, enter &lt;code&gt;scoop update all&lt;/code&gt;. When I was creating a VM for developer training with numerous prerequisite software packages, I found it useful to put this command in a startup script.&lt;/p&gt;

&lt;p&gt;Having recently revisited the VM setup for &lt;a href="https://labs.azure.com/" rel="noopener noreferrer"&gt;Azure Labs&lt;/a&gt;, I've discovered a few gotchas. Here's how I got it working.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an Azure Labs template VM from the &lt;em&gt;Windows 10 N Enterprise with Visual Studio 2019 Community&lt;/em&gt; image in the marketplace.&lt;/li&gt;
&lt;li&gt;Fire up the VM, then from the standard shell enter: &lt;code&gt;runas /trustlevel:0x20000 powershell.exe&lt;/code&gt;. The Scoop install script won't run with admin privileges. The &lt;code&gt;ExecutionPolicy&lt;/code&gt; directive doesn't work when the shell is run as administrator. And the VM template doesn't give you the option to start a shell without admin privileges.&lt;/li&gt;
&lt;li&gt;From PoweShell, enter: &lt;code&gt;iwr get.scoop.sh -useb | iex&lt;/code&gt;. The &lt;code&gt;-useb&lt;/code&gt; directive is required with Windows N (something to do with Internet Explorer).&lt;/li&gt;
&lt;li&gt;Close both shells and then open PoweShell and enter &lt;code&gt;scoop bucket add main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;scoop update&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now carry on as normal. You don't need to install Git, because it's included with the VS 2019 Community image.&lt;/p&gt;

&lt;p&gt;Image: Original by &lt;a href="https://unsplash.com/photos/TLD6iCOlyb0" rel="noopener noreferrer"&gt;Ian Dooley&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>scoop</category>
      <category>devops</category>
      <category>windows</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>Using GitHub Actions to automatically unpack a zip archive</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 19 Dec 2022 19:39:05 +0000</pubDate>
      <link>https://community.ops.io/aowendev/using-github-actions-to-automatically-unpack-a-zip-archive-5610</link>
      <guid>https://community.ops.io/aowendev/using-github-actions-to-automatically-unpack-a-zip-archive-5610</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was recently working with some software that could push a &lt;code&gt;zip&lt;/code&gt; archive of content to a Git repository. However, what I really wanted was for the contents of the archive to be pushed to the repository. So I created a GitHub Action to do that for me. I've covered the format of GitHub Actions before. But to recap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;name&lt;/strong&gt; is what gets displayed in the actions list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;on&lt;/strong&gt; sets the triggers:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;push&lt;/strong&gt; and &lt;strong&gt;pull&lt;/strong&gt; trigger the script on push and pull requests. If you don't specify &lt;strong&gt;branches&lt;/strong&gt;, they default to &lt;strong&gt;main&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;paths&lt;/strong&gt; specifies the paths to match. In this case, the script will trigger when a &lt;code&gt;zip&lt;/code&gt; file is pushed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;worklflow_dispatch&lt;/strong&gt; enables you to manually trigger the script from the actions list.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;jobs&lt;/strong&gt; defines one or more named tasks, in this example &lt;strong&gt;unzip&lt;/strong&gt;.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;runs-on&lt;/strong&gt; specifies the VM environment. If you can use Ubuntu, it's the cheapest option with hosted runners.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;steps&lt;/strong&gt; can be used to invoke actions such as &lt;strong&gt;checkout&lt;/strong&gt; (which fetches a copy of the repository to the VM) and to execute shell commands with &lt;strong&gt;name: run&lt;/strong&gt;.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;run&lt;/strong&gt; with a pipe character ( &lt;strong&gt;|&lt;/strong&gt; ) executes a multi-line script. Without it, a single line is executed.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And here's the action I created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;extract a zip file&lt;/span&gt;

    &lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.zip'&lt;/span&gt;
      &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;unzip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;rm -r css&lt;/span&gt;
              &lt;span class="s"&gt;rm -r en&lt;/span&gt;
              &lt;span class="s"&gt;rm -r fonts&lt;/span&gt;
              &lt;span class="s"&gt;rm -r js&lt;/span&gt;
              &lt;span class="s"&gt;filename=$(basename -s .zip *.zip)&lt;/span&gt;
              &lt;span class="s"&gt;unzip *.zip&lt;/span&gt;
              &lt;span class="s"&gt;rm *.zip&lt;/span&gt;
              &lt;span class="s"&gt;mv $filename temp&lt;/span&gt;
              &lt;span class="s"&gt;mv temp/out/* .&lt;/span&gt;
              &lt;span class="s"&gt;rm -r temp&lt;/span&gt;
              &lt;span class="s"&gt;git config user.name github-actions&lt;/span&gt;
              &lt;span class="s"&gt;git config user.email github-actions@github.com&lt;/span&gt;
              &lt;span class="s"&gt;git add .&lt;/span&gt;
              &lt;span class="s"&gt;git commit -m "unzip"&lt;/span&gt;
              &lt;span class="s"&gt;git push origin main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is written for the Linux command line. Let's break it down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; css

    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; en
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; fonts
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea here is that the contents of the zip file should replace what is already in that particular branch of the repository. You might want to call the branch &lt;strong&gt;uploads&lt;/strong&gt;. The checkout action has already been run, but it's a good idea to clear out any known folders. The &lt;code&gt;-r&lt;/code&gt; tag makes the action recursive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; .zip &lt;span class="k"&gt;*&lt;/span&gt;.zip&lt;span class="si"&gt;)&lt;/span&gt;

    unzip &lt;span class="k"&gt;*&lt;/span&gt;.zip
    &lt;span class="nb"&gt;rm&lt;/span&gt; .zip
    &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt; temp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script assumes that we don't know the name of the &lt;code&gt;zip&lt;/code&gt; file, but that there is only one file. It will determine the name, unzip the file to the root, remove the &lt;code&gt;zip&lt;/code&gt; file and rename the folder containing the zip to &lt;code&gt;temp&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;temp/out/ &lt;span class="nb"&gt;.&lt;/span&gt;

    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; temp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, the contents of the &lt;code&gt;zip&lt;/code&gt; file are two folders deep (in the &lt;code&gt;out&lt;/code&gt; folder). This moves the contents from the nested folder to the root, and then removes the &lt;code&gt;temp&lt;/code&gt; folder and its contents (the empty &lt;code&gt;out&lt;/code&gt; folder). The dot (&lt;code&gt;.&lt;/code&gt;) represents the current working directory (where the repo was checked out on the VM).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config user.name github-actions

    git config user.email github-actions@github.com
    git add &lt;span class="nb"&gt;.&lt;/span&gt;
    git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"unzip"&lt;/span&gt;
    git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part of the script pushes the changes back to the repository.&lt;/p&gt;

&lt;p&gt;Image: Detail from The Unarchiver zip file icon. I looked for an appropriate unzip image with a Creative Commons license, but the results were not safe for work.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>cicd</category>
      <category>tutorials</category>
    </item>
    <item>
      <title>Manage the contents of a Readme.io docs site from your own Git repository</title>
      <dc:creator>Andrew Owen</dc:creator>
      <pubDate>Mon, 22 Aug 2022 14:49:46 +0000</pubDate>
      <link>https://community.ops.io/aowendev/manage-the-contents-of-a-readmeio-docs-site-from-your-own-git-repository-3n98</link>
      <guid>https://community.ops.io/aowendev/manage-the-contents-of-a-readmeio-docs-site-from-your-own-git-repository-3n98</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article originally appeared on my personal dev blog: &lt;a href="https://andrewowen.net/blog/" rel="noopener noreferrer"&gt;Byte High, No Limit&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://readme.io/" rel="noopener noreferrer"&gt;ReadMe.io&lt;/a&gt; is a popular user docs site. It has a Markdown editor, theme builder and Swagger / OpenAPI file import. It's fast and responsive, and it looks nice. But the last time I checked, all your content goes in a bucket that you don't have direct access to.&lt;/p&gt;

&lt;p&gt;One upshot of this is that if you want to make changes across documents, such as changing a product name, you have to edit documents individually. It would be much nicer if you could directly access the repository in VS Code and make site-wide changes.&lt;/p&gt;

&lt;p&gt;Even though this isn't supported, there are a couple of features in Readme.io that can enable you to take control of your data. First, you can &lt;a href="https://docs.readme.com/docs/exporting-docs" rel="noopener noreferrer"&gt;export your content&lt;/a&gt;. Second, Readme.io has an &lt;a href="https://docs.readme.com/reference/intro-to-the-readme-api" rel="noopener noreferrer"&gt;API&lt;/a&gt;. There are some tasks that you'll still have to perform in the web interface. But for the most part, you can keep your data in a Git repository, make local changes, and use the API to push your changes to your live site.&lt;/p&gt;

&lt;h4&gt;
  
  
  ReadMe.io APIs
&lt;/h4&gt;

&lt;p&gt;When I was getting started, I found it useful to use &lt;a href="https://www.postman.com/downloads/" rel="noopener noreferrer"&gt;Postman&lt;/a&gt; to query the Readme.io site. You can save this collection as &lt;code&gt;readme.postman_collection.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
"info": {
"_postman_id": "&amp;lt;!--insert your postman ID here--&amp;gt;",
"name": "readme.io",
"schema": "[https://schema.getpostman.com/json/collection/v2.1.0/collection.json](https://schema.getpostman.com/json/collection/v2.1.0/collection.json "https://schema.getpostman.com/json/collection/v2.1.0/collection.json")"
},
"item": \[
{
"name": "Get doc",
"request": {
"method": "GET",
"header": \[\],
"url": {
"raw": "[https://dash.readme.com/api/v1/docs/](https://dash.readme.com/api/v1/docs/ "https://dash.readme.com/api/v1/docs/"){{slug}}",
"protocol": "https",
"host": \[
"dash",
"readme",
"com"
\],
"path": \[
"api",
"v1",
"docs",
"{{slug}}"
\]
}
},
"response": \[\]
},
{
"name": "Get category ID",
"request": {
"method": "GET",
"header": \[\],
"url": {
"raw": "[https://dash.readme.com/api/v1/docs/get-category-id](https://dash.readme.com/api/v1/docs/get-category-id "https://dash.readme.com/api/v1/docs/get-category-id")",
"protocol": "https",
"host": \[
"dash",
"readme",
"com"
\],
"path": \[
"api",
"v1",
"docs",
"get-category-id"
\]
}
},
"response": \[\]
},
{
"name": "Update doc",
"request": {
"method": "PUT",
"header": \[\],
"body": {
"mode": "raw",
"raw": "{\\n \\"title\\": \\"{{title}}\\",\\n \\"excerpt\\": \\"{{excerpt}}\\",\\n \\"category\\": \\"{{category}}\\",\\n \\"hidden\\": {{hidden}},\\n \\"body\\": \\"{{body}}\\"\\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "[https://dash.readme.com/api/v1/docs/](https://dash.readme.com/api/v1/docs/ "https://dash.readme.com/api/v1/docs/"){{slug}}",
"protocol": "https",
"host": \[
"dash",
"readme",
"com"
\],
"path": \[
"api",
"v1",
"docs",
"{{slug}}"
\]
}
},
"response": \[\]
},
{
"name": "Delete doc",
"request": {
"method": "DELETE",
"header": \[\],
"url": {
"raw": "[https://dash.readme.com/api/v1/docs/](https://dash.readme.com/api/v1/docs/ "https://dash.readme.com/api/v1/docs/"){{slug}}",
"protocol": "https",
"host": \[
"dash",
"readme",
"com"
\],
"path": \[
"api",
"v1",
"docs",
"{{slug}}"
\]
}
},
"response": \[\]
},
{
"name": "Create doc",
"request": {
"method": "POST",
"header": \[\],
"body": {
"mode": "raw",
"raw": "{\\n \\"title\\": \\"{{title}}\\",\\n \\"excerpt\\": \\"{{excerpt}}\\",\\n \\"category\\": \\"{{category}}\\",\\n \\"hidden\\": true,\\n \\"body\\": \\"{{body}}\\"\\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "[https://dash.readme.com/api/v1/docs](https://dash.readme.com/api/v1/docs "https://dash.readme.com/api/v1/docs")",
"protocol": "https",
"host": \[
"dash",
"readme",
"com"
\],
"path": \[
"api",
"v1",
"docs"
\]
}
},
"response": \[\]
},
{
"name": "Search docs",
"request": {
"method": "POST",
"header": \[\],
"url": {
"raw": "[https://dash.readme.com/api/v1/docs/search?search=sphinx](https://dash.readme.com/api/v1/docs/search?search=sphinx "https://dash.readme.com/api/v1/docs/search?search=sphinx")",
"protocol": "https",
"host": \[
"dash",
"readme",
"com"
\],
"path": \[
"api",
"v1",
"docs",
"search"
\],
"query": \[
{
"key": "search",
"value": "sphinx"
}
\]
}
},
"response": \[\]
}
\],
"auth": {
"type": "basic",
"basic": \[
{
"key": "username",
"value": "{{apiKey}}",
"type": "string"
}
\]
},
"event": \[
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": \[
""
\]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": \[
""
\]
}
}
\],
"variable": \[
{
"key": "category\\n",
"value": "&amp;lt;!--get the category ID from the web interface--&amp;gt;"
},
{
"key": "slug",
"value": "sandbox"
},
{
"key": "title",
"value": "Sandbox"
},
{
"key": "hidden",
"value": "true"
},
{
"key": "body",
"value": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
\]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import the collection to Postman and use Basic Auth with the API key as the username and an empty password.&lt;/p&gt;

&lt;h5&gt;
  
  
  Create doc
&lt;/h5&gt;

&lt;p&gt;This endpoint requires these attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;title&lt;/strong&gt; – the name as you want it displayed on the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;category&lt;/strong&gt; – UUID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hidden&lt;/strong&gt; – false&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;body&lt;/strong&gt; – JSON string&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;hidden&lt;/code&gt; attribute is always set to &lt;code&gt;true&lt;/code&gt;. The final publishing stage after reviewing the content as it will appear is to set this to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unless I missed something, when I originally came up with this solution, it was only possible to create categories on Readme.io. But there &lt;em&gt;is&lt;/em&gt; an API for &lt;a href="https://docs.readme.com/reference/createcategory" rel="noopener noreferrer"&gt;that&lt;/a&gt;. I don't currently have an active Readme.io subscription to test, so I'll leave it to you to add those endpoints to the Postman collection.&lt;/p&gt;

&lt;p&gt;Articles can be posted under existing articles. In this case, you should do a &lt;code&gt;GET&lt;/code&gt; on the parent article to get the category ID.&lt;/p&gt;

&lt;h4&gt;
  
  
  Convert Markdown to JSON string with JQ
&lt;/h4&gt;

&lt;p&gt;Readme.io stores articles in Markdown format (with a YAML header). But the API requires the data in JSON format. Fortunately, there's a command line tool call &lt;a href="https://stedolan.github.io/jq/" rel="noopener noreferrer"&gt;JQ&lt;/a&gt; that can encapsulate the Markdown in a JSON string. This will form the &lt;code&gt;body&lt;/code&gt; attribute. From the CLI, enter:  &lt;code&gt;jq -R -s . &amp;lt; filename.md &amp;gt; filename.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pushing changes in your repository to the live site
&lt;/h4&gt;

&lt;p&gt;I wrote a Bash script to automatically updates all the &lt;code&gt;.md&lt;/code&gt; files in a repository using the &lt;code&gt;PUT&lt;/code&gt; method. It's a bit of a hack. It requires the &lt;code&gt;$AUTH&lt;/code&gt; token to be defined, and it will only work on files where the header is exactly eight lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
title: "article title"
category: "category UUID"
excerpt: "article description"
hidden: true
createdAt: "2022-05-19T00:00:00.000Z"
updatedAt: "2022-05-19T10:00:00.001Z"
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, articles download from Readme.io include &lt;code&gt;slug&lt;/code&gt; metadata but no &lt;code&gt;category&lt;/code&gt; metadata. Since the slug is derived from the filename, you need to replace the &lt;code&gt;slug&lt;/code&gt; metadata with the &lt;code&gt;category&lt;/code&gt; metadata and give it the appropriate UUID for the category heading it appears under.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you don't include an &lt;code&gt;excerpt&lt;/code&gt; definition, you must ensure the &lt;code&gt;body&lt;/code&gt; text starts on line &lt;code&gt;9&lt;/code&gt; of the file.&lt;/p&gt;

&lt;p&gt;Here's the Bash script. Save it as &lt;code&gt;md2json.sh&lt;/code&gt; in the &lt;code&gt;v1.0&lt;/code&gt; folder of your exported site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export AUTH="&amp;lt;!--your AUTH token--&amp;gt;"
for subdir in *; do
test -d "$subdir" || continue
echo $subdir
cd "$subdir"
for f in _.md; do
export SLUG=${f%%._}
sed '1,8d' $f &amp;gt; "$SLUG.tmp"
sed '6,$d' $f &amp;gt; "$SLUG.yml"
jq -R -s . &amp;lt; "$SLUG.tmp" &amp;gt; "$SLUG.bdy"
rm "$SLUG.tmp"
printf "body: " &amp;gt;&amp;gt; "$SLUG.yml"
cat "$SLUG.bdy" &amp;gt;&amp;gt; "$SLUG.yml"
yq &amp;lt; "$SLUG.yml" &amp;gt; "$SLUG.json"
rm "$SLUG.bdy"
rm "$SLUG.yml"
curl -X PUT -H 'Content-Type: application/json'   
\-H "Authorization: Basic $AUTH"   
\-d "$(&amp;lt;$SLUG.json)"   
https://dash.readme.com/api/v1/docs/$SLUG
rm "$SLUG.json"
done
cd ..
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Managing images
&lt;/h4&gt;

&lt;p&gt;The last part of the puzzle is managing images. At the time of writing, I don't know a way to upload images using the API. My suggestion is that you keep a hidden article on the website and add images to it using the web interface. Readme.io will rename the file and assign it an ID. You can then extract this information from the article. I would then add a copy of the image with this modified filename to your repository (in case you ever want to migrate to a different docs solution). This will also enable you to preview your content locally before pushing it to the live site. &lt;/p&gt;

</description>
      <category>readmeio</category>
      <category>git</category>
      <category>postman</category>
      <category>tutorials</category>
    </item>
  </channel>
</rss>
