Devlog: wiring Day Poetry into Twine


In December 2023, ten months after completing Day Poetry, I started learning Twine for a portfolio project. Even before then, I had a vague desire to make a version of Day Poetry that was more accessible than downloading and reading from a PDF. But that desire on its own wasn't enough to start this project. It wasn't until I spent a year on and off in Harlowe that I formed a real goal for that "more accessible" version that couldn't be done in a PDF: letting readers get to the poem of the day in a single click.

Quick Guide

This dev log is for both people unfamiliar with Twine, the program I used to create this project, and people familiar with the program. For those who may be unfamiliar, these are the terms and concepts I mention:

  • Harlowe: One of Twine's "story formats," and the one I used for this project. By the succinct description in the Twine Cookbook, "Each story format provides a different visual layout, set of macros, and internal JavaScript functionality." All code snippets in this log are compatible with Harlowe; I can't vouch for any other story format.
  • Macro: These are bits of code baked into Twine. In Harlowe, they look something like this: (macro-name:). Macros can do many different things, and they are the primary way we program in Twine.
  • Passage: These are essentially pages in Twine. Some of them, like the poem passages, are visible to the reader, while others are invisible and serve to hold and run code. All passages have names: this is akin to the names of webpages, and naming passages similarly allows programmers to link between them.
    • Startup- and header-tagged passages: Special passages that have been tagged so that Twine knows that the content in them should be handled in a specific way. Startup-tagged passages run as soon as the project starts running (that is, at startup), and they're useful for things like setting variables that will be used for styling in the rest of the project. Header-tagged passages take advantage of the fact that Twine reads passages from top to bottom, so anything in a header-tagged passage will be read (and run) before anything else in a passage. This type of passage is good for things like my navbar, which is used in every (or almost every) passage.
  • Variables: In code, a variable is a box for holding something else. These boxes are given names that can then be called upon to display, run, or read whatever they're holding. In Harlowe, this name looks like $variableName. Variables can hold anything from a line of text to a string of code; this is described as "setting" the variable (like setting something in a box or setting an alarm clock).

Technical example: If I want to let the reader click a button to go from the passage "Home" to the passage "About," I could use the macro (link-goto: "Learn more!", "About"). To add some styling to that button, I could set a variable, $buttonStyle, using the macro (set: $buttonStyle to (b4r: 'solid')+(corner-radius: 6)), and then attach it to the click-goto macro like so: $buttonStyle[(link-goto: "Learn more!", "About")]. That would give the button a "solid" border outline with rounded edges like the "similar" button in Day Poetry: in Twine.

Setting the date

