# Diff of /2024/01/29/feedletter-tutorial/index.html

## Revision e55882f63ed583549888ac31d22c7fae968d7359 => current

Line | e55882f63ed583549888ac31d22c7fae968d7359 | current
--- | --- | ---
1 | <!DOCTYPE html> | <!DOCTYPE html>
2 | <html> | <html>
3 | <head> | <head>
⋮ | ⋮ | ⋮
85 | <hr class="below-title"> | <hr class="below-title">
86 | </div> | </div>
87 | <div class="update-prepend rss-description-exclude"> | <div class="update-prepend rss-description-exclude">
88 | <em> ➣ This post was meaningfully revised at 2024-06-20 @ 01:10 PM EDT. The previous revision is <a href="index-oldcommit-acb84c08c1bbcc83036b9cad70f6a3c5d60865c4.html">here</a>, diff <a href="index-diff-acb84c08c1bbcc83036b9cad70f6a3c5d60865c4-to-current.html">here</a>. (See <a href="index.html#update-history">update history</a>.) </em> | <em> ➣ This post was meaningfully revised at 2026-06-11 @ 12:30 PM EDT. The previous revision is <a href="index-oldcommit-e55882f63ed583549888ac31d22c7fae968d7359.html">here</a>, diff <a href="index-diff-e55882f63ed583549888ac31d22c7fae968d7359-to-current.html">here</a>. (See <a href="index.html#update-history">update history</a>.) </em>
89 | <hr> | <hr>
90 | </div> | </div>
91 | <div class="entry-body"> | <div class="entry-body">
⋮ | ⋮ | ⋮
391 | $ cd feedletter-local | $ cd feedletter-local
392 | $ ./feedletter db-init | $ ./feedletter db-init
393 | </code></pre> | </code></pre>
394 |  | <div class="note">
395 |  | <p>At this point, if you are running feedletter >=0.2.5 it's also a good idea to run the following command:</p>
396 |  | <pre><code class="language-plaintext">$ ./feedletter generate-starter-untemplates
397 |  | </code></pre>
398 |  | <p>That will make it simpler, going forward, to modify the look and feel of e-mails that you generate — newsletters, confirmation requests, etc.</p>
399 |  | <p>If you don't run this command, everything will still be fine, but you'll need to do a bit more work if you want to change the style of your e-mails.</p>
400 |  | <p>Be sure to perform this step <em>before <a href="#12-define-subscribables-to-feeds">defining any subscribables</a></em>. Any subscribables you define before generating the starter templates will use compiled-in untemplates, and you'll have to define new untemplates and update your subscriptions to use them if you want to change the styling. (See the notes in <a href="#15-tweak-the-newsletter-styles">Section 15</a> of this tutorial.)</p>
401 |  | <p>(Update added 2026-06-11)</p>
402 |  | </div>
403 | <h2><a href="#10-perform-in-database-configuration" id="10-perform-in-database-configuration" name="10-perform-in-database-configuration" class="anchorlink"></a>10. Perform in-database configuration</h2> | <h2><a href="#10-perform-in-database-configuration" id="10-perform-in-database-configuration" name="10-perform-in-database-configuration" class="anchorlink"></a>10. Perform in-database configuration</h2>
404 | <p>Some of <em>feedletter</em>'s config sits in the secrets file, but much lives in the application's database itself.</p> | <p>Some of <em>feedletter</em>'s config sits in the secrets file, but much lives in the application's database itself.</p>
405 | <p>We can see <em>feedletter</em>'s current (default) configuration simply by running</p> | <p>We can see <em>feedletter</em>'s current (default) configuration simply by running</p>
⋮ | ⋮ | ⋮
845 | <p>By default, we just pulled the first item (and most recent, since blogs are usually reverse-chronological) from the feed. We can also pull a random item off the feed to view with <code>--random</code>, or a particular item identified by its <code><guid></code> element in the feed with <code>--guid <guid></code>.</p> | <p>By default, we just pulled the first item (and most recent, since blogs are usually reverse-chronological) from the feed. We can also pull a random item off the feed to view with <code>--random</code>, or a particular item identified by its <code><guid></code> element in the feed with <code>--guid <guid></code>.</p>
846 | </div> | </div>
847 | <p>Let's see how we can restyle this post to make it a bit better.</p> | <p>Let's see how we can restyle this post to make it a bit better.</p>
848 | <p>We have been using the built-in default untemplate to compose our items. We cannot modify that.</p> | <p>If we ran <code>./feedletter generate-starter-templates</code> before defining your subscribables, as recommended in a note <a href="#9-initialize-the-feedletter-database">above</a>, things are very easy. In the <code>untemplate</code> directory you will find a subdirectory <code>default/subscription/email</code>. In that directory, you'll see a file called <code>composeUniversal.html.untemplate</code>.</p>
849 | <p>But our feedletter installation directory contains a copy of this untemplate that we can deploy and tweak.</p> | <p>If we start editing that template, our edits will take effect immediately when running <code>feedletter-style</code>, like this:</p>
850 | <p>To do so, we'll have to create a folder for this file under <code>untemplate</code>. We’ll call it <code>tutorial</code>. Then...</p> | <pre><code class="language-plaintext">$ ./feedletter-style compose-single --subscribable-name lgm --port 45612
851 | <pre><code class="language-plaintext">$ mkdir untemplate/tutorial/ | [50/50] runMainBackground
852 |  | Watching for changes to 14 paths and 9 other values... (Enter to re-run, Ctrl-C to exit)
853 |  | Starting single-page webserver on interface '0.0.0.0', port 45612...
854 |  | </code></pre>
855 |  | <div class="note">
856 |  | <p>If we did not generate starter templates, we will have been using the compiled-in default untemplate to compose our items. We cannot modify that.</p>
857 |  | <p>But our feedletter installation directory contains a copy of this untemplate that we can deploy and tweak.</p>
858 |  | <p>To do so, we'll have to create a folder for this file under <code>untemplate</code>. We’ll call it <code>tutorial</code>. Then...</p>
859 |  | <pre><code class="language-plaintext">$ mkdir untemplate/tutorial/
860 | $ cp sample/defaultCompose.html.untemplate untemplate/tutorial/lgmCompose.html.untemplate | $ cp sample/defaultCompose.html.untemplate untemplate/tutorial/lgmCompose.html.untemplate
861 | </code></pre> | </code></pre>
862 | <p><code>feedletter</code> now has access to this untemplate, under a name you can find by calling <code>./feedletter list-untemplates</code>:</p> | <p><code>feedletter</code> now has access to this untemplate, under a name you can find by calling <code>./feedletter list-untemplates</code>:</p>
863 | <pre><code class="language-plaintext">$ ./feedletter list-untemplates | <pre><code class="language-plaintext">$ ./feedletter list-untemplates
864 | [42/49] compile | [42/49] compile
865 | [info] compiling 2 Scala sources to /home/feedletter/feedletter-local/out/compile.dest/classes ... | [info] compiling 2 Scala sources to /home/feedletter/feedletter-local/out/compile.dest/classes ...
866 | [info] done compiling | [info] done compiling
⋮ | ⋮ | ⋮
877 | ¦ tutorial.lgmCompose_html ¦ com.mchange.feedletter.style.ComposeInfo.Universal ¦ | ¦ tutorial.lgmCompose_html ¦ com.mchange.feedletter.style.ComposeInfo.Universal ¦
878 | +---------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+ | +---------------------------------------------------------------+-----------------------------------------------------------------------------------------------------+
879 | </code></pre> | </code></pre>
880 | <p>Now we can ask <code>feedletter-style</code> to show us what this post would look like using our "new" untemplate to render it:</p> | <p>Now we can ask <code>feedletter-style</code> to show us what this post would look like using our "new" untemplate to render it:</p>
881 | <pre><code class="language-plaintext">$ ./feedletter-style compose-single --subscribable-name lgm --untemplate-name tutorial.lgmCompose_html --port 45612 | <pre><code class="language-plaintext">$ ./feedletter-style compose-single --subscribable-name lgm --untemplate-name tutorial.lgmCompose_html --port 45612
882 | [50/50] runMainBackground | [50/50] runMainBackground
883 | Watching for changes to 14 paths and 9 other values... (Enter to re-run, Ctrl-C to exit) | Watching for changes to 14 paths and 9 other values... (Enter to re-run, Ctrl-C to exit)
884 | Starting single-page webserver on interface '0.0.0.0', port 45612... | Starting single-page webserver on interface '0.0.0.0', port 45612...
885 | </code></pre> | </code></pre>
886 | <p>Initially it looks exactly the same, because it is just a copy of the default untemplate!</p> | </div>
887 | <p>But now we can just modify that file, <code>untemplate/tutorial/lgmCompose.html.untemplate</code>, hit reload, and play!</p> | <p>Initially it will look exactly the same, because it is just a copy of the default untemplate!</p>
888 |  | <p>But now we can just modify our untemplate (either <code>untemplate/default/subscription/email/composeUniversal.html.untemplate</code> or <code>untemplate/tutorial/lgmCompose.html.untemplate</code>), hit reload, and play around with it!</p>
889 | <p>This is why we needed a second terminal window. We edit the template in one terminal while <code>./feedletter-style</code> is running in the other. After each edit and save, we hit reload to see our changes. (We may have to wait 10-15 secs!)</p> | <p>This is why we needed a second terminal window. We edit the template in one terminal while <code>./feedletter-style</code> is running in the other. After each edit and save, we hit reload to see our changes. (We may have to wait 10-15 secs!)</p>
890 | <div class="note"> | <div class="note">
891 | <p>Occasionally the autoreload glitches out, in which case you should manually <ctrl-c> and rerun your <code>./feedletter-style</code> command.</p> | <p>Occasionally the autoreload glitches out, in which case you should manually <ctrl-c> and rerun your <code>./feedletter-style</code> command.</p>
892 | <p>If you see error messages when you rerun, you may have hit compilation errors (an <a href="https://github.com/swaldman/untemplate-doc#readme">untemplate is transformed into a Scala source code</a>, which is then compiled), which you will have to resolve. You can <a href="mailto:swaldman@mchange.com">ask for help</a>!)</p> | <p>If you see error messages when you rerun, you may have hit compilation errors (an <a href="https://github.com/swaldman/untemplate-doc#readme">untemplate is transformed into a Scala source code</a>, which is then compiled), which you will have to resolve. You can <a href="mailto:swaldman@mchange.com">ask for help</a>!)</p>
893 | </div> | </div>
894 | <p>Our new untemplate has a section that looks like this:</p> | <p>Our untemplate has a section that looks like this:</p>
895 | <pre><code class="language-html"><html> | <pre><code class="language-html"><html>
896 | <head> | <head>
897 | <style> | <style>
⋮ | ⋮ | ⋮
934 | <p>Looks better!</p> | <p>Looks better!</p>
935 | <img alt="Screenshot of web-served lgm newsletter with a better laid-out image." src="scotus-disqualify-good-good-endline.png" style="width: 100%;"> | <img alt="Screenshot of web-served lgm newsletter with a better laid-out image." src="scotus-disqualify-good-good-endline.png" style="width: 100%;">
936 | <p>We can keep editing all we like. We add the <code>--random</code> flag and run our <code>./feedletter-style</code> command over and over to make sure that posts in general render well.</p> | <p>We can keep editing all we like. We add the <code>--random</code> flag and run our <code>./feedletter-style</code> command over and over to make sure that posts in general render well.</p>
937 | <p>When we are happy, we want to tell our subscription to use the new untemplate.</p> | <p>When we are happy, we are done, if we've been editing the starter untemplate at <code>untemplate/default/subscription/email/composeUniversal.html.untemplate</code>. (If not, see the note just below.)</p>
938 | <p>Remember, the name of the untemplate we've been editing was <code>tutorial.lgmCompose_html</code>.</p> | <div class="note">
939 | <p>Let's check that command out:</p> | <p>If we created a new untemplate called <code>tutorial.lgmCompose_html</code>, we'll need to to tell our subscription to use the new untemplate.</p>
940 | <pre><code class="language-plaintext">$ ./feedletter set-untemplates --help | <p>Remember, the name of the untemplate we've been editing was <code>tutorial.lgmCompose_html</code>.</p>
941 |  | <p>Let's check that command out:</p>
942 |  | <pre><code class="language-plaintext">$ ./feedletter set-untemplates --help
943 | [49/49] runMain | [49/49] runMain
944 | Usage: feedletter set-untemplates --subscribable-name <name> [--compose-untemplate <fully-qualified-name>] [--confirm-untemplate <fully-qualified-name>] [--removal-notification-untemplate <fully-qualified-name>] [--status-change-untemplate <fully-qualified-name>] | Usage: feedletter set-untemplates --subscribable-name <name> [--compose-untemplate <fully-qualified-name>] [--confirm-untemplate <fully-qualified-name>] [--removal-notification-untemplate <fully-qualified-name>] [--status-change-untemplate <fully-qualified-name>]
945 |  |
⋮ | ⋮ | ⋮
961 | 1 targets failed | 1 targets failed
962 | runMain subprocess failed | runMain subprocess failed
963 | </code></pre> | </code></pre>
964 | <p>Okay. So we run...</p> | <p>Okay. So we run...</p>
965 | <pre><code class="language-plaintext">$ ./feedletter set-untemplates --subscribable-name lgm --compose-untemplate tutorial.lgmCompose_html | <pre><code class="language-plaintext">$ ./feedletter set-untemplates --subscribable-name lgm --compose-untemplate tutorial.lgmCompose_html
966 | [49/49] runMain | [49/49] runMain
967 | Updated Subscription Manager: { | Updated Subscription Manager: {
968 | "composeUntemplateName": "tutorial.lgmCompose_html", | "composeUntemplateName": "tutorial.lgmCompose_html",
⋮ | ⋮ | ⋮
979 | "version": 1 | "version": 1
980 | } | }
981 | </code></pre> | </code></pre>
982 |  | </div>
983 | <p>And we are done! We have restyled our newsletter.</p> | <p>And we are done! We have restyled our newsletter.</p>
984 | <p>We could (and should!) do the same with our other subscriptions (using <code>./feedletter-style compose-multiple</code>). We could also do much more elaborate things then just mess with the stylesheet. Our compose untemplate was really the definition of a pretty arbitrary Scala function that accepted a <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Single</code></a> object and produced a String (embedded in an <code>untemplate.Result</code>).</p> | <p>We could (and should!) do the same with our other subscriptions (using <code>./feedletter-style compose-multiple</code>). We could also do much more elaborate things then just mess with the stylesheet. Our compose untemplate was really the definition of a pretty arbitrary Scala function that accepted a <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Single</code></a> object and produced a String (embedded in an <code>untemplate.Result</code>).</p>
985 | <p>Learn more about untemplates <a href="https://github.com/swaldman/untemplate-doc#readme">here</a>.</p> | <p>Learn more about untemplates <a href="https://github.com/swaldman/untemplate-doc#readme">here</a>.</p>
986 | <p>The default compose untemplate actually accepts a <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Universal</code></a>, a parent type of both <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Single</code></a> and <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Multiple</code></a>. So we can fix up the glitches we know about already in our <code>lgm-daily</code> subscribable just by setting for it the same compose untemplate:</p> | <p>The default compose untemplate actually accepts a <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Universal</code></a>, a parent type of both <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Single</code></a> and <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/core.scala"><code>ComposeInfo.Multiple</code></a>.</p>
987 | <pre><code class="language-plaintext">$ ./feedletter set-untemplates --subscribable-name lgm-daily --compose-untemplate tutorial.lgmCompose_html | <p>If we generated starter untemplates, our <code>lgm-daily</code> subscribable used the same compose untemplate we just edited by default. Run <code>./feedletter list-untemplates</code> to verify this.</p>
988 |  | <p>Let's take a look at what that subscription looks like. Because it shows multiple posts, we use the <code>compose-multiple</code> subcommand.</p>
989 |  | <pre><code class="language-plaintext">$ ./feedletter-style compose-multiple --subscribable-name lgm-daily --port 45612
990 |  | [50/50] runMainBackground
991 |  | Watching for changes to 14 paths and 9 other values... (Enter to re-run, Ctrl-C to exit)
992 |  | Starting single-page webserver on interface '0.0.0.0', port 45612...
993 |  | </code></pre>
994 |  | <p>We've already fixed our glitches in that subscription as well. It looks pretty good!</p>
995 |  | <div class="note">
996 |  | <p>If we did not <code>generate-starter-untemplates</code>, we still have to tell <code>lgm-daily</code> to use our new untemplate, just like we had to tell <code>lgm</code> before:</p>
997 |  | <pre><code class="language-plaintext">$ ./feedletter set-untemplates --subscribable-name lgm-daily --compose-untemplate tutorial.lgmCompose_html
998 | [49/49] runMain | [49/49] runMain
999 | Updated Subscription Manager: { | Updated Subscription Manager: {
1000 | "composeUntemplateName": "tutorial.lgmCompose_html", | "composeUntemplateName": "tutorial.lgmCompose_html",
⋮ | ⋮ | ⋮
1011 | "version": 1 | "version": 1
1012 | } | }
1013 | </code></pre> | </code></pre>
1014 | <p>If we take a look at that with <code>compose-multiple</code>..</p> | <p>If we take a look at that with <code>compose-multiple</code>..</p>
1015 | <pre><code class="language-plaintext">$ ./feedletter-style compose-multiple --subscribable-name lgm-daily --port 45612 | <pre><code class="language-plaintext">$ ./feedletter-style compose-multiple --subscribable-name lgm-daily --port 45612
1016 | [50/50] runMainBackground | [50/50] runMainBackground
1017 | Watching for changes to 14 paths and 9 other values... (Enter to re-run, Ctrl-C to exit) | Watching for changes to 14 paths and 9 other values... (Enter to re-run, Ctrl-C to exit)
1018 | Starting single-page webserver on interface '0.0.0.0', port 45612... | Starting single-page webserver on interface '0.0.0.0', port 45612...
1019 | </code></pre> | </code></pre>
1020 | <p>We'll find that it looks pretty good!</p> | <p>We'll find that it looks pretty good!</p>
1021 |  | </div>
1022 | <h2><a href="#16-advanced-customize-the-content" id="16-advanced-customize-the-content" name="16-advanced-customize-the-content" class="anchorlink"></a>16. Advanced: Customize the content</h2> | <h2><a href="#16-advanced-customize-the-content" id="16-advanced-customize-the-content" name="16-advanced-customize-the-content" class="anchorlink"></a>16. Advanced: Customize the content</h2>
1023 | <p><em>feedletter</em> supports a variety of <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/Customizer.scala">customizers</a>, including</p> | <p><em>feedletter</em> supports a variety of <a href="https://github.com/swaldman/feedletter/blob/main/src/com/mchange/feedletter/style/Customizer.scala">customizers</a>, including</p>
1024 | <ul> | <ul>
⋮ | ⋮ | ⋮
1143 | <div class="entry-footer"> | <div class="entry-footer">
1144 | <div class="post-metainfo"> | <div class="post-metainfo">
1145 | <div class="updated-note"> | <div class="updated-note">
1146 | <a href="index.html#major-updates">Last major update at 2024-06-20 @ 01:10 PM EDT</a> | <a href="index.html#major-updates">Last major update at 2026-06-11 @ 12:30 PM EDT</a>
1147 | </div> | </div>
1148 | <div> | <div>
1149 | <a href="index.html" class="pubtime">10:30 AM EST</a> | <a href="index.html" class="pubtime">10:30 AM EST</a>
⋮ | ⋮ | ⋮
1166 | <div id="update-history" class="update-history"> | <div id="update-history" class="update-history">
1167 | <h3 class="update-history-title"><a id="major-updates" href=""></a>Major revisions:</h3> | <h3 class="update-history-title"><a id="major-updates" href=""></a>Major revisions:</h3>
1168 | <ul> | <ul>
1169 | <li><span class="update-timestamp"><i>2024-06-20 @ 01:10 PM EDT</i></span> — Add note to Section 16, "Advanced: Customize the content" documenting <i>feedletter</i> API changes that slightly modify this section of the tutorial. (<a href="index-diff-acb84c08c1bbcc83036b9cad70f6a3c5d60865c4-to-current.html">diff</a>)</li> | <li><span class="update-timestamp"><i>2026-06-11 @ 12:30 PM EDT</i></span> — Modify the tutorial to encourage use of new './feedletter generate-starter-untemplates', and document by default in terms of those. (<a href="index-diff-e55882f63ed583549888ac31d22c7fae968d7359-to-current.html">diff</a>)</li>
1170 |  | <li><span class="update-timestamp"><i> <a href="index-oldcommit-e55882f63ed583549888ac31d22c7fae968d7359.html">2024-06-20 @ 01:10 PM EDT</a></i></span> — Add note to Section 16, "Advanced: Customize the content" documenting <i>feedletter</i> API changes that slightly modify this section of the tutorial. (<a href="index-diff-acb84c08c1bbcc83036b9cad70f6a3c5d60865c4-to-e55882f63ed583549888ac31d22c7fae968d7359.html">diff</a>)</li>
1171 | <li><span class="update-timestamp"><i> <a href="index-oldcommit-acb84c08c1bbcc83036b9cad70f6a3c5d60865c4.html">2024-01-29 @ 10:30 AM EST</a></i></span> — Initial publication.</li> | <li><span class="update-timestamp"><i> <a href="index-oldcommit-acb84c08c1bbcc83036b9cad70f6a3c5d60865c4.html">2024-01-29 @ 10:30 AM EST</a></i></span> — Initial publication.</li>
1172 | </ul> | </ul>
1173 | <div class="update-history-note"> | <div class="update-history-note">
1174 | Timestamps represent "major", substantative revisions. There may have been subsequent typo fixes and language reworkings within a major revision, after the time displayed. For a more complete and fine-grained update history, you can view the <a href="https://github.com/swaldman/tech.interfluidity.com/commits/main/">git repository commit history</a>. The most recent minor modification of this entry occurred 2025-01-14 @ 04:05 PM EST. | Timestamps represent "major", substantative revisions. There may have been subsequent typo fixes and language reworkings within a major revision, after the time displayed. For a more complete and fine-grained update history, you can view the <a href="https://github.com/swaldman/tech.interfluidity.com/commits/main/">git repository commit history</a>. The most recent minor modification of this entry occurred 2026-06-12 @ 02:39 PM EDT.
1175 | </div> | </div>
1176 | </div> | </div>
1177 | </div> | </div>
⋮ | ⋮ | ⋮
1182 | </div> | </div>
1183 | </body> | </body>
1184 | </html> | </html>