├── .eleventy.js ├── .eleventyignore ├── .forestry └── settings.yml ├── .gitattributes ├── .gitignore ├── .nvmrc ├── 404.md ├── LICENSE ├── LICENSE.txt ├── README.md ├── _data └── site.json ├── _includes ├── assets │ ├── css │ │ ├── 404.css │ │ └── inline.css │ ├── images │ │ └── github.svg │ ├── js │ │ ├── 404.js │ │ ├── inline.js │ │ └── search.js │ └── utils │ │ ├── image.txt │ │ └── imagesize.txt ├── components │ ├── contact.njk │ ├── footer.njk │ ├── head.njk │ ├── header.njk │ └── nav.njk ├── experimental │ └── blog │ │ ├── author.njk │ │ ├── authors.njk │ │ ├── components │ │ ├── pageslist.njk │ │ └── postslist.njk │ │ ├── content │ │ └── posts │ │ │ ├── firstpost.md │ │ │ ├── fourthpost.md │ │ │ ├── posts.json │ │ │ ├── secondpost.md │ │ │ ├── the-fifth-and-hopefully-final-example-post.md │ │ │ └── thirdpost.md │ │ ├── layouts │ │ ├── blog.njk │ │ └── post.njk │ │ ├── pages │ │ └── blog.md │ │ └── tags.njk └── layouts │ ├── 404.njk │ ├── base.njk │ ├── contact.njk │ ├── page.njk │ └── robots.njk ├── admin ├── config.yml ├── index.html └── preview-templates │ ├── index.js │ ├── page.js │ └── post.js ├── content ├── buy.md ├── images │ └── hello.jpg └── pages │ ├── contact.md │ ├── home.md │ └── pages.json ├── filters └── searchFilter.js ├── loader.js ├── manuscript ├── Book.txt ├── manuscript.json ├── process.json ├── process.md ├── rules.json ├── rules.md ├── why.json └── why.md ├── netlify.toml ├── outline.md ├── package.json ├── password_template.html ├── postcss.config.js ├── robots.md ├── search-index.json.njk ├── styles ├── tailwind.config.js └── tailwind.css └── uploads └── uws2.png /.eleventy.js: -------------------------------------------------------------------------------- 1 | const { DateTime } = require("luxon"); 2 | const CleanCSS = require("clean-css"); 3 | const UglifyJS = require("uglify-es"); 4 | const htmlmin = require("html-minifier"); 5 | const svgContents = require("eleventy-plugin-svg-contents"); 6 | const mdIterator = require('markdown-it-for-inline') 7 | const embedEverything = require("eleventy-plugin-embed-everything"); 8 | const pluginTOC = require('eleventy-plugin-nesting-toc'); 9 | const eleventyNavigationPlugin = require("@11ty/eleventy-navigation"); 10 | const Image = require("@11ty/eleventy-img"); 11 | const fs = require('fs'); 12 | module.exports = function(eleventyConfig) { 13 | // eleventyConfig.addPlugin(pluginTOC); 14 | eleventyConfig.addPlugin(svgContents); 15 | eleventyConfig.addPlugin(embedEverything); 16 | eleventyConfig.addShortcode("version", function () { 17 | return String(Date.now()); 18 | }); 19 | 20 | // Responsive image shortcode 21 | eleventyConfig.addLiquidShortcode("image", async function(src, alt, sizes = "100vw") { 22 | if(alt === undefined) { 23 | // You bet we throw an error on missing alt (alt="" works okay) 24 | throw new Error(`Missing \`alt\` on responsiveimage from: ${src}`); 25 | } 26 | src = './content/images/'+src 27 | let metadata = await Image(src, { 28 | widths: [400, 600, 800, 1000, 1200, 1400, 1600, 1900], 29 | formats: ['webp', 'jpeg', 'png'], 30 | urlPath: "/content/images/", 31 | outputDir: "./_site/content/images/" 32 | }); 33 | 34 | let lowsrc = metadata.jpeg[0]; 35 | 36 | let picture = ` 37 | ${Object.values(metadata).map(imageFormat => { 38 | return ` `; 39 | }).join("\n")} 40 | ${alt} 45 | `; 46 | 47 | return `${picture}`; 48 | 49 | }); 50 | 51 | eleventyConfig.addLiquidShortcode("icon", function(title,url) { 52 | return ''+title+''; 53 | }); 54 | 55 | // Button shortcode -- experimental 56 | // eleventyConfig.addLiquidShortcode("button", function(title,url) { 57 | // return ''+title+''; 58 | // }); 59 | 60 | 61 | // Tailwind pass through and watch target 62 | eleventyConfig.addWatchTarget("./_tmp/style.css"); 63 | eleventyConfig.addPassthroughCopy({ "./_tmp/style.css": "./style.css" }); 64 | 65 | // Alpine.js pass through 66 | eleventyConfig.addPassthroughCopy({ 67 | "./node_modules/alpinejs/dist/alpine.js": "./js/alpine.js", 68 | }); 69 | 70 | // Eleventy Navigation https://www.11ty.dev/docs/plugins/navigation/ 71 | eleventyConfig.addPlugin(eleventyNavigationPlugin); 72 | 73 | // Configuration API: use eleventyConfig.addLayoutAlias(from, to) to add 74 | // layout aliases! Say you have a bunch of existing content using 75 | // layout: post. If you don’t want to rewrite all of those values, just map 76 | // post to a new file like this: 77 | // eleventyConfig.addLayoutAlias("post", "layouts/my_new_post_layout.njk"); 78 | 79 | // Merge data instead of overriding 80 | // https://www.11ty.dev/docs/data-deep-merge/ 81 | eleventyConfig.setDataDeepMerge(true); 82 | 83 | // Add support for maintenance-free post authors 84 | // Adds an authors collection using the author key in our post frontmatter 85 | // Thanks to @pdehaan: https://github.com/pdehaan 86 | // eleventyConfig.addCollection("authors", collection => { 87 | // const blogs = collection.getFilteredByGlob("posts/*.md"); 88 | // return blogs.reduce((coll, post) => { 89 | // const author = post.data.author; 90 | // if (!author) { 91 | // return coll; 92 | // } 93 | // if (!coll.hasOwnProperty(author)) { 94 | // coll[author] = []; 95 | // } 96 | // coll[author].push(post.data); 97 | // return coll; 98 | // }, {}); 99 | // }); 100 | 101 | // Creates custom collection "pages" 102 | eleventyConfig.addCollection("pages", function(collection) { 103 | return collection.getFilteredByGlob("pages/*.md"); 104 | }); 105 | 106 | // Creates custom collection "posts" 107 | // eleventyConfig.addCollection("posts", function(collection) { 108 | // const coll = collection.getFilteredByGlob("posts/*.md"); 109 | 110 | // for(let i = 0; i < coll.length ; i++) { 111 | // const prevPost = coll[i-1]; 112 | // const nextPost = coll[i + 1]; 113 | 114 | // coll[i].data["prevPost"] = prevPost; 115 | // coll[i].data["nextPost"] = nextPost; 116 | // } 117 | 118 | // return coll; 119 | // }); 120 | 121 | 122 | // Creates custom collection "results" for search 123 | const searchFilter = require("./filters/searchFilter"); 124 | eleventyConfig.addFilter("search", searchFilter); 125 | eleventyConfig.addCollection("results", collection => { 126 | return [...collection.getFilteredByGlob("**/*.md")]; 127 | }); 128 | 129 | // Creates custom collection "menuItems" 130 | eleventyConfig.addCollection("menuItems", collection => { 131 | const coll = collection 132 | .getFilteredByGlob("manuscript/*.md") 133 | .sort((a, b) => { 134 | return (a.data.eleventyNavigation.order || 0) - (b.data.eleventyNavigation.order || 0); 135 | }) 136 | return coll; 137 | }); 138 | 139 | // Date formatting (human readable) 140 | eleventyConfig.addFilter("readableDate", dateObj => { 141 | return DateTime.fromJSDate(dateObj).toFormat("LLL dd, yyyy"); 142 | }); 143 | 144 | // Date formatting (machine readable) 145 | eleventyConfig.addFilter("machineDate", dateObj => { 146 | 147 | return DateTime.fromJSDate(dateObj).toFormat("yyyy-MM-dd"); 148 | }); 149 | 150 | // Minify CSS 151 | eleventyConfig.addFilter("cssmin", function(code) { 152 | return new CleanCSS({}).minify(code).styles; 153 | }); 154 | 155 | // Minify JS 156 | eleventyConfig.addFilter("jsmin", function(code) { 157 | let minified = UglifyJS.minify(code); 158 | if (minified.error) { 159 | console.log("UglifyJS error: ", minified.error); 160 | return code; 161 | } 162 | return minified.code; 163 | }); 164 | 165 | // Minify HTML output 166 | eleventyConfig.addTransform("htmlmin", function(content, outputPath) { 167 | if (outputPath.indexOf?.(".html") > -1) { 168 | let minified = htmlmin.minify(content, { 169 | useShortDoctype: true, 170 | removeComments: true, 171 | collapseWhitespace: true 172 | }); 173 | return minified; 174 | } 175 | return content; 176 | }); 177 | 178 | // Don't process folders with static assets e.g. images 179 | eleventyConfig.addPassthroughCopy("favicon.ico"); 180 | eleventyConfig.addPassthroughCopy("images/") 181 | eleventyConfig.addPassthroughCopy("content/images/") 182 | eleventyConfig.addPassthroughCopy("admin"); 183 | eleventyConfig.addPassthroughCopy("_includes/assets/"); 184 | eleventyConfig.addPassthroughCopy("_includes/experimental/"); 185 | 186 | /* Markdown Plugins */ 187 | let markdownIt = require("markdown-it"); 188 | let markdownItAnchor = require("markdown-it-anchor"); 189 | let markdownItEmoji = require("markdown-it-emoji"); 190 | let markdownItFootnote = require("markdown-it-footnote"); 191 | let markdownItContainer = require("markdown-it-container"); 192 | let markdownLinkifyImages = require('markdown-it-linkify-images') 193 | let markdownToc = require('markdown-it-table-of-contents') 194 | let markdownItTasks = require('markdown-it-task-lists') 195 | let markdownItAttrs = require("markdown-it-attrs") 196 | let markdownItCenterText = require("markdown-it-center-text") 197 | let options = { 198 | html: true, 199 | breaks: false, 200 | linkify: true, 201 | typographer: true 202 | }; 203 | let opts = { 204 | // permalink: true, 205 | // permalinkClass: "direct-link", 206 | // permalinkSymbol: "#" 207 | }; 208 | 209 | eleventyConfig.setLibrary("md", markdownIt(options) 210 | .use(mdIterator, 'url_new_win', 'link_open', function (tokens, idx) { 211 | const [attrName, href] = tokens[idx].attrs.find(attr => attr[0] === 'href') 212 | if (href && (!href.includes('franknoirot.co') && !href.startsWith('/') && !href.startsWith('#'))) { 213 | tokens[idx].attrPush([ 'target', '_blank' ]) 214 | tokens[idx].attrPush([ 'rel', 'noopener noreferrer' ]) 215 | } 216 | }) 217 | .use(markdownItAnchor, opts) 218 | .use(markdownItEmoji) 219 | .use(markdownItFootnote) 220 | .use(markdownItContainer, 'callout') 221 | .use(markdownItContainer, 'callout-blue') 222 | .use(markdownItContainer, 'callout-pink') 223 | .use(markdownItContainer, 'callout-green') 224 | .use(markdownItContainer, 'warning') 225 | .use(markdownItTasks) 226 | .use(markdownItCenterText) 227 | .use(markdownLinkifyImages, { 228 | imgClass: "p-4", 229 | }) 230 | .use(markdownItAttrs, { 231 | includeLevel: [2,3], 232 | listType: "ol" 233 | }) 234 | ); 235 | 236 | return { 237 | templateFormats: ["md", "njk", "html", "liquid"], 238 | 239 | // If your site lives in a different subdirectory, change this. 240 | // Leading or trailing slashes are all normalized away, so don’t worry about it. 241 | // If you don’t have a subdirectory, use "" or "/" (they do the same thing) 242 | // This is only used for URLs (it does not affect your file structure) 243 | pathPrefix: "/", 244 | markdownTemplateEngine: "liquid", 245 | htmlTemplateEngine: "njk", 246 | dataTemplateEngine: "njk", 247 | dir: { 248 | input: ".", 249 | includes: "_includes", 250 | data: "_data", 251 | output: "_site" 252 | } 253 | }; 254 | }; -------------------------------------------------------------------------------- /.eleventyignore: -------------------------------------------------------------------------------- 1 | README.md 2 | .github/ 3 | functions/* 4 | functions/ 5 | -------------------------------------------------------------------------------- /.forestry/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | new_page_extension: md 3 | auto_deploy: false 4 | admin_path: '' 5 | webhook_url: 6 | sections: 7 | - type: directory 8 | path: '' 9 | label: Test 10 | create: all 11 | match: "**/*" 12 | upload_dir: uploads 13 | public_path: "/uploads" 14 | front_matter_path: '' 15 | use_front_matter_path: true 16 | file_template: ":filename:" 17 | build: 18 | preview_env: 19 | - ELEVENTY_ENV=dev 20 | preview_output_directory: _site 21 | install_dependencies_command: npm install 22 | preview_docker_image: forestryio/node:12 23 | mount_path: "/srv" 24 | working_dir: "/srv" 25 | instant_preview_command: npx @11ty/eleventy --serve 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | 19 | 20 | _site/ 21 | node_modules/ 22 | package-lock.json 23 | .idea 24 | .vscode 25 | *~ 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | node_modules/ 3 | package-lock.json 4 | .idea 5 | .vscode 6 | *~ 7 | .cache 8 | _tmp -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14 2 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 404 3 | permalink: /404.html 4 | layout: layouts/404.njk 5 | --- 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tim Broeker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Creative Commons 2 | Attribution-NonCommercial-NoDerivs 3.0 Unported 3 | 4 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. 5 | 6 | License 7 | 8 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. 9 | 10 | BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 11 | 12 | 1. Definitions 13 | 14 | "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. 15 | "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined above) for the purposes of this License. 16 | "Distribute" means to make available to the public the original and copies of the Work through sale or other transfer of ownership. 17 | "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. 18 | "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. 19 | "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. 20 | "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. 21 | "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. 22 | "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 23 | 24 | 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 25 | 26 | 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: 27 | 28 | to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; and, 29 | to Distribute and Publicly Perform the Work including as incorporated in Collections. 30 | 31 | The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats, but otherwise you have no rights to make Adaptations. Subject to 8(f), all rights not expressly granted by Licensor are hereby reserved, including but not limited to the rights set forth in Section 4(d). 32 | 33 | 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: 34 | 35 | You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. 36 | You may not exercise any of the rights granted to You in Section 3 above in any manner that is primarily intended for or directed toward commercial advantage or private monetary compensation. The exchange of the Work for other copyrighted works by means of digital file-sharing or otherwise shall not be considered to be intended for or directed toward commercial advantage or private monetary compensation, provided there is no payment of any monetary compensation in connection with the exchange of copyrighted works. 37 | If You Distribute, or Publicly Perform the Work or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work. The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Collection, at a minimum such credit will appear, if a credit for all contributing authors of Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. 38 | 39 | For the avoidance of doubt: 40 | Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; 41 | Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License if Your exercise of such rights is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b) and otherwise waives the right to collect royalties through any statutory or compulsory licensing scheme; and, 42 | Voluntary License Schemes. The Licensor reserves the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License that is for a purpose or use which is otherwise than noncommercial as permitted under Section 4(b). 43 | Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. 44 | 45 | 5. Representations, Warranties and Disclaimer 46 | 47 | UNLESS OTHERWISE MUTUALLY AGREED BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 48 | 49 | 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 50 | 51 | 7. Termination 52 | 53 | This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. 54 | Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 55 | 56 | 8. Miscellaneous 57 | 58 | Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. 59 | If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 60 | No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. 61 | This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. 62 | The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. 63 | 64 | Creative Commons Notice 65 | 66 | Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. 67 | 68 | Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of this License. 69 | 70 | Creative Commons may be contacted at https://creativecommons.org/. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prose for Programmers 2 | 3 | This is a book for programmers. 4 | It is a guide to mastering the most difficult programming language of all: human language. 5 | 6 | Just as with programming, 7 | we write with particular goals in mind. 8 | That goal might be as simple as reminding someone about a requirement 9 | or as complex as justifying an entirely new architecture for a product. 10 | But in every case we write in order to achieve something. 11 | 12 | This book will teach you how to achieve those goals more effectively with clearer, more persuasive writing. 13 | 14 | ## It is a work in progress 15 | 16 | The first three chapters are content complete. 17 | 18 | * [Why should developers study writing?](manuscript/why.md) 19 | * [General Rules](manuscript/rules.md) 20 | * [The Writing Process](manuscript/process.md) 21 | 22 | And there are four chapters to come. 23 | 24 | * Writing Structures 25 | * Audience 26 | * Genres 27 | * Other Resources 28 | 29 | For more detail, take a look at [the outline](outline.md). 30 | 31 | ## Can I get it in EPUB/MOBI/PDF? 32 | 33 | [Yes, you can.](https://leanpub.com/proseforprogrammers) 34 | 35 | ## Contributing 36 | 37 | * For general questions and suggestions, please [open an issue][issues]. 38 | * To correct typos or suggest specific changes in the way something is written, please [open a pull request][prs]. 39 | 40 | Note: By contributing content to this repository, you grant the book author (Joshua Clanton) a non-exclusive license to use that content in the book as he deems appropriate. 41 | 42 | ## License 43 | 44 | Creative Commons License
Prose for Programmers by Joshua Clanton is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License. 45 | 46 | [issues]:https://github.com/joshuacc/prose-for-programmers/issues 47 | [prs]:https://github.com/joshuacc/prose-for-programmers/pulls -------------------------------------------------------------------------------- /_data/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Prose for Programmers", 3 | "subtitle": "Learn to write for people, not just computers", 4 | "description": "Learn to write for people, not just computers", 5 | "footer": "Prose for Programmers by Joshua Clanton is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.", 6 | "url": "https://spacebook.app", 7 | "githubUrl": "https://github.com/joshuacc/prose-for-programmers", 8 | "githubBranch": "master", 9 | "navigationStyle": "vertical", 10 | "emoji": "✍🏻", 11 | "enableSearch": false, 12 | "enableDarkMode": true, 13 | "enableEditButton": true, 14 | "enableDatestamp": true, 15 | "enableGithubLink": true, 16 | "enableContact": false, 17 | "enableNetlifyCMS": false, 18 | "enableComments": false, 19 | "enableEncryption": false, 20 | "enablePageNavigation": true 21 | } 22 | -------------------------------------------------------------------------------- /_includes/assets/css/404.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | width: 100%; 5 | margin: 0px; 6 | background: linear-gradient(90deg, #2f3640 23%, #181b20 100%); 7 | } 8 | 9 | .moon { 10 | background: linear-gradient(90deg, #d0d0d0 48%, #919191 100%); 11 | position: absolute; 12 | top: -100px; 13 | left: -300px; 14 | width: 900px; 15 | height: 900px; 16 | content: ''; 17 | border-radius: 100%; 18 | box-shadow: 0px 0px 30px -4px rgba(0, 0, 0, 0.5); 19 | } 20 | 21 | .moon__crater { 22 | position: absolute; 23 | content: ''; 24 | border-radius: 100%; 25 | background: linear-gradient(90deg, #7a7a7a 38%, #c3c3c3 100%); 26 | opacity: 0.6; 27 | } 28 | 29 | .moon__crater1 { 30 | top: 250px; 31 | left: 500px; 32 | width: 60px; 33 | height: 180px; 34 | } 35 | 36 | .moon__crater2 { 37 | top: 650px; 38 | left: 340px; 39 | width: 40px; 40 | height: 80px; 41 | transform: rotate(55deg); 42 | } 43 | 44 | .moon__crater3 { 45 | top: -20px; 46 | left: 40px; 47 | width: 65px; 48 | height: 120px; 49 | transform: rotate(250deg); 50 | } 51 | 52 | .star { 53 | background: grey; 54 | position: absolute; 55 | width: 5px; 56 | height: 5px; 57 | content: ''; 58 | border-radius: 100%; 59 | transform: rotate(250deg); 60 | opacity: 0.4; 61 | animation-name: shimmer; 62 | animation-duration: 1.5s; 63 | animation-iteration-count: infinite; 64 | animation-direction: alternate; 65 | } 66 | 67 | @keyframes shimmer { 68 | from { 69 | opacity: 0; 70 | } 71 | to { 72 | opacity: 0.7; 73 | } 74 | } 75 | 76 | .star1 { 77 | top: 40%; 78 | left: 50%; 79 | animation-delay: 1s; 80 | } 81 | 82 | .star2 { 83 | top: 60%; 84 | left: 90%; 85 | animation-delay: 3s; 86 | } 87 | 88 | .star3 { 89 | top: 10%; 90 | left: 70%; 91 | animation-delay: 2s; 92 | } 93 | 94 | .star4 { 95 | top: 90%; 96 | left: 40%; 97 | } 98 | 99 | .star5 { 100 | top: 20%; 101 | left: 30%; 102 | animation-delay: 0.5s; 103 | } 104 | 105 | .error { 106 | position: absolute; 107 | left: 100px; 108 | top: 400px; 109 | transform: translateY(-60%); 110 | font-family: 'Righteous', cursive, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 111 | color: #363e49; 112 | } 113 | 114 | .error__title { 115 | font-size: 10em; 116 | } 117 | 118 | .error__subtitle { 119 | font-size: 2em; 120 | } 121 | 122 | .error__description { 123 | opacity: 0.5; 124 | } 125 | 126 | .error__button { 127 | min-width: 7em; 128 | margin-top: 3em; 129 | margin-right: 0.5em; 130 | padding: 0.5em 2em; 131 | outline: none; 132 | border: 2px solid #2f3640; 133 | background-color: transparent; 134 | border-radius: 8em; 135 | color: #576375; 136 | cursor: pointer; 137 | transition-duration: 0.2s; 138 | font-size: 0.75em; 139 | font-family: 'Righteous', cursive,ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";; 140 | } 141 | 142 | .error__button:hover { 143 | color: #21252c; 144 | } 145 | 146 | .error__button--active { 147 | background-color: #e67e22; 148 | border: 2px solid #e67e22; 149 | color: white; 150 | } 151 | 152 | .error__button--active:hover { 153 | box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.5); 154 | color: white; 155 | } 156 | 157 | .astronaut { 158 | position: absolute; 159 | width: 185px; 160 | height: 300px; 161 | left: 70%; 162 | top: 50%; 163 | transform: translate(-50%, -50%) rotate(20deg) scale(1.2); 164 | } 165 | 166 | .astronaut__head { 167 | background-color: white; 168 | position: absolute; 169 | top: 60px; 170 | left: 60px; 171 | width: 60px; 172 | height: 60px; 173 | content: ''; 174 | border-radius: 2em; 175 | } 176 | 177 | .astronaut__head-visor-flare1 { 178 | background-color: #7f8fa6; 179 | position: absolute; 180 | top: 28px; 181 | left: 40px; 182 | width: 10px; 183 | height: 10px; 184 | content: ''; 185 | border-radius: 2em; 186 | opacity: 0.5; 187 | } 188 | 189 | .astronaut__head-visor-flare2 { 190 | background-color: #718093; 191 | position: absolute; 192 | top: 40px; 193 | left: 38px; 194 | width: 5px; 195 | height: 5px; 196 | content: ''; 197 | border-radius: 2em; 198 | opacity: 0.3; 199 | } 200 | 201 | .astronaut__backpack { 202 | background-color: #bfbfbf; 203 | position: absolute; 204 | top: 90px; 205 | left: 47px; 206 | width: 86px; 207 | height: 90px; 208 | content: ''; 209 | border-radius: 8px; 210 | } 211 | 212 | .astronaut__body { 213 | background-color: #e6e6e6; 214 | position: absolute; 215 | top: 115px; 216 | left: 55px; 217 | width: 70px; 218 | height: 80px; 219 | content: ''; 220 | border-radius: 8px; 221 | } 222 | 223 | .astronaut__body__chest { 224 | background-color: #d9d9d9; 225 | position: absolute; 226 | top: 140px; 227 | left: 68px; 228 | width: 45px; 229 | height: 25px; 230 | content: ''; 231 | border-radius: 6px; 232 | } 233 | 234 | .astronaut__arm-left1 { 235 | background-color: #e6e6e6; 236 | position: absolute; 237 | top: 127px; 238 | left: 9px; 239 | width: 65px; 240 | height: 20px; 241 | content: ''; 242 | border-radius: 8px; 243 | transform: rotate(-30deg); 244 | } 245 | 246 | .astronaut__arm-left2 { 247 | background-color: #e6e6e6; 248 | position: absolute; 249 | top: 102px; 250 | left: 7px; 251 | width: 20px; 252 | height: 45px; 253 | content: ''; 254 | border-radius: 8px; 255 | transform: rotate(-12deg); 256 | border-top-left-radius: 8em; 257 | border-top-right-radius: 8em; 258 | } 259 | 260 | .astronaut__arm-right1 { 261 | background-color: #e6e6e6; 262 | position: absolute; 263 | top: 113px; 264 | left: 100px; 265 | width: 65px; 266 | height: 20px; 267 | content: ''; 268 | border-radius: 8px; 269 | transform: rotate(-10deg); 270 | } 271 | 272 | .astronaut__arm-right2 { 273 | background-color: #e6e6e6; 274 | position: absolute; 275 | top: 78px; 276 | left: 141px; 277 | width: 20px; 278 | height: 45px; 279 | content: ''; 280 | border-radius: 8px; 281 | transform: rotate(-10deg); 282 | border-top-left-radius: 8em; 283 | border-top-right-radius: 8em; 284 | } 285 | 286 | .astronaut__arm-thumb-left { 287 | background-color: #e6e6e6; 288 | position: absolute; 289 | top: 110px; 290 | left: 21px; 291 | width: 10px; 292 | height: 6px; 293 | content: ''; 294 | border-radius: 8em; 295 | transform: rotate(-35deg); 296 | } 297 | 298 | .astronaut__arm-thumb-right { 299 | background-color: #e6e6e6; 300 | position: absolute; 301 | top: 90px; 302 | left: 133px; 303 | width: 10px; 304 | height: 6px; 305 | content: ''; 306 | border-radius: 8em; 307 | transform: rotate(20deg); 308 | } 309 | 310 | .astronaut__wrist-left { 311 | background-color: #e67e22; 312 | position: absolute; 313 | top: 122px; 314 | left: 6.5px; 315 | width: 21px; 316 | height: 4px; 317 | content: ''; 318 | border-radius: 8em; 319 | transform: rotate(-15deg); 320 | } 321 | 322 | .astronaut__wrist-right { 323 | background-color: #e67e22; 324 | position: absolute; 325 | top: 98px; 326 | left: 141px; 327 | width: 21px; 328 | height: 4px; 329 | content: ''; 330 | border-radius: 8em; 331 | transform: rotate(-10deg); 332 | } 333 | 334 | .astronaut__leg-left { 335 | background-color: #e6e6e6; 336 | position: absolute; 337 | top: 188px; 338 | left: 50px; 339 | width: 23px; 340 | height: 75px; 341 | content: ''; 342 | transform: rotate(10deg); 343 | } 344 | 345 | .astronaut__leg-right { 346 | background-color: #e6e6e6; 347 | position: absolute; 348 | top: 188px; 349 | left: 108px; 350 | width: 23px; 351 | height: 75px; 352 | content: ''; 353 | transform: rotate(-10deg); 354 | } 355 | 356 | .astronaut__foot-left { 357 | background-color: white; 358 | position: absolute; 359 | top: 240px; 360 | left: 43px; 361 | width: 28px; 362 | height: 20px; 363 | content: ''; 364 | transform: rotate(10deg); 365 | border-radius: 3px; 366 | border-top-left-radius: 8em; 367 | border-top-right-radius: 8em; 368 | border-bottom: 4px solid #e67e22; 369 | } 370 | 371 | .astronaut__foot-right { 372 | background-color: white; 373 | position: absolute; 374 | top: 240px; 375 | left: 111px; 376 | width: 28px; 377 | height: 20px; 378 | content: ''; 379 | transform: rotate(-10deg); 380 | border-radius: 3px; 381 | border-top-left-radius: 8em; 382 | border-top-right-radius: 8em; 383 | border-bottom: 4px solid #e67e22; 384 | } -------------------------------------------------------------------------------- /_includes/assets/css/inline.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuacc/prose-for-programmers/fc0c8a4a093f406d380768aa626615307fabf707/_includes/assets/css/inline.css -------------------------------------------------------------------------------- /_includes/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | GitHub icon -------------------------------------------------------------------------------- /_includes/assets/js/404.js: -------------------------------------------------------------------------------- 1 | (function (window, document) { 2 | "use strict"; 3 | window.addEventListener('load', function () { 4 | const cordCanvas = document.getElementById('cord'); 5 | const ctx = cordCanvas.getContext('2d'); 6 | 7 | let y1 = 160; 8 | let y2 = 100; 9 | let y3 = 100; 10 | 11 | let y1Forward = true; 12 | let y2Forward = false; 13 | let y3Forward = true; 14 | drawVisor() 15 | //animate() 16 | }); 17 | })(window, document); 18 | 19 | function drawVisor() { 20 | const canvas = document.getElementById('visor'); 21 | const ctx = canvas.getContext('2d'); 22 | 23 | ctx.beginPath(); 24 | ctx.moveTo(5, 45); 25 | ctx.bezierCurveTo(15, 64, 45, 64, 55, 45); 26 | 27 | ctx.lineTo(55, 20); 28 | ctx.bezierCurveTo(55, 15, 50, 10, 45, 10); 29 | 30 | ctx.lineTo(15, 10); 31 | 32 | ctx.bezierCurveTo(15, 10, 5, 10, 5, 20); 33 | ctx.lineTo(5, 45); 34 | 35 | ctx.fillStyle = '#2f3640'; 36 | ctx.strokeStyle = '#f5f6fa'; 37 | ctx.fill(); 38 | ctx.stroke(); 39 | } 40 | 41 | 42 | 43 | function animate() { 44 | requestAnimationFrame(animate); 45 | ctx.clearRect(0, 0, innerWidth, innerHeight); 46 | 47 | ctx.beginPath(); 48 | ctx.moveTo(130, 170); 49 | ctx.bezierCurveTo(250, y1, 345, y2, 400, y3); 50 | 51 | ctx.strokeStyle = 'white'; 52 | ctx.lineWidth = 8; 53 | ctx.stroke(); 54 | 55 | 56 | if (y1 === 100) { 57 | y1Forward = true; 58 | } 59 | 60 | if (y1 === 300) { 61 | y1Forward = false; 62 | } 63 | 64 | if (y2 === 100) { 65 | y2Forward = true; 66 | } 67 | 68 | if (y2 === 310) { 69 | y2Forward = false; 70 | } 71 | 72 | if (y3 === 100) { 73 | y3Forward = true; 74 | } 75 | 76 | if (y3 === 317) { 77 | y3Forward = false; 78 | } 79 | 80 | y1Forward ? y1 += 1 : y1 -= 1; 81 | y2Forward ? y2 += 1 : y2 -= 1; 82 | y3Forward ? y3 += 1 : y3 -= 1; 83 | } 84 | -------------------------------------------------------------------------------- /_includes/assets/js/inline.js: -------------------------------------------------------------------------------- 1 | if (window.netlifyIdentity) { 2 | window.netlifyIdentity.on("init", user => { 3 | if (!user) { 4 | window.netlifyIdentity.on("login", () => { 5 | document.location.href = "/admin/"; 6 | }); 7 | } 8 | }); 9 | } 10 | 11 | document.addEventListener("DOMContentLoaded", function() { 12 | 13 | const el = document.getElementById("main"); 14 | el.addEventListener("click", closeNavigation, false) 15 | 16 | if (localStorage.getItem('darkmode') === 'true') { 17 | document.body.classList.add("dark"); 18 | } else { 19 | document.body.classList.remove("dark"); 20 | } 21 | }); 22 | 23 | function logout() { 24 | localStorage.removeItem("passphrase") 25 | window.location.href = "/"; 26 | } 27 | 28 | function showNavigation() { 29 | const navigation = document.getElementById("navigation"); 30 | navigation.classList.remove("hidden", "sticky","pt-32"); 31 | navigation.classList.add("absolute","right-0", "top-0", "-mt-0", "z-50", "pt-0", "bg-white","border-l", "border-gray-200"); 32 | } 33 | 34 | function closeNavigation() { 35 | const navigation = document.getElementById("navigation"); 36 | navigation.classList.add("hidden"); 37 | navigation.classList.remove("absolute","right-0","z-50", "bg-gray-100", "border-r", "border-gray-800" ); 38 | } 39 | 40 | function activateDarkMode() { 41 | if (localStorage.getItem('darkmode') === 'true') { 42 | localStorage.setItem('darkmode', 'false') 43 | document.body.classList.remove("dark"); 44 | } else { 45 | localStorage.setItem('darkmode', 'true') 46 | document.body.classList.add("dark"); 47 | } 48 | } 49 | 50 | function toggleLayout(state) { 51 | if (localStorage.getItem('layout') === "horizontal") { 52 | localStorage.setItem('layout', 'vertical') 53 | } else if (localStorage.getItem('layout') === "vertical") { 54 | localStorage.setItem('layout', "horizontal") 55 | } else if (!localStorage.getItem('layout')) { 56 | if (state === "horizontal") { 57 | localStorage.setItem('layout', 'vertical') 58 | } else { 59 | localStorage.setItem('layout', 'horizontal') 60 | } 61 | } 62 | 63 | 64 | console.log(localStorage.getItem('layout')) 65 | } -------------------------------------------------------------------------------- /_includes/assets/js/search.js: -------------------------------------------------------------------------------- 1 | (function (window, document) { 2 | "use strict"; 3 | const search = (e) => { 4 | const results = window.searchIndex.search(e.target.value, { 5 | bool: "OR", 6 | expand: true, 7 | }); 8 | const searchBox = document.getElementById("searchField"); 9 | const resEl = document.getElementById("searchResults"); 10 | const noResultsEl = document.getElementById("noResultsFound"); 11 | const navigation = document.getElementById("navigation"); 12 | 13 | searchBox.addEventListener('focus', (event) => { 14 | event.target.classList.remove("hidden"); 15 | resEl.classList.remove("hidden"); 16 | }); 17 | 18 | document.addEventListener('click', function(event) { 19 | var isClickInside = searchBox.contains(event.target); 20 | if (!isClickInside) { 21 | console.log('hidden') 22 | resEl.classList.add("hidden"),noResultsEl.classList.add("hidden") 23 | noResultsEl.classList.add("hidden") 24 | } 25 | }); 26 | 27 | resEl.innerHTML = ""; 28 | if (e.target.value != "") { 29 | if (results != "") { 30 | noResultsEl.classList.add("hidden") 31 | resEl.classList.add("p-4") 32 | results.map((r) => { 33 | const { id, title, description } = r.doc; 34 | const el = document.createElement("li", { tabindex: '-1' }); 35 | resEl.appendChild(el); 36 | 37 | const h3 = document.createElement("h3"); 38 | el.appendChild(h3); 39 | 40 | const a = document.createElement("a"); 41 | a.setAttribute("href", id); 42 | a.textContent = title; 43 | h3.appendChild(a); 44 | 45 | const p = document.createElement("p"); 46 | p.textContent = description; 47 | el.appendChild(p); 48 | }); 49 | } else { 50 | noResultsEl.classList.remove("hidden") 51 | } 52 | } else { 53 | noResultsEl.classList.add("hidden") 54 | } 55 | }; 56 | fetch("/search-index.json").then((response) => 57 | response.json().then((rawIndex) => { 58 | window.searchIndex = elasticlunr.Index.load(rawIndex); 59 | document.getElementById("searchField").addEventListener("input", search); 60 | }) 61 | ); 62 | })(window, document); -------------------------------------------------------------------------------- /_includes/assets/utils/image.txt: -------------------------------------------------------------------------------- 1 | {% image "sagan.jpg" "Carl Sagan, 1987" %} -------------------------------------------------------------------------------- /_includes/assets/utils/imagesize.txt: -------------------------------------------------------------------------------- 1 | {% image "sagan.jpg" "Carl Sagan, 1987" "200px" %} -------------------------------------------------------------------------------- /_includes/components/contact.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Send a message

5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /_includes/components/footer.njk: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | {% if site.enableContact == 1 %} 6 | 7 | 8 | 9 | {% endif %} 10 |
11 | 12 |
13 | 14 | {% if site.footer %} 15 | {{ site.footer | safe }} 16 | {% endif %} 17 | 18 |
19 | {% if site.enableNetlifyCMS == 1 %} 20 | 28 | {% endif %} 29 | {% if site.enableGithubLink == 1 %} 30 |
31 | 32 |
33 | {% endif %} 34 |
35 |
36 | -------------------------------------------------------------------------------- /_includes/components/head.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ renderData.title or title }} - {{ site.name }} 5 | 6 | 7 | 8 | 9 | {% set js %} 10 | {% include "assets/js/inline.js" %} 11 | {% include "assets/js/search.js" %} 12 | {% endset %} 13 | 14 | 15 | 16 | {% if site.enableNetlifyCMS == 1 %} 17 | 18 | 19 | {% endif %} 20 | 21 | {% if site.enableSearch == 1 %} 22 | 23 | {% endif %} 24 | 25 | {% if site.navigationStyle == 'horizontal' %} 26 | 27 | {% endif %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /_includes/components/header.njk: -------------------------------------------------------------------------------- 1 |
2 | 33 | {% if site.navigationStyle == 'horizontal' %} 34 |
35 | {% include "components/nav.njk" %} 36 |
37 | {% endif %} 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /_includes/components/nav.njk: -------------------------------------------------------------------------------- 1 | {% set navPages = collections.all | eleventyNavigation %} 2 | 3 | {% macro renderNavListItem(entry,rel) -%} 4 | 5 | {% if entry.url == page.url or entry.title == eleventyNavigation.parent or entry.title == eleventyComputed.key%} 6 | {% if rel == "child" %} 7 | {% set highlight = 'border-gray-600 font-semibold text-gray-500 focus:border-gray-700' %} 8 | {% else %} 9 | {% set highlight = 'border-gray-600 font-semibold focus:border-gray-700' %} 10 | {% endif %} 11 | {% set current = 'aria-current="page"' %} 12 | {% else %} 13 | {% set highlight = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-500 ' %} 14 | {% set current = '' %} 15 | {% endif %} 16 | 17 | {%- if not entry.children.length -%} 18 | {% if rel == "child" %} 19 | {{ entry.title }} 35 | {% else %} 36 | {{ entry.title }} 55 | {% endif %} 56 | 57 | {%- else -%} 58 | 59 | {%- if entry.children.length -%} 60 | 61 |
62 | 67 |
68 |
69 | {%- for child in entry.children %}{{ renderNavListItem(child) }}{% endfor -%} 70 |
71 |
72 |
73 | {%- endif -%} 74 | {%- endif -%} 75 | {%- endmacro %} 76 | 77 |
78 |
79 | 80 | 85 | 86 | 96 |
97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /_includes/experimental/blog/author.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Author archive 3 | layout: layouts/base.njk 4 | pagination: 5 | data: collections.authors 6 | size: 1 7 | alias: author 8 | permalink: "authors/{{ author | slug }}/" 9 | renderData: 10 | author: "{{ author }}" 11 | title: "Posts by {{ author }}" 12 | metaDescription: "An archive of all posts by the author: {{ author }}." 13 | --- 14 | 15 |

{{ renderData.title | safe }}

16 | 17 |
18 | {% for post in collections.authors[author] | reverse %} 19 | 20 |

21 | 22 | {% if post.title %} 23 | {{ post.title }} 24 | {% else %} 25 | Untitled 26 | {% endif %} 27 | 28 |

29 | {% if post.summary %} 30 |

31 | {{ post.summary }} 32 |

33 | {% endif %} 34 |

35 | 38 |

39 | {% if post.tags %} 40 |

41 | {% for tag in post.tags %} 42 | {%- if tag != "post" -%} 43 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 44 | 45 | {%- endif -%} 46 | {% endfor %} 47 |

48 | {% endif %} 49 | 50 | {% endfor %} 51 | 54 |
-------------------------------------------------------------------------------- /_includes/experimental/blog/authors.njk: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authors 3 | metaDescription: This page lists all of the Blog's authors. 4 | layout: layouts/base.njk 5 | pagination: 6 | data: collections.authors 7 | size: Infinity 8 | permalink: "authors/index.html" 9 | --- 10 | 11 |

{{ title }}

12 | 13 |
14 | {% for author in pagination.items %} 15 |
16 |

17 | {{ author | safe }} 18 |

19 |
20 | {% endfor %} 21 | 24 |
-------------------------------------------------------------------------------- /_includes/experimental/blog/components/pageslist.njk: -------------------------------------------------------------------------------- 1 |
2 | {% for page in pageslist | reverse %} 3 | 4 |

5 | 6 | {% if page.data.title %} 7 | {{ page.data.title }} 8 | {% else %} 9 | Untitled 10 | {% endif %} 11 | 12 |

13 | {% if page.data.summary %} 14 |

15 | {{ page.data.summary }} 16 |

17 | {% endif %} 18 |

19 | 22 |

23 | {% if page.data.tags %} 24 |

25 | {% for tag in page.data.tags %} 26 | {%- if tag != "post" -%} 27 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 28 | 29 | {%- endif -%} 30 | {% endfor %} 31 |

32 | {% endif %} 33 | 34 | {% endfor %} 35 |
36 | -------------------------------------------------------------------------------- /_includes/experimental/blog/components/postslist.njk: -------------------------------------------------------------------------------- 1 |
2 | 25 | {% if post.data.tags %} 26 |

27 | {% for tag in post.data.tags %} 28 | {%- if tag != "post" -%} 29 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 30 | 31 | {%- endif -%} 32 | {% endfor %} 33 |

34 | {% endif %} 35 | 36 |
37 | {# 38 |

Foo

39 | {% set latest_posts = collections.posts %} 40 | {% for post in latest_posts.slice(0,2) | reverse %} 41 | {{ post.data.title}} 42 | {% endfor %} #} 43 | -------------------------------------------------------------------------------- /_includes/experimental/blog/content/posts/firstpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is the first example post 3 | metaDescription: This is a sample meta description. If one is not present in your page/post's front matter, the default metadata.desciption will be used instead. 4 | date: 2019-01-01T00:00:00.000Z 5 | author: John Appleseed 6 | summary: Why contemplating our mortality can be a powerful catalyst for change 7 | tags: 8 | - tech 9 | - environment 10 | - politics 11 | - sport 12 | --- 13 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 14 | 15 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 16 | 17 | ## Section Header 18 | 19 | Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line. 20 | 21 | ``` text/2-3 22 | // this is a command 23 | function myCommand() { 24 | let counter = 0; 25 | counter++; 26 | } 27 | ``` 28 | Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 29 | -------------------------------------------------------------------------------- /_includes/experimental/blog/content/posts/fourthpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is the fourth example post 3 | date: 2020-02-03 4 | author: John Doe 5 | summary: Why contemplating our mortality can be a powerful catalyst for change 6 | tags: 7 | - environment 8 | - politics 9 | --- 10 | Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 11 | 12 | ![A sample inlined image](https://source.unsplash.com/random/600x400) 13 | 14 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 15 | -------------------------------------------------------------------------------- /_includes/experimental/blog/content/posts/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/post.njk", 3 | "permalink": "posts/{{ title | slug }}/index.html", 4 | "author": "Anonymous", 5 | "tags": [ 6 | "post" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /_includes/experimental/blog/content/posts/secondpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is the second example post 3 | summary: Why contemplating our mortality can be a powerful catalyst for change 4 | date: 2020-01-01 5 | author: John Appleseed 6 | tags: 7 | - sport 8 | --- 9 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 10 | 11 | ## Section Header 12 | 13 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 14 | 15 | Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line. 16 | -------------------------------------------------------------------------------- /_includes/experimental/blog/content/posts/the-fifth-and-hopefully-final-example-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The fifth and hopefully final example post yo yo 3 | date: 2020-10-15T12:23:39.598Z 4 | author: Jane Doe 5 | summary: Why contemplating our mortality can be a powerful catalyst for change 6 | tags: 7 | - environment 8 | - sport 9 | eleventyComputed: 10 | key: Spacelog 11 | --- 12 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 13 | 14 | ![A sample inlined image](https://source.unsplash.com/random/600x400) 15 | 16 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. -------------------------------------------------------------------------------- /_includes/experimental/blog/content/posts/thirdpost.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is the third example post which has a slightly longer title than the others 3 | date: 2020-01-01 4 | author: Jane Doe 5 | summary: Why contemplating our mortality can be a powerful catalyst for change 6 | tags: 7 | - tech 8 | - politics 9 | --- 10 | Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. 11 | 12 | ``` 13 | pre, 14 | code { 15 | line-height: 1.5; 16 | } 17 | ``` 18 | 19 | Bring to the table win-win survival strategies to ensure proactive domination. At the end of the day, going forward, a new normal that has evolved from generation X is on the runway heading towards a streamlined cloud solution. User generated content in real-time will have multiple touchpoints for offshoring. 20 | 21 | ## Section Header 22 | 23 | Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line. 24 | -------------------------------------------------------------------------------- /_includes/experimental/blog/layouts/blog.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: blog 4 | permalink: /blog/index.html 5 | --- 6 |
7 |
8 |
9 |
10 |
11 | 12 |

{{ title }}

13 | 14 | {{ layoutContent | safe }} 15 | 16 | {% set postslist = collections.post %} 17 | {% include "components/postslist.njk" %} 18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /_includes/experimental/blog/layouts/post.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: post 4 | --- 5 | {% if site.navigationStyle == "vertical" %} 6 | {% if site.enableEditButton == true or site.enableDatestamp == true %} 7 |
8 | {% if site.enableDatestamp == true %} 9 |
Updated
10 | {% endif %} 11 | {% if site.enableEditButton == true %} 12 |
Edit
13 | {% endif %} 14 |
15 | {% endif %} 16 | {% endif %} 17 | 18 |
19 |
20 |
21 | {% if site.enableTOC and toc %} 22 |
23 | {% else %} 24 |
25 | {% endif %} 26 |
27 |

{{ title }}

28 | {% if site.navigationStyle == "horizontal" %} 29 | {% if site.enableEditButton == true or site.enableDatestamp == true %} 30 |
31 | {% if site.enableDatestamp == true %} 32 |
Updated
33 | {% endif %} 34 | {% if site.enableEditButton == true %} 35 | 36 | {% endif %} 37 |
38 | {% endif %} 39 | {% endif %} 40 |
41 | {{ layoutContent | safe }} 42 | {% if (site.enableComments) and (comments !== 0) %} 43 | 44 | {% endif %} 45 | 46 | {% if tags %} 47 |

48 | {% for tag in tags %} 49 | {%- if tag != "post" -%} 50 | {% set tagUrl %}/tags/{{ tag }}/{% endset %} 51 | 52 | {%- endif -%} 53 | {% endfor %} 54 |

55 | {% endif %} 56 | 57 |
58 | {% if nextPost.url %} 59 |

60 | Next: 61 | 62 |

63 | {% endif %} 64 | {% if prevPost.url %} 65 |

66 | Previous: 67 | 68 |

69 | {% endif %} 70 | 73 | 74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /_includes/experimental/blog/pages/blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/blog.njk 3 | title: Spacelog 4 | date: 2020-12-21 5 | permalink: /blog/index.html 6 | eleventyNavigation: 7 | key: Spacelog 8 | order: 200 9 | --- -------------------------------------------------------------------------------- /_includes/experimental/blog/tags.njk: -------------------------------------------------------------------------------- 1 | --- 2 | pagination: 3 | data: collections 4 | size: 1 5 | alias: tag 6 | filter: 7 | - all 8 | - nav 9 | - post 10 | - posts 11 | - page 12 | - pages 13 | permalink: /tags/{{ tag }}/ 14 | layout: layouts/base.njk 15 | renderData: 16 | title: {{ tag }} 17 | metaDescription: "All content tagged with “{{ tag }}”" 18 | --- 19 |
20 |
21 |
22 |
23 |

Tagged: {{ tag }}

24 | 25 | {% set postslist = collections[tag] %} 26 | {% include "components/postslist.njk" %} 27 | 28 | 31 | 32 |
33 | 34 | 35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /_includes/layouts/404.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | {% set css %} 7 | {% include "_includes/assets/css/404.css" %} 8 | {% endset %} 9 | 10 | {% set js %} 11 | {% include "_includes/assets/js/404.js" %} 12 | {% endset %} 13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 |
404
27 |
Ground Control to Major Tom
28 |
Your circuit's dead, there's something wrong
29 | 30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 |
57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /_includes/layouts/base.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include "components/head.njk" %} 4 | 5 |
6 | {% include "components/header.njk" %} 7 | {% if site.navigationStyle == "vertical" %} 8 | 18 | {% endif %} 19 | {% if site.navigationStyle == 'horizontal' %} 20 |
21 | {% else %} 22 |
23 | {% endif %} 24 | {{ layoutContent | safe }} 25 | {% include "components/footer.njk" %} 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /_includes/layouts/contact.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: contact 4 | --- 5 |
6 | {% include "components/contact.njk" %} 7 |
-------------------------------------------------------------------------------- /_includes/layouts/page.njk: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/base.njk 3 | section: page 4 | --- 5 | 6 | {% if site.navigationStyle == "vertical" %} 7 | {% if site.enableEditButton == true or site.enableDatestamp == true %} 8 |
9 | {% if site.enableDatestamp == true %} 10 |
Updated
11 | {% endif %} 12 | {% if site.enableEditButton == true %} 13 | 14 | {% endif %} 15 |
16 | {% endif %} 17 | {% endif %} 18 | 19 |
20 |
21 |
22 | {% if site.enableTOC and toc %} 23 |
24 | {% else %} 25 |
26 | {% endif %} 27 |
28 |

{{ title }}

29 | {% if site.navigationStyle == "horizontal" %} 30 | {% if site.enableEditButton == true or site.enableDatestamp == true %} 31 |
32 | {% if site.enableDatestamp == true %} 33 |
Updated
34 | {% endif %} 35 | {% if site.enableEditButton == true %} 36 | 37 | {% endif %} 38 |
39 | {% endif %} 40 | {% endif %} 41 |
42 | {{ layoutContent | safe }} 43 | {% if (site.enableComments) and (comments !== 0) %} 44 | 45 | {% endif %} 46 | 47 | {% if site.enablePageNavigation == true %} 48 | 57 | {% endif %} 58 |
59 |
60 |
61 | 62 | {% if site.enableTOC and toc %} 63 | 69 | {% endif %} 70 |
71 |
72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /_includes/layouts/robots.njk: -------------------------------------------------------------------------------- 1 | User-agent: * Disallow: / -------------------------------------------------------------------------------- /admin/config.yml: -------------------------------------------------------------------------------- 1 | backend: 2 | name: git-gateway 3 | branch: main # Branch to update (optional; defaults to main) 4 | 5 | # Uncomment below to enable drafts 6 | # publish_mode: editorial_workflow 7 | 8 | media_folder: "static/img" # Media files will be stored in the repo under images/uploads 9 | 10 | # # Cloudinary 11 | # media_library: 12 | # name: cloudinary 13 | # config: 14 | # cloud_name: broeker 15 | # api_key: 159818251742281 16 | 17 | collections: 18 | # Our blog posts 19 | # - name: "blog" # Used in routes, e.g., /admin/collections/blog 20 | # label: "Post" # Used in the UI 21 | # folder: "posts" # The path to the folder where the documents are stored 22 | # create: true # Allow users to create new documents in this collection 23 | # slug: "{{slug}}" # Filename template, e.g., YYYY-MM-DD-title.md 24 | # fields: # The fields for each document, usually in front matter 25 | # - { label: "Title", name: "title", widget: "string" } 26 | # - { label: "Publish Date", name: "date", widget: "datetime" } 27 | # - { label: "Author", name: "author", widget: "string", default: "Anonymous" } 28 | # - { label: "Summary", name: "summary", widget: "text" } 29 | # - { label: "Tags", name: "tags", widget: "list", default: ["post"] } 30 | # - { label: "Body", name: "body", widget: "markdown" } 31 | # Our pages e.g. About 32 | - name: "pages" 33 | label: "Page" 34 | folder: "pages" 35 | create: true # Change to true to allow editors to create new pages 36 | slug: "{{slug}}" 37 | fields: 38 | - { label: "Title", name: "title", widget: "string" } 39 | - { label: "Publish Date", name: "date", widget: "datetime" } 40 | - { label: "Permalink", name: "permalink", widget: "string" } 41 | - label: "Navigation" # https://www.11ty.dev/docs/plugins/navigation/ 42 | name: "eleventyNavigation" 43 | widget: "object" 44 | fields: 45 | - { label: "Key", name: "key", widget: "string" } 46 | - { label: "Order", name: "order", widget: "number", default: 100 } 47 | - { label: "Parent", name: "parent", widget: "string", required: false, hint: "(Optional) Enter a matching parent key to set this a nested or child page" } 48 | - { label: "Title", name: "title", widget: "string", required: false, hint: "(Optional) Enter alternate text for navigation link" } 49 | - { label: "Body", name: "body", widget: "markdown" } 50 | - label: "Globals" 51 | name: "globals" 52 | files: 53 | - label: "Site Data" 54 | name: "site_data" 55 | delete: false 56 | file: "_data/site.json" 57 | fields: 58 | - {label: "Site name", name: "name", widget: "string"} 59 | - {label: "Site subtitle", name: "subtitle", widget: "string", required: false} 60 | - {label: "Meta description", name: "description", widget: "string"} 61 | - {label: "Site footer", name: "footer", widget: "string", required: false} 62 | - {label: "Site Url", name: "url", widget: "string"} 63 | - {label: "Github Url", name: "githubUrl", widget: "string"} 64 | - {label: "Github branch", name: "githubBranch", widget: "string"} 65 | - {label: "Navigation style", name: "navigationStyle", widget: "string"} 66 | - {label: "Site emoji", name: "emoji", widget: "string", required: false} 67 | - {label: "Enable search", name: "enableSearch", widget: "boolean"} 68 | - {label: "Enable darkmode", name: "enableDarkMode", widget: "boolean"} 69 | - {label: "Enable edit button", name: "enableEditButton", widget: "boolean"} 70 | - {label: "Enable datestamp", name: "enableDatestamp", widget: "boolean"} 71 | - {label: "Enable Github link", name: "enableGithubLink", widget: "boolean"} 72 | - {label: "Enable contact form", name: "enableContact", widget: "boolean"} 73 | - {label: "Enable Netlify CMS", name: "enableNetlifyCMS", widget: "boolean", default: false} 74 | - {label: "Enable comments", name: "enableComments", widget: "boolean", default: false} 75 | - {label: "Enable encryption", name: "enableEncryption", widget: "boolean", default: false} 76 | - {label: "Enable page navigation", name: "enablePageNavigation", widget: "boolean", default: false} -------------------------------------------------------------------------------- /admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Netlify CMS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /admin/preview-templates/index.js: -------------------------------------------------------------------------------- 1 | import Page from "/admin/preview-templates/page.js"; 2 | // import Post from "/admin/preview-templates/post.js"; 3 | 4 | // Register the Post component as the preview for entries in the blog collection 5 | CMS.registerPreviewTemplate("pages", Page); 6 | // CMS.registerPreviewTemplate("blog", Post); 7 | 8 | CMS.registerPreviewStyle("/style.css"); 9 | 10 | // Register any CSS file on the home page as a preview style 11 | // fetch("/") 12 | // .then(response => response.text()) 13 | // .then(html => { 14 | // const f = document.createElement("html"); 15 | // f.innerHTML = html; 16 | // Array.from(f.getElementsByTagName("link")).forEach(tag => { 17 | // if (tag.rel == "stylesheet" && !tag.media) { 18 | // CMS.registerPreviewStyle(tag.href); 19 | // } 20 | // }); 21 | // }); 22 | -------------------------------------------------------------------------------- /admin/preview-templates/page.js: -------------------------------------------------------------------------------- 1 | import htm from "https://unpkg.com/htm?module"; 2 | 3 | const html = htm.bind(h); 4 | 5 | // Preview component for a Page 6 | const Page = createClass({ 7 | render() { 8 | const entry = this.props.entry; 9 | 10 | return html` 11 |
12 |

${entry.getIn(["data", "title"], null)}

13 |
14 | ${this.props.widgetFor("body")} 15 |
16 |
17 | `; 18 | } 19 | }); 20 | 21 | export default Page; 22 | -------------------------------------------------------------------------------- /admin/preview-templates/post.js: -------------------------------------------------------------------------------- 1 | import htm from "https://unpkg.com/htm?module"; 2 | import format from "https://unpkg.com/date-fns@2.7.0/esm/format/index.js?module"; 3 | 4 | const html = htm.bind(h); 5 | 6 | // Preview component for a Post 7 | const Post = createClass({ 8 | render() { 9 | const entry = this.props.entry; 10 | 11 | return html` 12 |
13 |
14 |

${entry.getIn(["data", "title"], null)}

15 |
Updated
16 |

${entry.getIn(["data", "summary"], "")}

17 |
18 | ${this.props.widgetFor("body")} 19 |
20 |

21 | ${ 22 | entry.getIn(["data", "tags"], []).map( 23 | tag => 24 | html` 25 | 26 | ` 27 | ) 28 | } 29 |

30 |
31 |
32 | `; 33 | } 34 | }); 35 | 36 | export default Post; 37 | -------------------------------------------------------------------------------- /content/buy.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: false 3 | eleventyNavigation: 4 | key: Get the eBook edition 5 | order: 10 6 | url: https://leanpub.com/proseforprogrammers 7 | --- -------------------------------------------------------------------------------- /content/images/hello.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuacc/prose-for-programmers/fc0c8a4a093f406d380768aa626615307fabf707/content/images/hello.jpg -------------------------------------------------------------------------------- /content/pages/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layouts/contact.njk 3 | title: Send a message 4 | section: contact 5 | date: Last Modified 6 | permalink: /contact/index.html 7 | --- 8 | You can use [Netlify Forms](https://www.netlify.com/docs/form-handling/) to create contact forms like this one, or any other custom forms you may wish to create. All submissions are sent directly to your Netlify dashboard (with optional notifications.) All forms utilize Netlify's native spam filter will display a CAPTCHA for any flagged submissions. 9 | -------------------------------------------------------------------------------- /content/pages/home.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About this book 3 | permalink: / 4 | eleventyNavigation: 5 | key: About this book 6 | order: 0 7 | --- 8 | 9 | _Prose for Programmers_ is a book for programmers. 10 | It is a guide to mastering the most difficult programming language of all: human language. 11 | 12 | Just as with programming, 13 | we write with particular goals in mind. 14 | That goal might be as simple as reminding someone about a requirement 15 | or as complex as justifying an entirely new architecture for a product. 16 | But in every case we write in order to achieve something. 17 | 18 | This book will teach you how to achieve those goals more effectively with clearer, more persuasive writing. 19 | 20 | ## It is a work in progress 21 | 22 | The first three chapters are content complete. 23 | 24 | * [Why should developers study writing?](/manuscript/why) 25 | * [General Rules](/manuscript/rules) 26 | * [The Writing Process](/manuscript/process) 27 | 28 | And there are four chapters to come. 29 | 30 | * Writing Structures 31 | * Audience 32 | * Genres 33 | * Other Resources 34 | 35 | ## It is editable on GitHub 36 | 37 | The manuscript of _Prose for Programmers_, 38 | as well as the code for the website is hosted on [GitHub](https://github.com/joshuacc/prose-for-programmers). 39 | Each chapter has a convenient edit link at the top of the page. 40 | If you see any mistakes, typos, or other issues, 41 | feel free to edit and open a pull request. 42 | 43 | ## It is available in eBook form for your Kindle or other e-reader 44 | 45 | You can get the EPUB/MOBI/PDF version of _Prose for Programmers_ [via Leanpub](https://leanpub.com/proseforprogrammers). -------------------------------------------------------------------------------- /content/pages/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/page.njk" 3 | } 4 | -------------------------------------------------------------------------------- /filters/searchFilter.js: -------------------------------------------------------------------------------- 1 | const elasticlunr = require("elasticlunr"); 2 | const emojiRegex = require('emoji-regex/RGI_Emoji.js') 3 | 4 | module.exports = function (collection) { 5 | // what fields we'd like our index to consist of 6 | var index = elasticlunr(function () { 7 | this.addField("title"); 8 | this.addField("content"); 9 | this.setRef("id"); 10 | }); 11 | 12 | // loop through each page and add it to the index 13 | collection.forEach((page) => { 14 | index.addDoc({ 15 | id: page.url, 16 | title: page.template.frontMatter.data.title, 17 | content: squash(page.templateContent), 18 | }); 19 | }); 20 | 21 | function squash(text) { 22 | const regex = emojiRegex(); 23 | var content = new String(text); 24 | 25 | // all lower case, please 26 | var content = content.toLowerCase(); 27 | 28 | // remove all html elements and new lines 29 | var re = /(.*?<.*?>)/gi; 30 | var plain = unescape(content.replace(re, '')); 31 | 32 | // remove duplicated words 33 | var words = plain.split(' '); 34 | var deduped = [...(new Set(words))]; 35 | var dedupedStr = deduped.join(' ') 36 | 37 | // remove short and less meaningful words 38 | var result = dedupedStr.replace(/\b(\.|\,|\<;|the|a|an|and|am|you|I|to|if|of|off|me|my|on|in|it|is|at|as|we|do|be|has|but|was|so|no|not|or|up|for)\b/gi, ''); 39 | //remove newlines, and punctuation 40 | result = result.replace(/\.|\,|\?|

|-|—|\n/g, ''); 41 | //remove repeated spaces 42 | result = result.replace(/[ ]{2,}/g, ' '); 43 | // remove most emoji 44 | result = result.replace(/([#0-9]\u20E3)|[\xA9\xAE\u203C\u2047-\u2049\u2122\u2139\u3030\u303D\u3297\u3299][\uFE00-\uFEFF]?|[\u2190-\u21FF][\uFE00-\uFEFF]?|[\u2300-\u23FF][\uFE00-\uFEFF]?|[\u2460-\u24FF][\uFE00-\uFEFF]?|[\u25A0-\u25FF][\uFE00-\uFEFF]?|[\u2600-\u27BF][\uFE00-\uFEFF]?|[\u2900-\u297F][\uFE00-\uFEFF]?|[\u2B00-\u2BF0][\uFE00-\uFEFF]?|(?:\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDEFF])[\uFE00-\uFEFF]?/g, ''); 45 | 46 | let match; 47 | while (match = regex.exec(result)) { 48 | const emoji = match[0]; 49 | result = result.replace(emoji,' ') 50 | } 51 | 52 | return result; 53 | } 54 | 55 | return index.toJSON(); 56 | }; -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | if (localStorage.getItem('password')) { 2 | insertPlainHTML(atob(localStorage.getItem('password'))) 3 | } else { 4 | document.getElementById('staticrypt-form').addEventListener('submit', function(e) { 5 | e.preventDefault(); 6 | insertPlainHTML(document.getElementById('staticrypt-password').value); 7 | }); 8 | } -------------------------------------------------------------------------------- /manuscript/Book.txt: -------------------------------------------------------------------------------- 1 | why.md 2 | rules.md 3 | process.md -------------------------------------------------------------------------------- /manuscript/manuscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "layouts/page.njk" 3 | } 4 | -------------------------------------------------------------------------------- /manuscript/process.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "The Writing Process", 3 | "eleventyNavigation": { 4 | "key": "The Writing Process", 5 | "order": 3 6 | } 7 | } -------------------------------------------------------------------------------- /manuscript/process.md: -------------------------------------------------------------------------------- 1 | # The Writing Process 2 | 3 | Many people are under the impression that writing is a mystical 4 | -- or at least mysterious -- 5 | activity, governed entirely by inspiration. 6 | While it is true that some authors work this way, 7 | waiting for inspiration to strike isn't a common strategy among professional writers, 8 | even novelists. 9 | 10 | For programmers and others writing non-fiction in a business context, 11 | there is a simple process that yields quality results. 12 | 13 | 1. Determine the goal 14 | 2. Determine the audience 15 | 3. Choose a suitable structure 16 | 4. Build an outline 17 | 5. Turn the outline into prose 18 | 6. Revise obvious weaknesses 19 | 7. Obtain feedback 20 | 8. Revise based on feedback 21 | 9. Publish 22 | 23 | In this chapter, we will walk through what each of these steps entail. 24 | 25 | ## 1. Determine the Goal 26 | 27 | ### Why am I writing this, anyway? 28 | 29 | Every time you start writing something, 30 | ask yourself, "Why?" 31 | Sometimes the answer is obvious, 32 | but often it's not. 33 | And even on those occasions when it is obvious, 34 | asking the question helps keep you focused on the goal. 35 | 36 | It's important to remember that we're trying to find the purpose of the document, 37 | not your personal motivation for writing it. 38 | If your answer to "Why am I writing this?" includes the words "I" or "me," 39 | it is worth evaluating whether you're really thinking of the document's goal. 40 | 41 | For instance, if the answer to why you are writing a tutorial is, 42 | "So that I can get a raise," 43 | you're thinking of the wrong thing. 44 | It's not that the motivation is _wrong_. 45 | It's just _irrelevant_ to producing a good tutorial. 46 | A better answer is, 47 | "To help others improve their game's performance." 48 | 49 | Because this answer is focused on what the audience receives, 50 | it is actually useful for guiding your writing efforts. 51 | 52 | ### Keep asking why. 53 | 54 | Often, the first goal we come up with will be superficial. 55 | "Teaching others how to use object pools," 56 | may be accurate, 57 | and even somewhat useful, 58 | but a bit more context can be even more useful. 59 | 60 | There is a technique for root cause analysis called [5 Whys][], 61 | the premise of which is that you need to ask "Why?" multiple times to find the root cause of a problem. 62 | This technique also works well for determining a useful goal for writing. 63 | 64 | Suppose the first goal that occurs to you is 65 | "Teach developers how to use object pools." 66 | That is a perfectly clear and useful goal. 67 | But if you write only to that narrow goal, 68 | your readers won't know why the technique is useful. 69 | So ask yourself, "Why object pools?" 70 | 71 | Perhaps the answer is "To improve performance." 72 | But again we can ask "Why?" 73 | And our answer is something like, "To keep gameplay smooth." 74 | 75 | At this point we can state the goal as: 76 | "Teach game developers how to keep their games running smoothly by using object pools." 77 | 78 | With this more robust goal in mind we're more likely to communicate, 79 | not just the technique, 80 | but also its importance and trade-offs. 81 | 82 | ## 2. Determine the Audience 83 | 84 | Once we understand the goal of the document, 85 | we have enough information to start considering our audience. 86 | Our first question, though, takes a step back from the goal. 87 | 88 | ### Who will be reading this? 89 | 90 | Often the readership of documents is wider than we would initially expect. 91 | A framework's website will be read by developers, of course, 92 | but perhaps also: 93 | 94 | * Architects evaluating technology choices 95 | * Managers trying to keep abreast of what their team is using 96 | * Technical recruiters trying to understand the skillset they are hiring for 97 | * Designers trying to understand technical constraints on their designs 98 | * Sysadmins figuring out the framework's deployment features 99 | 100 | Take time to list the different types of potential readers. 101 | Doing so will help draw out whether additional supporting material is needed. 102 | 103 | ### Which readers are primary? 104 | 105 | Once you have the list of potential readers, 106 | refer back to the goal to decide which readers are most important. 107 | In most cases there will be a single group of readers who constitute your primary audience. 108 | 109 | Continuing the previous example: 110 | the primary audience of a framework website is developers. 111 | Others are important, but secondary to that audience. 112 | 113 | ### What is their relation to the subject? 114 | 115 | At this point we have our primary audience, 116 | but haven't given much detail about them. 117 | So it's time to consider their relation to the subject. 118 | 119 | One important axis to consider is their current level of expertise. 120 | Are they absolute beginners, 121 | world class experts, 122 | or somewhere in between? 123 | The answer to this question drives 124 | which sorts of things can be assumed 125 | and which need to be explained in great detail. 126 | 127 | Another axis is the readers emotional relationship with the subject. 128 | Are they intimidated, enthusiastic, or something else? 129 | A developer who fled Java for Ruby will have a very different attitude toward Haskell than a brand new developer. 130 | They may both be fearful, 131 | but about different things. 132 | Those emotional concerns also need to be addressed. 133 | 134 | ### What is their relation to you? 135 | 136 | In addition to the subject, 137 | we must also consider how the audience is related to you, 138 | the author. 139 | Are they coworkers? 140 | Then you can make certain assumptions about their familiarity with the business. 141 | 142 | Are you on friendly terms or is some of the audience hostile? 143 | You may need to focus more on your logical case for the latter. 144 | 145 | Are you part of the same culture/subculture? 146 | "Corporate suits" probably won't find image macros as amusing as your 22-year-old developer buddy. 147 | 148 | [5 Whys]: http://en.wikipedia.org/wiki/5_Whys 149 | 150 | ## 3. Choose a Suitable Structure 151 | 152 | Now that we know the goal and the audience 153 | we can consider how to structure the document to serve them. 154 | 155 | ### Choosing a structure based on the goal 156 | 157 | Goals can generally be placed on a spectrum between purely informational and purely persuasive. 158 | On one end of the spectrum, 159 | topical hierarchies are great at communicating lots of information efficiently. 160 | The hierarchy conveys the big picture while also supporting scanning through subtopics. 161 | 162 | At the other end of the spectrum, 163 | narrative is particularly effective at persuading people by engaging our natural sense of empathy. 164 | Telling a child that "Lying is a bad idea," is less likely to change their behavior than hearing _The Boy Who Cried Wolf_. 165 | For an example closer to our industry, consider _The Phoenix Project_: 166 | it vividly illustrates the problems of bureacratic IT departments 167 | as well as the benefits of embracing the DevOps movement. 168 | 169 | ### Choosing a structure based on the audience 170 | 171 | Your structure also needs to be suited to your audience. 172 | 173 | If your primary audience is developers with deep experience in your topic, 174 | then you may want to address them with an equally deep and detailed hierarchy of topics. 175 | A beginner, however, would likely benefit from a shallower step by step "recipe" guide. 176 | 177 | The reader's level of interest factors into which structure is best suited. 178 | A highly motivated reader may want to get right to the heart of the matter. 179 | But a distinterested reader 180 | --who is only looking at your document because his boss made him-- 181 | may need to be enticed with a joke or anecdote explaining what is in it for him. 182 | That sort of reader will probably also need frequent positive reinforcement. 183 | 184 | The audience's relationship with you also makes a difference. 185 | If they are familiar with you and trust your judgment, 186 | they are more likely to be patient waiting for a payoff. 187 | But if they don't know anything about you, 188 | the internet is only a click away. 189 | 190 | ### Mixing and matching 191 | 192 | Most writing structures are relatively flexible 193 | and can be combined with other structures as warranted by the goal and the audience. 194 | **Don't be afraid to mix and match them.** 195 | 196 | For example, suppose that you need to convince business leaders that 197 | it is worthwhile to break your large monolithic application into several separate services. 198 | You'd probably start with a Problem-Solution structure like this: 199 | 200 | * Problems with the current architecture 201 | * Proposed solution 202 | 203 | In order to help the businesspeople make a decision, you'd probably extend it with a Pro-Con analysis as well. 204 | 205 | * Problems with the current architecture 206 | * Proposed solution 207 | * Costs and risks of change 208 | * Costs and risks of not changing 209 | 210 | A simple factual elaboration on that outline may win intellectual assent, 211 | but is unlikely to elicit wholehearted support from non-technical leaders. 212 | Why? 213 | Because they lack experiential knowledge of the problems, solutions, and risks. 214 | 215 | But parables and other metaphorical narratives can impart a degree of experiential knowledge without requiring actual experience. 216 | What sort of narrative would work in this case? 217 | 218 | Perhaps a story of a military building a single gigantic ship. 219 | It is powerful, 220 | but difficult to maneuver, 221 | and if it somehow fails then its entire arsenal is useless. 222 | (Business books are fond of military metaphors for some reason.) 223 | Then contrast this with building a fleet of small independently maneuverable ships. 224 | 225 | Revising the outline to incorporate this narrative gives us: 226 | 227 | * Problems with the current architecture 228 | * Parable of the dreadnought 229 | * Supporting factual details 230 | * Proposed solution 231 | * Parable of the fleet 232 | * Supporting factual details 233 | * Costs and risks of change 234 | * Refer to parable 235 | * Supporting factual details 236 | * Costs and risks of not changing 237 | * Refer to parable 238 | * Supporting factual details 239 | 240 | Combining these three structures creates a much more powerful and persuasive account than any of them alone. 241 | 242 | ## 4. Build an Outline 243 | 244 | With a structure 245 | (or set of structures) 246 | chosen for your project, 247 | it is time to build an outline. 248 | Some structures, 249 | like API documentation, 250 | may come with an outline template to follow. 251 | But many others, 252 | like the inverted pyramid, 253 | do not. 254 | 255 | This phase is all about taking your chosen structures 256 | and combining them with the details of your subject 257 | to produce the skeleton of your document. 258 | 259 | The outline itself is a simple hierarchical list of 260 | the things you want to communicate 261 | and the approach you want to take at each step. 262 | Particularly for smaller projects, 263 | the line between choosing a structure and outlining can be rather blurry. 264 | (Evidenced by the the section on choosing structures ending with an outline!) 265 | But an outline should show in detail **how** you intend to implement your chosen structure. 266 | 267 | ### How detailed should an outline be? 268 | 269 | This will likely vary from person to person, 270 | but I prefer to outline in enough detail that 271 | each low level bullet point corresponds to one or two paragraphs in the final text. 272 | This gives me sufficient guidance that I don't get lost, 273 | but also gives me enough flexibility to expand on some points without deviating from the outline. 274 | 275 | ### How do you actually create the outline? 276 | 277 | There are two basic approaches: 278 | depth-first 279 | and breadth-first. 280 | A depth-first approach starts at the top 281 | and explores all the way down to the details one point at a time. 282 | A breadth-first approach starts at the highest level 283 | and only becomes more detailed after all the higher level points have been filled in. 284 | 285 | **Partially completed depth-first outline** 286 | 287 | * Point 1 288 | * Point 1.1 289 | * Point 1.1.1 290 | * Point 1.1.2 291 | * Point 1.1.3 292 | 293 | **Partially completed breadth-first outline** 294 | 295 | * Point 1 296 | * Point 2 297 | * Point 3 298 | * Point 4 299 | * Point 5 300 | 301 | Breadth-first generally produces better results, 302 | because it provides more overall context each time we step down a level of detail. 303 | That extra context makes it much less likely that we will get stuck on trivia. 304 | 305 | ### Evaluate and revise 306 | 307 | Once you've drafted your outline, 308 | be sure to evaluate it with your goals and audience in mind. 309 | It is much easier to move or edit a few bullet points 310 | than to revise entire sections of your document. 311 | 312 | ## 5. Turn the outline into prose 313 | 314 | Everything we've talked about so far has been aimed at preparing you for this part of the process. 315 | You should know your goal, audience and structure. 316 | And your outline gives you a game plan. 317 | Now it's time to turn all that into something for others to read. 318 | 319 | ### Putting flesh on the bones 320 | 321 | Right now what you've got is a skeleton. 322 | It's vaguely in the shape you want, 323 | but if it tried to walk around it would quickly fall apart. 324 | There are two principle things that it is lacking: 325 | muscle to give it strength, 326 | and ligaments to hold things together. 327 | Let's start with the muscle. 328 | 329 | Bare statements of fact rarely carry much weight with readers, 330 | regardless of how important those facts might be. 331 | They have to be elaborated on 332 | and placed into their proper context. 333 | And usually the most important context for a reader is the answer to 334 | "Why do I care about this?" 335 | A reason to care gives your prose strength. 336 | 337 | Consider this example: 338 | 339 | > Continuous integration improves the development process. 340 | 341 | > Continuous integration improves the development process by helping us detect defects sooner. 342 | > That means fewer late night calls to debug production. 343 | 344 | The latter includes more supporting detail about _how_ it helps, 345 | as well as a reason for developers to care about it. 346 | 347 | In addition to muscle we need ligaments. 348 | The ligaments are what actually connects one bone to another. 349 | The prose equivalent is a transition. 350 | 351 | Sudden jumps from one topic to another 352 | (even closely related) 353 | are jarring. 354 | Transitions serve to smooth that over by 355 | clearly signaling the close of one topic, 356 | the introduction of the next, 357 | and possibly how they are related. 358 | 359 | A transition can be a simple phrase. 360 | It can be an entire sentence. 361 | It can be a paragraph. 362 | Or multiple paragraphs. 363 | It all depends on the size of the pieces you're transitioning between. 364 | 365 | For an example, take a look at the paragraph that started this section (#5) above. 366 | 367 | ### It's not necessarily exposition 368 | 369 | When working from your outline, 370 | beware of always choosing the most direct expansion of your points. 371 | Simple exposition is the most common way of describing things, 372 | but it isn't always the best. 373 | Using other forms of expansion will give your prose variety and help maintain reader interest. 374 | 375 | Especially in a longer text, 376 | including the occasional story, joke, or anecdote 377 | can make the difference between a pleasant read or an unbearably boring one. 378 | 379 | Whatever you choose to use, 380 | it should be integrally related to the points you're trying to make. 381 | 382 | There is a story about a high school student who had to give a book report. 383 | When he stood up in front of the class he yelled, 384 | "Sex! Sex! Sex!" 385 | then continued, 386 | "Now that I have your attention, 387 | I'd like to talk to you about The Complete History of World War II." 388 | 389 | Don't be like the high school student. 390 | 391 | ## 6. Revise 392 | 393 | At this point you have a fleshed out draft, 394 | so now it is time to fix the gaps and rough edges. 395 | 396 | ### Evaluate 397 | 398 | Starting at the beginnning, read each unit of your work. 399 | Depending on the length of your document, that unit might be a paragraph, section, or chapter. 400 | As you read, ask yourself the following questions: 401 | 402 | * Does this advance the goal? 403 | * Is it clear? 404 | * Is it concise? 405 | * Is it well organized? 406 | * Is it scannable? 407 | 408 | You'll likely want to read the unit multiple times to answer these questions. 409 | For a longer unit, one read per question -- or even more -- may be appropriate. 410 | 411 | > #### Tip: Try different contexts 412 | > 413 | > Reading in a different context than you usually write in can help you to see things differently and spot problems more easily. 414 | > For example, if you usually write at a computer, 415 | > try printing your document out on paper, 416 | > or even reading it out loud. 417 | 418 | ### Plan your corrections 419 | 420 | Take notes on each thing that needs to be improved, 421 | but don't make corrections right away. 422 | Introducing a slight delay between identifying the problem and attempting to fix it 423 | will give your mind a chance to work on the problem, 424 | and often arrive at a better solution than the first one you thought of. 425 | 426 | Your notes can take many forms: 427 | a separate text file, 428 | using Track Changes in Microsoft Word, 429 | and so on. 430 | I tend to prefer annnotating a physical paper with pen or pencil, 431 | but you should experiement and find what works best for you. 432 | 433 | After you have a list of all the corrections you plan to make, 434 | you may be able to spot common patterns for future improvement. 435 | But the immediate task is to begin making corrections. 436 | 437 | ### Make your corrections 438 | 439 | Proceed through your notes from beginning to end, 440 | correcting each problem. 441 | If you get stuck on something, 442 | leave it to the side for now. 443 | You'll come back to it later. 444 | 445 | Once you've completed all the corrections you were able to make, 446 | take another look at any you left behind. 447 | You may know how to fix them now. 448 | If so, go ahead. 449 | If not, we'll leave them for the next stage of the writing process: feedback. 450 | 451 | ## 7. Get feedback 452 | 453 | Up to this point everything has come from you, 454 | even thinking about the audience. 455 | But this is where you actually engage with other people for the first time. 456 | Brace yourself, 457 | because feedback from real people is almost certain to suprise you, 458 | regardless of whether it's positive or negative. 459 | 460 | ### Sources of feedback 461 | 462 | Generally you will be getting feedback from one of three types of readers: 463 | members of the target aurdience, 464 | experts on the subject, 465 | and friends/coworkers. 466 | Each of these groups has strengths and weaknesses. 467 | Getting feedback from all three is ideal, 468 | and occasionally the groups overlap, 469 | but feedback from any of them is useful for checking your assumptions. 470 | 471 | #### Target audience 472 | 473 | Members of the target audience are extremely valuable 474 | because they can tell you whether your writing is achieving the intended goal. 475 | Unfortunately, they often can't tell you why. 476 | And even if they do offer a reason, 477 | it shouldn't always be taken at face value. 478 | 479 | To help overcome this issue, 480 | it can help to supply a list of specific questions for them to answer. 481 | For example: 482 | 483 | * By the end did you think that it is worth investigating the new technology I described? 484 | * If not, was there a specific point where I lost you? 485 | * Did any of my supporting evidence seem questionable? 486 | 487 | Unfortunately, it can be difficult or impossible to get feedback from the audience. 488 | After all, when drafting an email to your boss's boss, 489 | you can't very well ask them to proofread it. 490 | 491 | #### Experts 492 | 493 | Expert feedback can also be very useful, 494 | particularly for identifying inaccuracies and mistakes. 495 | One issue to watch out for, though, 496 | is that experts may not remember what it was like to be a beginner. 497 | So if beginners and non-experts are in your target audience, 498 | be careful of suggestions to includes lots of additional details which they won't have the context to understand. 499 | 500 | #### Friends and coworkers 501 | 502 | Friends and coworkers are often the easiest reviewers to obtain. 503 | Unfortunately, they are often the least reliable in providing needed correction. 504 | No one wants to hurt the feelings of someone they like. 505 | But they are often excellent at providing encouragement, 506 | and when working on a large writing project, 507 | that can be invaluable. 508 | 509 | ### Evaluating feedback 510 | 511 | When evaluating the feedback there are a few principles to keep in mind. 512 | 513 | First, _readers are always right about their reactions_. 514 | If a reader says that they were confused by a certain point, 515 | then they were, 516 | no matter how clear it may appear to you. 517 | 518 | Second, _a reader's negative reaction does not automatically lead to revision_. 519 | It is your responsibility to evaluate 520 | whether issues raised are actually problems for your target audience 521 | and serious enough to require addressing. 522 | 523 | Third, _a reader's proposed solution may not be correct_. 524 | Sometimes readers will suggest solutions without explaining the problem they identified. 525 | For example, "I think you need a fancy graphic right here." 526 | This suggestion might be a good one, 527 | but you should work to understand the underlying problem. 528 | Perhaps the text became boring, 529 | or perhaps a diagram would help with clarity. 530 | Whatever the case may be, 531 | don't just take the suggestion at face value. 532 | 533 | Finally, _a reader's feedback should be weighted differently based on who they are and what area their feedback is in._ 534 | An expert's feedback regarding correctness should be prioritized over a beginner's, 535 | and an audience member's feedback regarding readability should be prioritized over the expert's. 536 | 537 | ### Compile the feedback into revision notes 538 | 539 | After taking all of this in, 540 | you should have a list of changes to make. 541 | And you may also have a list of positive points which you should try to retain. 542 | Despite the focus on improving problems, 543 | you also don't want to edit away something that is working. 544 | 545 | ## 8. Revise based on feedback 546 | 547 | Just like with your first revision notes, 548 | you will go back through and make your changes one at a time, 549 | evaluating the criteria of clarity, concision, organization, scannability and goal focus. 550 | By the time you are done, 551 | you should have a pretty solid draft. 552 | 553 | As you revise, make changes one at a time, 554 | assessing clarity, concision, organization, scannability, and goal focus. 555 | Pay attention to common themes in the feedback you receive. 556 | Those are the ones most likely to require revision. 557 | 558 | Remember to prioritize the most significant changes first. 559 | Tackle issues like organization or persuasiveness before minor details like grammar. 560 | Getting something grammatically correct is a waste of time 561 | if that entire section will be reworked or removed. 562 | Throughout revision, continuously review your goals and audience, 563 | and be prepared for multiple rounds of changes. 564 | 565 | After you've revised, seek additional feedback, 566 | both to verify initial concerns are addressed 567 | and to ensure no new problems have been introduced. 568 | Consult with both original and new readers to get different perspectives. 569 | Remember, continuous revision is crucial for producing a polished and effective piece of writing. 570 | 571 | ## 9. Publish 572 | 573 | Publishing your work is usually the most critical part of the writing process, 574 | but remember, it can happen at any point, depending on the project. 575 | In some cases, publishing might be part of an ongoing process, 576 | with updates and revisions made after the first release. 577 | This is especially true for things like wiki-based technical documentation 578 | or presentations that are given multiple times and need tweaks based on audience feedback. 579 | 580 | Before you publish, think about the context and the level of polish your writing needs. 581 | Not every situation calls for super polished writing. 582 | A quick email to a coworker won't need the same level of refinement as a formal report for the big boss. 583 | Adapt the level of polish to the context so you strike the right balance between quality and getting things done. 584 | 585 | Before hitting that publish button, 586 | give your work a once-over for any errors or inconsistencies, 587 | and make sure your formatting looks good, 588 | based on what's required in your situation. 589 | This final check is crucial for presenting your writing in the best light. 590 | 591 | After publishing, don't forget to spread the word. 592 | Share your work with the people who need to see it, 593 | whether that's passing a report to colleagues, 594 | posting a blog on social media, 595 | or presenting your findings at a conference. 596 | Keep in mind that publishing doesn't mean you're done with revisions. 597 | Be open to feedback even after your work is out there, 598 | and be ready to make updates or corrections as needed. 599 | Embracing this back-and-forth will help you keep improving your writing skills 600 | and adapt to the needs of your audience in all kinds of situations. 601 | 602 | ## Summing it up 603 | 604 | The writing process is all about striking the right balance between the formal process and the needs of the situation. 605 | 606 | From setting goals and figuring out who you're writing for, 607 | to making an outline, 608 | revising, 609 | and hitting that publish button, 610 | each step is important. 611 | But not equally important. 612 | 613 | Keep an eye on your context and switch things up as needed, 614 | whether it's the level of polish or the amount of feedback and revision you're going for. 615 | Don't forget, the process can and should be trimmed down when it makes sense for the situation, 616 | helping you focus on what's most important in each specific context. 617 | The more you work at it, the better you'll get at writing and connecting with your audience. 618 | 619 | Writing takes time, practice, and being open to learning from both the things you nail and the things you, well, don't. 620 | As you gain experience as a writer, 621 | you'll find that this whole process starts to feel more natural, 622 | and your ability to get your point across will only get better. 623 | Just keep writing, learning, and tweaking your process to fit each unique writing situation you find yourself in. -------------------------------------------------------------------------------- /manuscript/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "General Rules", 3 | "eleventyNavigation": { 4 | "key": "General Rules", 5 | "order": 2 6 | } 7 | } -------------------------------------------------------------------------------- /manuscript/rules.md: -------------------------------------------------------------------------------- 1 | # General Rules 2 | 3 | Rules. 4 | The very word can trigger skepticism. 5 | And understandably so. 6 | We are often confronted with seemingly arbitrary rules 7 | which make our lives harder for no apparent reason. 8 | 9 | In this chapter I will give you rules to follow, 10 | but I will also explain each rule's rationale. 11 | So if you find yourself in a situation beyond the scope of the rule, 12 | you'll be equipped to make intelligent decisions about how or if to apply it. 13 | 14 | It is also important to acknowledge the limits of these rules. 15 | They are targeted principally at writing within a professional business context. 16 | If you are writing a novel, 17 | these rules will be of limited use. 18 | And if you are writing poetry, 19 | they will probably be entirely useless. 20 | But if you need to communicate ideas to your coworkers, 21 | then these rules will serve you well. 22 | 23 | Now that we've set the appropriate context, let's look at those rules. 24 | 25 | ## 1. Be goal oriented. 26 | 27 | > "In my end is my beginning." 28 | > 29 | > -- T.S. Eliot 30 | 31 | ### Everything you write has a goal 32 | 33 | Suppose a boss or client came to you and said they needed you to build a web application. 34 | Your first question would probably be, 35 | "What is the application for?" 36 | The very idea of writing an application without knowing its purpose is ridiculous. 37 | Yet when it comes to writing prose, 38 | we too often fail to ask the obvious question. 39 | 40 | Everything you write has a goal, 41 | whether explicit or implicit. 42 | Those API docs? 43 | The stated goal is to inform other developers how to use your library. 44 | That weekly status report you email your client? 45 | The unstated goal is reassuring them that their money is being well spent. 46 | 47 | The goal of a document isn't always obvious, 48 | but in the next chapter, we'll look at some tools to figure it out. 49 | 50 | ### Knowing the goal equips you to fulfill it. 51 | 52 | Let's go back to our hypothetical web application above. 53 | Suppose you did try to build it without actually knowing its purpose? 54 | You are just given a list of features and told to start building. 55 | What would happen? 56 | 57 | You'd have no way to evaluate how well or poorly the application was doing. 58 | And that's not just a problem at the end. 59 | All along you'd have questions about how best to build each feature. 60 | But without knowing the application's purpose, 61 | you'd have no way of choosing between the alternatives. 62 | That kind of uncertainty is crippling. 63 | And I suspect that is why many people avoid writing. 64 | 65 | But when you do know the goal you are writing toward, 66 | decisions are easier, 67 | and it is simpler to tell whether your writing succeeds in achieving its end. 68 | 69 | ### The structure of goals 70 | 71 | We've talked about goals a lot, 72 | but so far we haven't really clarified what kind of goals we're talking about. 73 | It's all about the document. 74 | 75 | In this book, 76 | when I talk about goals, 77 | I mean the purpose intrinsic to the document[^telos], 78 | not the author's personal motivations. 79 | For instance, 80 | your goal in writing API docs may be to keep your tech lead from bugging you about it yet again. 81 | But the purpose of the docs themselves is to assist users of the API. 82 | 83 | A document's goals will generally fall into one of two categories: 84 | to inform or to persuade. 85 | Most documents will include a bit of both, 86 | but one will be primary. 87 | To go back to the API docs example, 88 | it may help persuade people to use your library, 89 | but the primary purpose is just to inform them about how to do so. 90 | 91 | [^telos]: If you're familiar with ancient Greek philosophy, 92 | this is what Aristotle would call the document's _telos_. 93 | 94 | ## 2. Be concise. 95 | 96 | In a professional context, 97 | everyone is pressed for time. 98 | Being concise is about respecting your readers' time. 99 | Don't force them to read a page when a paragraph will do. 100 | 101 | Conciseness also helps you achieve your goal. 102 | The more time investment your text requires, 103 | the more reluctant readers will be to give it to you. 104 | So we should do our best to keep the required investment to a minimum. 105 | 106 | At the same time, conciseness does not mean being excessively terse. 107 | Conciseness means saying the exact amount necessary to achieve the goal: 108 | no more and no less. 109 | 110 | ## 3. Be Clear 111 | 112 | ### Avoid ambiguity 113 | 114 | Human language is rife with ambiguity. 115 | Take as an example my first title idea for this book: 116 | "Writing for Developers." 117 | Only three words, 118 | yet it could be read in two totally different ways. 119 | That title could have meant 120 | -- as I intended -- 121 | "A book for developers on the subject of writing." 122 | But another equally reasonable interpretation was, 123 | "A book on writing for an audience of developers." 124 | 125 | Ambiguous language is useful in art and poetry, 126 | but not when we are trying to communicate as efficiently as possible. 127 | 128 | ### Avoid jargon 129 | 130 | It is also wise to avoid unfamiliar terms and jargon. 131 | Of course, this is dependent on your audience. 132 | To a developer, this sentence makes perfect sense: 133 | 134 | > "Hoth is a lightweight MVVM framework 135 | > which leverages dirty checking 136 | > and immutable data structures for performance." 137 | 138 | To a non-developer, 139 | it sounds more like this: 140 | 141 | > Hoth is a lightweight magic framework 142 | > which leverages scary magic 143 | > and scarier magic for performance. 144 | 145 | For readers unfamiliar with the terminology it communicates next to nothing. 146 | 147 | ### Narrow the scope of your words 148 | 149 | We love to speak in generalities. 150 | Perhaps because it is difficult to definitely prove a generality wrong. 151 | The problem is that the more general a statement is, 152 | the more difficult it is to understand and apply. 153 | Consider this example: 154 | 155 | > Object-oriented programming is terrible. 156 | 157 | It's not completely meaningless, 158 | but it is extremely broad, 159 | and the implications are unclear. 160 | Should readers avoid all object oriented languages? 161 | It's hard to tell what the author is thinking. 162 | 163 | Contrast with this: 164 | 165 | > Class-based inheritance tends to make code unnecessarily complex. 166 | 167 | This version has narrowed the scope in two ways. 168 | It has narrowed from all object-oriented programming to class-based inheritance. 169 | And it has narrowed from "terrible" to "tends toward unnecessary complexity." 170 | As a result, 171 | this sentence is much easier to understand and evaluate. 172 | 173 | Given the choice between the specific and the general, choose the specific. 174 | 175 | ## 4. Be organized. 176 | 177 | For many people this is the most difficult part. 178 | They know the goal. 179 | They can write clear, concise sentences. 180 | But they have a hard time putting those pieces together into a coherent whole. 181 | 182 | In the next chapter we'll look at how to do it. 183 | For now let's explore why. 184 | 185 | ### Structure aids comprehension 186 | 187 | The human mind is built for pattern matching. 188 | And it is pretty good at its job, 189 | otherwise we'd have all been eaten by tigers long ago. 190 | 191 | Consider two different lists: 192 | 193 | * Apple pie 194 | * Blueberry pie 195 | * Cranberry pie 196 | 197 | * Wrench 198 | * Justice 199 | * Puppy 200 | 201 | The first list is much more memorable and comprehensible. 202 | Why? 203 | Because it is organized into a structure that our minds can easily extract. 204 | Each item in the list was a pie. 205 | Each of the pies was fruit-based. 206 | And the pies were alphabetically ordered. 207 | But the second list had no unifying organizational structure. 208 | 209 | ### Structure is fractal 210 | 211 | Writing structures are simultaneously high level and low level. 212 | They encompass everything 213 | from your three main points 214 | to that sub-sub-sub-point in paragraph 45. 215 | And if you've chosen your structure well, 216 | the micro and macro levels will tend to mirror each other. 217 | For example, consider this outline for an article on the Hoth Framework. 218 | 219 | > * Intro to Hoth 220 | > * Immutable data structures 221 | > * Intro 222 | > * Benefits 223 | > * Examples 224 | > * Dirty checking 225 | > * Intro 226 | > * Benefits 227 | > * Examples 228 | > * Example application 229 | > * Conclusion 230 | 231 | Both of the main sub-points follow the same structure as the outline as a whole. 232 | The mirroring isn't always this obvious, 233 | but it is generally present. 234 | 235 | ### Projects may come with built-in structure 236 | 237 | Certain kinds of projects come with ready-made, 238 | very detailed structures, 239 | simply because of how common that type of project is. 240 | API docs, 241 | for example, 242 | may vary a bit by language, 243 | but will generally look pretty close to this. 244 | 245 | * Modules 246 | * Name 247 | * Description 248 | * Classes 249 | * Name 250 | * Description 251 | * Methods 252 | * Name 253 | * Arguments 254 | * Return value 255 | * Description 256 | 257 | Not every predetermined structure is that specific, though. 258 | Library websites have several things they need to include, 259 | like language, 260 | purpose of the library, 261 | and how to install it, 262 | but they also have more flexibility. 263 | 264 | ### General purpose structures 265 | 266 | In addition to these very specific structures, 267 | there are also many general purpose structures. 268 | These are things like the inverted pyramid, objection-response, etc. 269 | 270 | Typically you will pick two or three of these general purpose structures 271 | and use them as the organizing principle of the project. 272 | 273 | ## 5. Be scannable. 274 | 275 | While the previous rule was about the conceptual structure of a text, 276 | this rule is about the visual typographic structure. 277 | The point of scannability is to allow readers to locate important information at a glance, 278 | rather than reading every word on the page. 279 | Even for readers that do read every word, 280 | scannable typography makes it easier to refer back to key ideas in the text. 281 | 282 | ### Break text into bite-sized pieces 283 | 284 | Have you ever read a paragraph that took up an entire printed page? 285 | They are no fun to read. 286 | With no visual breaks, 287 | it is difficult to keep track of where you are, 288 | much less follow the flow of ideas. 289 | 290 | To maximize scannability, 291 | text should be broken into bite-sized units of thought. 292 | Paragraphs, for example, should generally be 2-5 sentences long. 293 | Much longer and it becomes difficult to read. 294 | Shorter and you may want to use a heading instead. 295 | 296 | ### Divide and conquer with headings 297 | 298 | The primary purpose of headings is to make your text's conceptual structure explicit. 299 | However, they also have two important scannability implications. 300 | 301 | First, headings provide a visual anchor allowing readers to locate where a concept is discussed. 302 | 303 | Second, headings breaks the text into units of progress which help the reader stay motivated to continue. 304 | This is part of the reason list posts are such a popular article format. 305 | Each time the reader reaches the next heading, 306 | they get to cross something off their mental checklist. 307 | 308 | ### Use lists for your lists 309 | 310 | While not as common as paragraphs, 311 | the humble list is one of the author's most helpful tools. 312 | It provides: 313 | 314 | * Visual attraction for the list 315 | * Visual attraction for each item in the list 316 | * A break from the monotony of paragraphs 317 | * A sense of progress 318 | 319 | You can, of course, embed lists directly into your sentences and paragraphs. 320 | 321 | > While not as common as paragraphs, 322 | > the humble list is one of the author's most helpful tools. 323 | > It provides: 324 | > visual attraction for the list, 325 | > visual attraction for each item in the list, 326 | > a break from the monotony of paragraphs, 327 | > and a sense of progress. 328 | 329 | However, doing so relegates the list contents to a secondary status. 330 | In general, 331 | using typographic lists 332 | (bulleted, numbered, etc.) 333 | for your conceptual lists makes sense if the exact contents are worth emphasizing. 334 | 335 | ### Emphasize key ideas with bold or italics 336 | 337 | Ordinary paragraphs are the parts of a text most likely to be skipped over. 338 | But sometimes important ideas only make sense as part of a paragraph. 339 | That's where typographic emphasis comes in. 340 | 341 | Highlighting important ideas with bold or italics allows readers to see key ideas at a glance. 342 | 343 | However, there are some rules of thumb to keep in mind. 344 | 345 | * Only highlight one phrase or sentence per paragraph. 346 | * Don't highlight something in _every_ paragraph. 347 | * **Never** highlight something with both bold and italics. 348 | 349 | Go beyond these limits and readers are likely to feel that you are shouting at them. -------------------------------------------------------------------------------- /manuscript/why.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Why should developers study writing?", 3 | "eleventyNavigation": { 4 | "key": "Why should developers study writing?", 5 | "order": 1 6 | } 7 | } -------------------------------------------------------------------------------- /manuscript/why.md: -------------------------------------------------------------------------------- 1 | # Why should developers study writing? 2 | 3 | If you've picked up this book, then you're probably a software developer. 4 | Furthermore, you're someone who cares about your career. 5 | You've probably spent countless hours 6 | studying manuals, 7 | reading library docs, 8 | learning how to write clean code, 9 | and otherwise mastering your craft. 10 | 11 | But you may be skeptical. 12 | There are a thousand other things you could learn. 13 | Why spend the time studying writing? 14 | Let's walk through some of the things you might be thinking. 15 | 16 | ## I'm paid to code, not to write. 17 | 18 | In most cases, developers aren't paid to code. 19 | We are paid to produce working software that solves a business problem. 20 | In order to do that we may need to: 21 | 22 | * Email a product manager to clarify a requirement 23 | * Instant message a designer to verify the color and placement of a call-to-action button 24 | * Document the details of an internal REST API 25 | * Submit a patch to an open-source project you use 26 | * File a bug report about a part of the software developed by another team 27 | * Provide explanatory notes about that spike in server errors on the 14th 28 | * Tweak the wording of some UI text that is confusing users, 29 | since the designer is unavailable 30 | 31 | All of these things are run of the mill tasks for a software developer. 32 | And every single one of them is a form of writing. 33 | 34 | ## I already know how to write. I did well in school. 35 | 36 | Unfortunately, literacy, 37 | even at college or university level, 38 | doesn't guarantee the ability to write effectively. 39 | Many degree programs place little emphasis on writing. 40 | And those that do emphasize it often encourage an academic style which doesn't mesh well with most developer's jobs. 41 | 42 | Academic writing tends to value a kind of distant objectivity which can manifest as: 43 | 44 | * Unnecessary jargon 45 | * Verbose and indirect prose 46 | * Extreme formality 47 | 48 | In contrast, most developers would benefit from making their prose clear, direct, and concise. 49 | 50 | ## But I could be studying the Hoth framework instead. It's so cool! 51 | 52 | Yes, no matter which topic you decide to study, 53 | you will be passing up something else. 54 | But writing deserves special consideration. 55 | Why? 56 | **Because writing is a durable skill.** 57 | 58 | Technology changes quickly. 59 | If you're not careful, 60 | you can easily invest a lot of time and energy into a technological one hit wonder. 61 | Writing, though, will never be rendered obsolete by the fickle winds of technology or fashion. 62 | 63 | **Writing is also a transferable skill.** 64 | If you decide to change careers and become a 65 | realtor, 66 | landscape artist, 67 | or -- God forbid! -- a manager, 68 | your expertise in the Hoth MVVM framework will be of limited use. 69 | But writing will remain. 70 | 71 | ## But what will I get out of studying writing? 72 | 73 | ### Less back and forth 74 | 75 | We've all experienced out of control email threads. 76 | It starts with a vague question, 77 | is usually followed by an ambiguous answer, 78 | and before you know it, 79 | someone in another department is freaking out about a non-existent problem. 80 | 81 | What if you could step in and make people understand? 82 | Even better, what if you could prevent the issue from escalating in the first place? 83 | That's the power of clear and effective writing. 84 | 85 | ### Fewer bugs and less rework 86 | 87 | Sometimes we run across frustratingly mysterious bits of code. 88 | Perhaps we've even written some ourselves. 89 | But what's even more frustrating 90 | is when the code is accompanied by an equally mysterious comment like, 91 | "fixes IE bug." 92 | 93 | What IE bug? And which version of IE? 94 | 95 | A developer working on this code, now needs to: 96 | 97 | * Talk to the original programmer, 98 | who won't remember anything. 99 | * Manually test to see if there are obvious bugs related to that line, 100 | which he probably won't catch. 101 | * And perhaps make changes anyway, 102 | without knowing what he broke. 103 | 104 | A clearly written comment would have prevented all of that. 105 | 106 | ### Protect your development flow 107 | 108 | Talking face to face is a valuable and necessary part of our jobs. 109 | But not every face to face conversation is valuable. 110 | 111 | If you find yourself explaining certain things over and over again 112 | (perhaps because you're an expert in a particular subsystem) 113 | then you should write it down in a public location like a team wiki. 114 | If your explanations are clear and easy to understand, 115 | you'll have fewer interruptions messing with your development flow. 116 | 117 | ### Recognition of your expertise 118 | 119 | Perhaps you have a different problem. 120 | You have a deep knowledge of some tool, library, or system, 121 | but no one realizes it. 122 | Writing guides, tutorials, and presentations 123 | can help you achieve greater recognition, 124 | both inside and outside your company. 125 | 126 | ### Greater trust from managers 127 | 128 | Watching a software development project from the outside can be a frustrating experience. 129 | This is doubly true if you are on the hook when something goes wrong. 130 | 131 | A developer who can clearly articulate the state of the project, 132 | without getting bogged down in technical details, 133 | is invaluable. 134 | 135 | If you are that developer, 136 | you will quickly earn the trust of your manager and non-technical colleagues. 137 | 138 | ### Improve your own understanding 139 | 140 | > "How can I tell you what I think till I see what I say?" 141 | > 142 | > -- E.M. Forster, _Aspects of the Novel_ 143 | 144 | The process of writing takes our often fuzzy and unformed ideas, 145 | and shapes them into something clear enough to communicate to others. 146 | This makes writing an excellent way to deepen our own understanding of a subject. 147 | Sometimes the improvement comes from research, 148 | but often it comes simply due to organizing our own thoughts. 149 | 150 | ## What now? 151 | 152 | Hopefully by this point you can see why studying writing is worthwhile. 153 | In the next chapter, 154 | we'll launch into some general rules you can use to improve your writing. -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "_site" 3 | command = "npm run build" 4 | #command = "npm run build && set -e && find ./_site -type f -name '*.html' -exec staticrypt -f password_template.html {} $PASSWORD -o {} \\;" 5 | functions = "functions" 6 | 7 | # REDIRECT and HEADERS examples 8 | 9 | # Redirect rule example 10 | # For more information see:- https://www.netlify.com/docs/netlify-toml-reference/ 11 | 12 | #[[redirects]] 13 | # from = "/*" 14 | # to = "/blog/:splat" 15 | 16 | # The default HTTP status code is 301, but you can define a different one e.g. 17 | # status = 302 18 | 19 | # Headers rule example 20 | # For more information see:- https://www.netlify.com/docs/netlify-toml-reference/ 21 | 22 | #[[headers]] 23 | # Define which paths this specific [[headers]] block will cover. 24 | # for = "/*" 25 | 26 | #[headers.values] 27 | # X-Frame-Options = "DENY" 28 | # X-XSS-Protection = "1; mode=block" 29 | # Content-Security-Policy = "frame-ancestors https://www.facebook.com" 30 | 31 | # Redirects and headers are GLOBAL for all builds – they do not get scoped to 32 | # contexts no matter where you define them in the file. 33 | # For context-specific rules, use _headers or _redirects files, which are 34 | # applied on a PER-DEPLOY basis. 35 | -------------------------------------------------------------------------------- /outline.md: -------------------------------------------------------------------------------- 1 | # Prose for Programmers Outline 2 | 3 | Checked boxes indicate that the initial draft of a section is complete. 4 | 5 | ## [Why should developers learn to write?](manuscript/why.md) 6 | 7 | - [x] Objections 8 | - [x] Paid to code, not write 9 | - [x] Laundry list of writing developers have to do 10 | - [x] Coding is writing 11 | - [x] I already know how to write. I did well in school. 12 | - [x] Literacy, even college level, doesn’t mean your writing is good 13 | - [x] Academics can even foster bad writing habits 14 | - [x] But I could be learning technology X instead 15 | - [x] Writing is a durable skill 16 | - [x] Writing is a transferable skill 17 | - [x] Benefits 18 | - [x] fewer back and forth emails/chats 19 | - [x] Fewer bugs due to misunderstood code/docs 20 | - [x] Fewer interruptions from coworkers with questions (keep flow) 21 | - [x] More understanding/empathy from managers 22 | - [x] Recognition of your knowledge 23 | - [x] Increase your own understanding 24 | 25 | ## [General Rules](manuscript/rules.md) 26 | 27 | - [x] Be goal oriented 28 | - [x] Everything you write has a goal or purpose 29 | - [x] may be implicit or explicit 30 | - [x] Understanding the goal promotes better writing 31 | - [x] Structure of goals 32 | - [x] 2 types 33 | - [x] inform 34 | - [x] persuade 35 | - [x] Most writing serves both to one degree or other 36 | - [x] But most writing has a primary goal, with others as secondary 37 | - [x] Be concise 38 | - [x] Not short or terse 39 | - [x] Everything contributes to the goal 40 | - [x] Avoids wordiness for the sake of the goal 41 | - [x] Be clear 42 | - [x] Avoid unfamiliar or unnecessary jargon 43 | - [x] Avoid ambiguity 44 | - [x] Be precise 45 | - [x] Be organized 46 | - [x] Human mind uses patterns to extract meaning 47 | - [x] Structure is fractal: macro and micro mirror each other 48 | - [x] Projects may come with built-in structure (API docs) 49 | - [x] Several general purpose structures available 50 | - [x] Be scannable 51 | - [x] Typographic structure to direct attention to ideas 52 | - [x] Break up text into digestible chunks 53 | - [x] use headlines 54 | - [x] use lists 55 | - [x] Make use of bold and italic to highlight points 56 | 57 | ## [The Writing Process](manuscript/process.md) 58 | 59 | - [x] Determine the goal 60 | - [x] Why am I writing this? 61 | - [x] Purpose of doc 62 | - [x] Not personal motivations 63 | - [x] Keep asking why 64 | - [x] Determine audience 65 | - [x] Who will be reading this? 66 | - [x] Given the goals, which readers are the primary audience? 67 | - [x] What is their relation to the subject? 68 | - [x] What is their relation to you? 69 | - [x] Examples of how audience affects writing 70 | - [x] Choose a suitable structure 71 | - [x] Different structures are suitable for different goals 72 | - [x] Hierarchical structures are often good for informational 73 | - [x] Narrative is good for persuasive 74 | - [x] Different structures for different audiences 75 | - [x] Expertise in topic 76 | - [x] Shallow bullet points 77 | - [x] Deep information hierarchy 78 | - [x] Level of interest 79 | - [x] Relationship 80 | - [x] Can mix and match 81 | - [x] Examples 82 | - [x] Build an outline 83 | - [x] Structure may come with an outline template (API docs) 84 | - [x] List of the ideas you want to communicate 85 | - [x] Evaluate outline based on goal and audience 86 | - [x] Revise outline 87 | - [x] Revising earlier is cheaper 88 | - [x] Turn outline into prose 89 | - [x] Put flesh on the bones 90 | - [x] Muscles: human connection/emotion for strength 91 | - [x] Connective tissue: transitions between ideas 92 | - [x] Not necessarily exposition 93 | - [x] Stories 94 | - [x] Jokes 95 | - [x] Revise 96 | - [x] Evaluate 97 | - [x] Does this unit advance the goal? 98 | - [x] Is it concise? 99 | - [x] Is it clear? 100 | - [x] Is it organized? 101 | - [x] Is it scannable? 102 | - [x] Plan a correction 103 | - [x] Make corrections 104 | - [x] Get feedback 105 | - [x] Types of readers 106 | - [x] Target audience 107 | - [x] Experts 108 | - [x] Friends 109 | - [x] Evaluating feedback 110 | - [x] Readers are always right about their reactions 111 | - [x] Readers may be wrong about the problems and solutions they identify 112 | - [x] Bear in mind what kind of reader this is 113 | - [x] Produce a list of problems to address and good points to keep 114 | - [x] Revise based on feedback 115 | - [x] See previous 116 | - [x] Publish 117 | - [x] May be done at any point (depending on project) 118 | - [x] Can be part of an iterative process 119 | - [x] Tell people about it 120 | - [x] Not the end of revision 121 | - [x] Conclusion 122 | - [x] Abbreviate process as appropriate 123 | 124 | ## Writing Structures 125 | 126 | - [ ] General Structures 127 | - [ ] Inverted Pyramid 128 | - [ ] what it is 129 | - [ ] why it is useful (time saving) 130 | - [ ] high level 131 | - [ ] low level (topic sentences) 132 | - [ ] List 133 | - [ ] Sequence 134 | - [ ] Q & A ? 135 | - [ ] Informational Structures 136 | - [ ] Hierarchy 137 | - [ ] Examples 138 | - [ ] Textbooks 139 | - [ ] API Docs 140 | - [ ] Persuasive structures 141 | - [ ] Objection - response 142 | - [ ] Persuasive version of Q&A 143 | - [ ] Problem - solution 144 | - [ ] Story 145 | - [ ] Syllogism 146 | - [ ] Juxtaposition (attention grabbers) 147 | - [ ] Combine as appropriate 148 | - [ ] Examples 149 | 150 | ## Audience 151 | 152 | - [ ] Aspects of audience 153 | - [ ] Expertise in subject 154 | - [ ] Relationship to you 155 | - [ ] Motivation for reading 156 | - [ ] Emotional state 157 | - [ ] Level of caution/skepticism 158 | - [ ] Common audiences for developers 159 | - [ ] Other developers 160 | - [ ] Somewhat similar outlook on the world 161 | - [ ] Not necessarily the same as you 162 | - [ ] Experience 163 | - [ ] Different technologies 164 | - [ ] Corporate vs startup 165 | - [ ] QA & testers 166 | - [ ] Designers 167 | - [ ] Teammates 168 | - [ ] Non-technical peers 169 | - [ ] Your boss 170 | - [ ] Management 171 | - [ ] Ops/SysAdmin 172 | - [ ] Users 173 | - [ ] Clients 174 | - [ ] Customers 175 | 176 | ## Genres 177 | 178 | - [ ] Genres 179 | - [ ] Email 180 | - [ ] Docs 181 | - [ ] Code Comments 182 | - [ ] Blog posts 183 | - [ ] Chat? 184 | - [ ] Project site 185 | - [ ] Case for new technology 186 | - [ ] Requirements 187 | - [ ] Bug reports 188 | - [ ] Error messages 189 | - [ ] Help articles 190 | - [ ] UI text 191 | - [ ] Commit messages 192 | - [ ] Code review 193 | - [ ] Code? 194 | - [ ] Talks/presentations? 195 | 196 | ## Other Resources 197 | 198 | - [ ] Books 199 | - [ ] The Elements of Style by Strunk & White 200 | - [ ] The Economist Style Guide 201 | - [ ] Steering the Craft by Ursula K. Le Guin 202 | - [ ] On Writing Well by William Zinsser 203 | - [ ] Writing That Works by Ogilvy? 204 | - [ ] Team Geek? 205 | - [ ] Applications & Tools 206 | - [ ] Workflowy 207 | - [ ] Writeroom 208 | - [ ] Writemonkey 209 | - [ ] IA Writer 210 | - [ ] Hemingway? 211 | - [ ] write-good npm library? -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacebook", 3 | "version": "1.0.0", 4 | "description": "A simple site generator based on Eleventy, Tailwind 2.0, and Alpine.js", 5 | "scripts": { 6 | "start": "eleventy --serve & postcss styles/tailwind.css --o _tmp/style.css --watch", 7 | "build": "ELEVENTY_PRODUCTION=true eleventy && NODE_ENV=production postcss styles/tailwind.css --o _site/style.css && ./node_modules/.bin/cleancss -o _site/style.css _site/style.css", 8 | "watch": "npx eleventy --watch", 9 | "debug": "DEBUG=* npx eleventy" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/broeker/spacebook.git" 14 | }, 15 | "author": "Tim Broeker (https://www.electriccitizen.com/)", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/broeker/spacebook/issues" 19 | }, 20 | "homepage": "https://github.com/broeker/spacebook", 21 | "devDependencies": { 22 | "@11ty/eleventy": "^0.11.1", 23 | "alpinejs": "^2.7.3", 24 | "eleventy-plugin-lazyimages": "^2.1.0", 25 | "eslint": "^7.9.0", 26 | "lazysizes": "^5.2.2", 27 | "luxon": "^1.25.0", 28 | "markdown-it": "^10.0.0", 29 | "markdown-it-anchor": "^5.3.0", 30 | "markdown-it-image-lazysizes": "^1.0.0", 31 | "postcss-cli": "^8.3.0", 32 | "prettier": "^2.1.2", 33 | "tailwindcss": "^2.0.2" 34 | }, 35 | "dependencies": { 36 | "@11ty/eleventy-img": "^0.5.0", 37 | "@11ty/eleventy-navigation": "^0.1.6", 38 | "@tailwindcss/forms": "^0.2.1", 39 | "@tailwindcss/typography": "^0.3.1", 40 | "autoprefixer": "^10.1.0", 41 | "clean-css": "^4.2.1", 42 | "clean-css-cli": "^4.3.0", 43 | "elasticlunr": "^0.9.5", 44 | "eleventy-plugin-embed-everything": "^1.9.4", 45 | "eleventy-plugin-nesting-toc": "^1.2.0", 46 | "eleventy-plugin-svg-contents": "^0.7.0", 47 | "eleventy-plugin-toc": "^1.1.0", 48 | "emoji-regex": "^9.2.0", 49 | "html-minifier": "^4.0.0", 50 | "markdown-it-attrs": "^3.0.3", 51 | "markdown-it-center-text": "^1.0.4", 52 | "markdown-it-container": "^3.0.0", 53 | "markdown-it-emoji": "^2.0.0", 54 | "markdown-it-footnote": "^3.0.2", 55 | "markdown-it-for-inline": "^0.1.1", 56 | "markdown-it-linkify-images": "^2.0.0", 57 | "markdown-it-table-of-contents": "^0.5.0", 58 | "markdown-it-task-lists": "^2.1.1", 59 | "postcss": "^8.2.2", 60 | "qs": "^6.9.4", 61 | "remove": "^0.1.5", 62 | "staticrypt": "^1.3.2", 63 | "uglify-es": "^3.3.9", 64 | "url-pattern": "^1.0.3" 65 | }, 66 | "main": ".eleventy.js" 67 | } 68 | -------------------------------------------------------------------------------- /password_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Private Page 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 136 | 142 | 143 | 144 | 145 | 146 |

147 | {crypto_tag} 148 | 149 | 227 | 228 |
229 |
230 |
231 |

🔒 Enter a password to unlock!

232 |
233 | 234 |
235 | 236 |
237 | 244 | 249 |
250 |
251 |
252 | 258 | 259 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require(`tailwindcss`)(`./styles/tailwind.config.js`), 4 | require(`autoprefixer`), 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /robots.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Robots 3 | permalink: /robots.txt 4 | layout: layouts/robots.njk 5 | --- 6 | -------------------------------------------------------------------------------- /search-index.json.njk: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /search-index.json 3 | --- 4 | {{ collections.results | search | dump | safe }} 5 | 6 | 7 | -------------------------------------------------------------------------------- /styles/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | important: true, 5 | future: { 6 | removeDeprecatedGapUtilities: true, 7 | purgeLayersByDefault: true, 8 | }, 9 | purge: { 10 | enabled: true, 11 | content: ["_site/**/*.html"], 12 | options: { 13 | safelist: [], 14 | }, 15 | }, 16 | darkMode: 'class', 17 | theme: { 18 | container: { 19 | center: true, 20 | }, 21 | extend: { 22 | typography: { 23 | DEFAULT: { 24 | css: { 25 | maxWidth: '100%', 26 | a: { 27 | color: '#1D4ED8', 28 | '&:hover': { 29 | color: '#1E3A8A', 30 | }, 31 | }, 32 | '.prose a.edit, .tag a': { 33 | color: '#333', 34 | 'text-decoration': 'none', 35 | }, 36 | 'ul.footer-nav': { 37 | '::before': { 38 | display: 'none', 39 | 'text-decoration': 'none', 40 | } 41 | }, 42 | 'ul.contains-task-list': { 43 | '::before': { 44 | display: 'none', 45 | } 46 | }, 47 | 'ul.spacelog': { 48 | '::before': { 49 | display: 'none', 50 | } 51 | }, 52 | }, 53 | }, 54 | } 55 | }, 56 | }, 57 | variants: {}, 58 | plugins: [ 59 | require('@tailwindcss/typography'), 60 | require('@tailwindcss/forms'), 61 | ], 62 | } -------------------------------------------------------------------------------- /styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | 7 | /* Set up some default image behavior for nicer images */ 8 | img { 9 | @apply w-auto shadow-md border-2 border-transparent !important 10 | } 11 | img:hover { 12 | @apply border-2 border-gray-100 13 | } 14 | /* Overrides for Tailwind Typography prose class */ 15 | .prose a { 16 | @apply dark:text-gray-400 17 | } 18 | .prose a:hover { 19 | @apply dark:text-gray-500 20 | } 21 | .prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 .prose hr, .prose strong { 22 | @apply dark:text-gray-400 23 | } 24 | .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 { 25 | scroll-margin-top: 3.5em; 26 | } 27 | .prose pre code { 28 | @apply overflow-x-auto !important 29 | } 30 | .prose .footer-nav a { 31 | @apply no-underline !important 32 | } 33 | .prose ul.contains-task-list, 34 | .prose ul.spacelog { 35 | @apply list-none -ml-6 !important; 36 | } 37 | .prose ul.contains-task-list .task-list-item, 38 | .prose ul.spacelog { 39 | ::before { 40 | @apply hidden !important; 41 | } 42 | } 43 | 44 | /* Define blockquotes and some standard callout blocks */ 45 | blockquote { 46 | @apply rounded-lg p-4 bg-gray-100 dark:bg-gray-500 border-gray-200 border-l-8 dark:border-gray-700; 47 | } 48 | .callout { 49 | @apply px-8 py-4 mb-4 rounded-lg bg-yellow-50; 50 | } 51 | .callout, .callout strong, .callout em { 52 | @apply dark:bg-gray-400 dark:text-gray-900; 53 | } 54 | .callout-blue { 55 | @apply px-8 py-4 mb-4 rounded-lg bg-blue-50; 56 | } 57 | .callout-blue, .callout-blue strong, .callout-blue em { 58 | @apply dark:text-gray-200 dark:bg-blue-900; 59 | } 60 | .callout-pink { 61 | @apply px-8 py-4 mb-4 rounded-lg bg-pink-50; 62 | } 63 | .callout-pink, .callout-pink strong, .callout-pink em { 64 | @apply dark:text-gray-200 dark:bg-pink-900; 65 | } 66 | .callout-green { 67 | @apply px-8 py-4 mb-4 rounded-lg bg-green-50; 68 | } 69 | .callout-green, .callout-green strong, .callout-green em { 70 | @apply dark:text-gray-200 dark:bg-green-900; 71 | } 72 | .warning { 73 | @apply px-8 py-4 mb-4 rounded-lg bg-red-800 text-gray-50; 74 | } 75 | .warning, .warning strong, .warning em { 76 | @apply text-gray-50 dark:bg-red-900 dark:text-gray-200; 77 | } 78 | 79 | /* Overrides for nav/Table of Contents block */ 80 | nav ul { 81 | @apply ml-0 text-gray-500; 82 | } 83 | nav ul ul { 84 | @apply ml-6 text-gray-500; 85 | } 86 | nav ul li a { 87 | @apply mb-1 pt-2 pr-4 pb-1 pl-2 w-full block text-gray-500 dark:text-gray-500; 88 | } 89 | nav ul li a:hover { 90 | @apply text-gray-900 dark:text-gray-400; 91 | } 92 | nav ul li a.active { 93 | @apply font-semibold; 94 | } 95 | nav.toc ol li { 96 | @apply pt-2 !important 97 | } 98 | nav.toc ol li li { 99 | @apply pt-2 ml-4 100 | } 101 | nav.toc ol li a { 102 | @apply text-gray-500 103 | } 104 | nav.toc ol li a:hover { 105 | @apply text-gray-900 106 | } 107 | .prose .footer-nav a:hover { 108 | @apply dark:text-gray-400 !important 109 | } 110 | 111 | /* Utilities and misc */ 112 | .adjust p img, .adjust img, .adjust p iframe, .adjust iframe, .twitter-tweet { 113 | @apply shadow-md ml-auto mr-auto p-2 !important 114 | } 115 | .adjust p img:hover, .adjust img:hover { 116 | @apply shadow-xl 117 | } 118 | .adjust img.button { 119 | @apply w-auto shadow-none !important 120 | } 121 | .text-align-center { 122 | @apply flex justify-center; 123 | } 124 | .icon-spacer { 125 | width: "24px"; 126 | } 127 | input { 128 | @apply dark:text-gray-400 129 | } 130 | } 131 | 132 | blockquote p::before { 133 | content: '' !important; 134 | } 135 | 136 | blockquote p::after { 137 | content: '' !important; 138 | } 139 | 140 | blockquote>p:first-child { 141 | margin-top: 0; 142 | } 143 | 144 | blockquote>p:last-child { 145 | margin-bottom: 0; 146 | } -------------------------------------------------------------------------------- /uploads/uws2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshuacc/prose-for-programmers/fc0c8a4a093f406d380768aa626615307fabf707/uploads/uws2.png --------------------------------------------------------------------------------