To take the reader to today's poem, the project reads the current date based on the reader's system settings and directs them to a separate passage dependent on the current month (there's a passage for January, February, March, etc.) where they're handed off to the correct poem passage depending on the day of the month. I use separate passages for these actions - one to set and format the date, one for the month directory, and one for each month - to keep things tidy, but they could all be executed in the same passage.

The date is set in a startup-tagged passage using (set: $date to (current-date:)), which uses the (current date:) macro to set the variable $date. By default, (current date:) is formatted to include the day of the week, the day of the month, the month, and the year, e.g., Fri Apr 11 2025. Because I only need the month and day of the month, I used this block of code to trim the information that (current-date:) places in $date:

(set: $date to (trimmed: (p-end: ...digit), $date))
(set: $date to (trimmed: (p-start: "Sun"), $date))
(set: $date to (trimmed: (p-start: "Mon"), $date))
(set: $date to (trimmed: (p-start: "Tue"), $date))
(set: $date to (trimmed: (p-start: "Wed"), $date))
(set: $date to (trimmed: (p-start: "Thu"), $date))
(set: $date to (trimmed: (p-start: "Fri"), $date))
(set: $date to (trimmed: (p-start: "Sat"), $date))
(set: $date to (trimmed: (p-start: whitespace), $date))
(set: $date to (trimmed: (p-end: whitespace), $date))

That way, $date renders as, e.g., Apr 11. This allows me to use "contains" in the passage "Month director" to take the reader to the correct month passage, e.g.:

(if: $date contains "Jan")[(go-to: "January")]
(else-if: $date contains "Feb")[(go-to: "February")]
(else-if: $date contains "Mar")[(go-to: "March")]

Instead of using "contains" in the month passages, I call for the exact date, e.g.:

(if: $date is "Apr 01")[(go-to: "April 1, 2022")]
(else-if: $date is "Apr 02")[(go-to: "April 2, 2022")]
(else-if: $date is "Apr 03")[(go-to: "April 3, 2022")]

"Contains" may work equally well, especially after trimming $date, but I chose this method to avoid potential bugs, e.g., (if: $date contains "Apr 01") taking the reader to the April 10 passage.

As shown in the example above, the month passages use (if:)/(else-if:) and (go-to:) macros to send the reader to the correct passage. This is uniform for all month passages except February. Day Poetry has a week of overlap for the month of February - there are two Feb 22 poems, two Feb 23 poems, etc. To deal with this, if $date is anywhere (anywhen?) from the 22nd to the 28th, the reader is quietly sent to another passage, "Last Feb Week." This passage displays the text, "There are two poems for today," and links to both poems for that day using (if:) and (else-if:). 2022 and 2023 weren't leap years, but I made a separate Feb 29 passage for future leap day readers.

Going to a random poem

After creating the poem-of-the-day feature, my second wishlist item was taking readers to a random poem in a single click. It wasn't immediately clear to me how to do this, and I tried a few things before discovering what was, in hindsight, a fairly straightforward method.

To maximize code reuse, I piggybacked off what I'd written for setting and reading the date. In a passage called "Random," a dice roll is simulated: first, a 12-sided die decides the month, then a 31-sided die decides the day. As an example, here's some of the code for deciding the month:

(set: $firstRoll to (random: 1, 12))
(if: $firstRoll is 1)[(set: $date to "Jan ")]
(else-if: $firstRoll is 2)[(set: $date to "Feb ")]
(else-if: $firstRoll is 3)[(set: $date to "Mar ")]

Note the whitespace after the month abbreviation. This is followed by the 31-sided die roll, which looks like:

(set: $secondRoll to (random: 1, 31))
(if: $secondRoll is 1)[(set: $date to it + "01")]
(else-if: $secondRoll is 2)[(set: $date to it + "02")]
(else-if: $secondRoll is 3)[(set: $date to it + "03")]

Using "it +" adds the number onto the already-set month; without it, $date would be overwritten to the number, e.g., instead of setting to "Jan 01," "Jan " would be overwritten to "01" only.

With $date set, the reader is then sent to "Month director" and redirected accordingly. Of course, this method creates dates like "Apr 31" and "Feb 30." My original solution to this was to add a line to month pages to direct those dates to the last poem of that month, e.g., Apr 31 goes to Apr 30. Now, I use this chunk of code, placed after the above (set:) + (if:) + (else-if:)s:

(if: $date is "Feb 29")[(go-to: "Random reroll")]
(else-if: $date is "Feb 30")[(go-to: "Random reroll")]
(else-if: $date is "Feb 31")[(go-to: "Random reroll")]
(else-if: $date is "Apr 31")[(go-to: "Random reroll")]
(else-if: $date is "June 31")[(go-to: "Random reroll")]
(else-if: $date is "Sep 31")[(go-to: "Random reroll")]
(else-if: $date is "Nov 31")[(go-to: "Random reroll")]
(else: )[(go-to: "Month director")]

The only code in the "Random reroll" passage is (go-to: "Random"). That way, the reader is redirected to the randomizer, effectively re-rolling the dice whenever their previous roll resulted in an odd date.

Adding tags

I added the tagging system late in development: it wasn't until after inputting and formatting all of the poems that I committed to adding tags. The idea occurred to me much earlier, but I was reluctant to further "curate" the collection by assigning tags to poems; the original reading experience for Day Poetry has very little commentary. However, Twine presents a new mode of travel through the collection, and tags complement that new mode.

The silver lining to tagging the poems after formatting them was that, with my memory of them newly refreshed, I had a good idea of what tags would cover most of the poems. "Happenstance," "stars," and "the sea" were late additions to the list; "fears and doubts" was added to cover the last untagged stragglers; "laughter" was added in the last update before this devlog. I didn't want the tag list to be exhaustive. Rather, I want it to reflect the timbre and mood of the collection.

Code-wise, tags are passage links with the hashtags rendered using the &ㅤ#35ㅤ; character code since Twine reads the pound sign/hashtag as markdown for formatting a header. The special "similar" tag uses either a direct passage link or, for linking to multiple similar poems, an (either:) macro nested in a (link-goto:) macro. An example:

(link-goto: "similar", (either: "October 21, 2022", "January 25, 2023"))

The styling for the similar tag is stored in a variable in my startup-tagged passage; that way, I don't have to copy-paste it over and over, and I only have to change the code in the startup-tagged passage if I want to update the styling throughout.

Content warnings

In individual volumes of Day Poetry, I include loose lists of potentially sensitive content on the opening page. This is a somewhat lax approach to content warnings, one that kept with treating those volumes as traditional books of poetry. Using Twine gave me more flexibility for balancing the reading experience with flagging content.

My original draft for content warnings included a precise list of checkboxes in the "Content" passage. Readers could select content they wanted flagged, and poems with that content would have the ⚠ warning description at the top. I felt this method provided readers with too little context, despite the precise content warning list. My lesser concern was that two poems with the same flagged content could have wildly different "intensities," but they would be treated the same using this flagging system. My greater concern was that this method placed a certain onus on the reader: they would have to preemptively know what they want to be warned about, perhaps even before reading any of the collection. The checkboxes in Twine are also finicky: they appear unchecked once the reader leaves or refreshes the passage, though the variables they set remain active.

I retained the checkbox opt-in method for censoring swear words and usages of G-d. In the "Content" passage, that code is set with:

(checkbox: bind $gd, " Always show it as G-d in any context")
(checkbox: bind $swear, " Censor swear words.") Choose how they will appear in-text: (dropdown: 2bind $cuss, "One (H*ll)", "Mostly (H░░░)", "Full (░░░░)")

Checking the "Censor swear words" sets the variable $swear to "true" so that Twine knows the reader wants those censored, and then a second variable, $cuss, is set using a dropdown menu that decides the visual styling of the censor. As an aside, swears are censored with asterisks, but I used block elements in the display options because Twine would otherwise try to read the multiple asterisks as formatting and cause an error. The styling runs in-passage, so the code on relevant pages looks something like:

(if: $gd is true)[(replace: "God")[G-d]]
(if: $swear is true)[
(replace: "damn")[
(if: $cuss is "One (H*ll)")[d*mn]
(else-if: $cuss is "Mostly (H░░░)")[d&astㅤ;&astㅤ;&astㅤ;]
(else-if: $cuss is "Full (░░░░)")[&astㅤ;&astㅤ;&astㅤ;&astㅤ;]
]
]

Note the use of the character code for the asterisks. The final draft of the content warning system is accomplished completely in-passage: poems with potentially sensitive content have a click-to-reveal link at the top that the reader can choose to interact with or ignore on a case-by-case basis. I originally used (click-replace:) macros for these links like this:

This would be placed at the top of the passage: (text-color: yellow)[⚠ Show]
The poem would be here. Then, at the bottom of the passage...
(click-replace: "⚠ Show")[(text-color:yellow)[⚠ This poem discusses ''drinking alcohol''. $skip[ [[Skip it?->December 29, 2022]] ]] ]

The $skip variable held my color styling for the "Skip it?" link. I swapped my (click-replace:) macros for (link:)s after incidentally reading that the (click:) macros must read the entire contents of a passage to work, potentially causing visual bugs. The result of the switch was placing this at the top of poem passages, under the navbar:

(link-style: (text-color: yellow))[(link: "⚠ Show")[(text-color: yellow)[⚠ This poem discusses ''drinking alcohol''.] (link-style: (text-color: #CD8A28))[ [[Skip it?->December 29, 2022]] ]]]

My generally untidy coding aside, this series of macros is a little less tidy than using (click-replace:) due to wanting to control two separate colors. Otherwise, this mode is functionally the same as the other mode and lets readers choose to interact with the warnings as they appear rather than having to decide ahead of time and on an entirely different page.

Styling

Going into this project, I had basic knowledge of HTML and even less knowledge of CSS. Because I was reluctant to mess with code I didn't understand, I started out using as little HTML and CSS as I could, beyond what Harlowe controls through macros. While I'm largely satisfied with the systems I made for the poem-of-the-day, random-poem, and tags, there is much I'd like to improve in the overall style of the project.

Formatting poems

One of my requirements for translating the collection into Twine was faithfully formatting the individual poems, particularly spacing and alignment. Using Tab to control spacing in my word processor was simpler than recreating that same spacing in Twine. Without using CSS, my solutions were largely uses of brute force: I used the &ㅤensp; character paired with regular space and the whitespace collapsing { } to line up text. At first, I stored certain amounts of &ㅤensp; in variables housed in my startup-tagged passage, but, where my alignment needs grew more specific, I ended up using those variables, plus raw &ㅤensp; and &ㅤemsp;, plus regular space, plus whitespace collapsing. This creates inaccurate spacing on mobile and other smaller screens, something I thought about even as far back as when I was writing the original Day Poetry.

Image: Comparison of the in-program code (top left) to a wide screen (bottom left) and small screen (right) output.

December 10, 2022 is an example of a poem that I was able to optimize better by using column formatting.

But it is a lone example; using column formatting in other poems with similar column structures, e.g., August 31, 2022, skews the poems for wide-screen readers.

Ultimately, my formatting and styling choices favor wider screens, then and now. I say that while acknowledging that my skills are what limited my options when translating the collection into Twine. This is an area I'd like to improve in.

Color palette

I drafted a handful of color schemes for the project before settling on the current one.

Image: Discarded WIP color schemes; "How to read" shows unclicked link hover color, "Overview" shows clicked link color, and "Content" shows clicked link hover color.

I chose the green-gold-grey color palette to evoke the same atmosphere as the cover image of Day Poetry without color-picking from the image. I originally wanted to let readers select a color scheme from a list, but, after a period of research and testing, I concluded that this was either impossible to achieve in Twine/Harlowe or simply beyond my skillset.

The code that controls the colors of individual elements goes in Story > Stylesheet:

tw-story {
background:#37403F;
color:white;
min-width: 100%;
}

tw-link {
color:#CD8A28;
}

.enchantment-link:hover, tw-link:hover {
color: #A49D7B;
}

.visited {
color: #8F7243;
.visited:hover {
color: #706A5B;
}

The first element decides the passage backgrounds; the second is the default link color; the third is the color of links when they're hovered over; the fourth is for links that have been clicked; the fifth is the color of already-clicked links when they're hovered over.

Floating navbar

Redesigning the navbar was the last component of updating the UI with mobile and smaller screens in mind. For most of the project, the navbar was a flat feature at the top of passages (and at the bottom of some longer poems), and its contents were much the same as they are now. The earliest draft had arrows next to Previous poem and Next poem, and there were no star/diamond spacers, just whitespace. It wasn't until I was researching the (box:) and (float-box:) macros that it occurred to me that a floating or sticky navbar was possible, albeit with much handling of CSS.

There was, in fact, very much handling of CSS. I went through several design iterations before managing the final result; and candidly, I came very close to giving up and returning to the original version on several of those occasions.

Code-wise, I pulled from two sources: a code snippet from Josh Grams on the IntFiction forums and another from Greyelf on Reddit. In my Story Stylesheet, I used:

tw-expression[name="float-box"] ~ tw-hook {
background-color: rgba(55, 64, 63, 0) !important;
overflow-y: inherit !important;
position:fixed;
z-index: 2;
height: 10vh !important;
}

tw-include[type="header"] {
width: 100vw;
height: 100vh;
left: 0;
top: 0;
z-index: 2;
margin: 0;
padding: 0;
}

In that same post on Reddit, Greyelf warns against overusing !important, but, as mentioned, I used crude solutions (the steamroller, if you will) where I lacked fine understanding. My use of !important here was to overcome baked-in formatting that I couldn't otherwise find a way around. Likewise, some of this code may be redundant without my knowing it: I had some issues that I solved by adding bits and pieces until the issues apparently stopped being issues.

The code in my stylesheet goes with the following code in a header-tagged passage called "navbar:"

(unless: (passage:)'s tags contains "no-header")[ (css: "text-align:center;") + (float-box: "X","Y=====")[(css: "background: rgb(55, 64, 63);")+(b4r:'solid')+(corner-radius: 6)+(text-size: 0.9)[navbar content] ] ]

Any passage I didn't want to use the navbar in, I added the no-header tag to. All passages that use the navbar have a (replace: "navbar content")[I put the links I want in the navbar here.] at the top. I used this method because all pages that use the navbar have slightly different links in their navbar. If there were a universal navbar, e.g., a link to "Home," "About," and "Random," then I could simply put those links in the "navbar" passage and be done with it.

Closing thoughts

It took about three months of on-and-off work between starting and finishing this project. There are a few things I'd like to add to it - a bookmarking/favoriting system, better mobile optimization - in the future, if possible, but I consider this a successful conclusion to Day Poetry. I plan to keep using and learning Twine, and I've just about convinced myself to try out another story format.

As for poetry writing...I'm not sure. I'm certainly writing poetry, but making large collections isn't intuitive to me. I hardly realize when I've begun and never know when it's ending. Don't even get me started on editing.

Thank you to everyone who has supported me during this project, directly and indirectly; I extend this thanks forward through time to any who wander here. Part of why I wrote (and shared) this devlog was to give other devs ideas for their own projects and to maybe encourage more people to make projects like this one.

References

The materials I lifted directly from while building this project, some of which I mention in this devlog.

Leave a comment

Log in with itch.io to leave a comment.