Ensimmäisessä postauksessa seikkailemme metatasolla kurkkien, kuinka tämä saitti onkaan rakennettu.
Rakkaat! Tervetuloa blogiini! Kyllä, aion puhutella lukijakuntaani rakkaina, sillä let’s face it: ei täällä muutamaa sukulaista lukuunottamatta varmaan kovin moni tule aikaansa viettämään. Koskapa kirjoittelen koodia elikäs devaan elikäs ohjelmoitsen, työkseni, on luonnollisesti kotisivussa tahi blogissa tärkeintä se, miten se on teknisesti toteutettu. Otin tämän tällaisena kesäprojektina ja ennen lomia aloitin keskustelua eräässä vapaamuotoisessa slack-ryhmässämme:

Aihe jakaa siis mielipiteitä 😀
Olen tehnyt tämän kotisivun varmaan viidesti: ainakin kahdella static site generatorilla, muutamalla cms:llä sekä pari kertaa vuella (nuxt.js) sekä clojurescriptillä. Yleensä homma on tyssännyt siihen, että kun olen saanut teknologian toimimaan, huomaan, että eihän mulla oikeastaan ole blogiin mitään sanottavaa. Nyt kuitenkin muutamia harrasteprojekteja on kertynyt, joten lätisen niistä tänne.
WordPressiä bäkkäriin, ClojureScriptiä fronttiin
Tein eräässä vaiheessa työurallani teknistä esiselvitystä erään organisaation vaatimasta cms-valinnasta. Muistan tällöin kysyneeni minua avustavalta konsultilta, että onko todella niin, että jos haluan, että sisällöntuottajat pääsevät käyttämään wordpressiä tai vaikka drupalia, on frontti myös väännettävä sinne, että eikö ole mitään mahdollisuutta käyttää frontille jotain vähän modernimpaa tekkiä (mikä tuolloin tarkoitti käytännössä Angular- tai Ember-appista). Kuulemma ei ollut. Jäin silloin jo miettimään tätä haastetta siinä, että toisaalta viestinnän parissa toimivat ihmiset osaavat jo käyttää wordpressiä, mutta sivustollahan on usein sitten paljon muutakin, kuin pelkkä blogi, esimerkiksi erilaisia integraatioita tai muuten semmosta appishenkistä toimintaa. Näitäkin toki nohevat wp-konsultit osaavat rakennella custom plugareiksi, mutta toisalta, jos henkilö on esmes innostunut käyttämään reactia, vue:ta tahi vaikkapa clojurescriptiä, niin ongelma on ilmeinen.
Myöhemminhän tuon esiselvityksen jälkeeen nousi puheenaiheeksi käsite “headless-cms”, joista tunnetuin lienee contentful. Ajatuksena on, että cms hoitaa sen, minkä cms osaa parhaiten eli sisällönhallinnan, mutta tiedon näyttämiseen käytetään kulloinkin haluttua teknologiaa. Tällöin varsinaisen saitin tekin uusiminen muutamien vuosien välein, joka nyt vaan on valitettava tosiasia, ei aiheuta valtaisaa migraatiohässäkkää, sillä ne sisällöt voivat edelleen asustella siellä cms:ssä, josta niitä sitten noukitaan REST-apin yli sopivaan näyttökerrokseen.
Vaikka contentful olisi mukava, se on kuitenkin myös maksullinen ja koska koitan pitää harrasteprojektieni kustannuskset about nollassa, halusin ilmaisen vaihtoehdon. Hence, wordpress.
WordPressissä on siis jo jonkin aikaa ollut yksinkertainen api, josta voit kysellä postauksia, käyttäjiä younameit. Voit käyttää julkista apia hakemaan postaukset, mutta plugareita löytyy myös priva-apeille, joihin voi vaikka tyrkätä apikeyn tai Oauthin. Elikäs sen varsinaisen wordpressaitin voi pitää vaikka ihan privana ja hakea sitten autentikoituneena kamat appikseensa.
Noh, tämä saittihan nyt on julkinen ilman mitään sisäänkirjautumisia ja pelkkä fronttikökkylä eniveis, joten apikeynhan näkisi helposti developertoolia vilauttamalla. Oma bäkkäri pelkästään blogipostausten hakuun taas tuntui vähän overkilliltä, joten en ryhtynyt moisia askartelemaan. Perustin siis ilmaisen, yksinkertaisen saitin, josta pistin draft-tilaan kaikki paget ja haen tähän postaukset.
Miksi ei staattista koodigenerointia?
Jos tietäisin, niinkun rehellisyyden nimissä kyllä melko varmasti tiedän, että olen ainoa, joka tähän blogiin tulee mitään kirjailemaan, voisin toki käyttää jotain markdownista staattista sivustoa leipovaa kikkaretta. Näitä on monia, itselleni tuttuja lähinnä Hugo, Gatsby ja Jekyll. Monista näistä saa myös aika kivasti suoraan vaikkapa GitHub Pagesiksi läiskäistyä lopputuloksen. Aikoinani käytin Docpadia, joka oli eri kiva, mutta se projekti on valitettavasti sittemmin kuopattu. Staattiset sivugeneraattorit ovat käteviä, jos ei haittaa, että sivu buildataan uudestaan jokaisen muutoksen jälkeen ja jos sisällöntuottajat ovat käteviä gitin ja komentorivin kanssa. Minua on kuitenkin kiehtonut ajatus siitä, että voisin tehdä sivun haluamallani teknologialla ja silti päästää ei-teknisesti orientoituneita ihmisiä toimittamaan osia sivustostani jollain helpohkolla editorilla ilman tarvetta askarrella itse editoria, käyttäjänhallintaa ja kuvapankkia. Tai entäs, jos haluan jostain lomalta blogauttaa vaikka ihan kännykän näytöltä ilman konsoli- ja git accessia? Hence, kaikille tuttu wordpressin admin, josta voi ihan vaan selaimen kautta julkaista uusia kirjoituksia ja kuitenkin vapaus vaikka vaihtaa fronttitekkiä viikoittain, jos niikseen tulee.
Olisihan tämän voinut yksinkertaisemminkin tehdä, mutta halusin ensinnäkin kitsastella postailemalla nämä blogipostaukset ilmaiseen wordpress.comiin ja toisaalta ulkoistaa cms:n ylläpidon wordpress.comille. Tein toki aluksi oman asennuksen, mutta tuntui tyhmältä pyörittää omaa mariadb-instanssia, säätää ssl-sertit ja pyöritellä ja päivitellä wordpressasennusta omassa kontissaan ihan vaan, jotta saan haettua yhdestä apista pari jsonfilua frontille. Nyt saan kaikki wordpress-editorin hyvät puolet käyttöön, mutta kuitenkin rakentaa tämän saitin frontin haluamallani fronttitekillä. Tällä hetkellä se tosiaan on clojurescript + re-frame (eli react), seuraavaksi se saattaa olla Elm tai vaikkapa Vue, enkä joudu välissä tekemään migraatiota vanhasta cms:stä uuteen, sillä postaukset ovat ja pysyvät wordpress.comissa. Jeespox.
WordPress API
WordPress tarjoaa siis rajapinnan, josta voit hakea sivullesi erilaisia asioita. Itse haen tällä hetkellä vain postaukset. Tarjolla on myös rajapinta postausten luomiseen, joten mahdollisuuksia on vaikkapa automaattisesti julkaista postaus kun jossain toisessa järjestelmässä tapahtuu jotain mielenkiintoista. Lisää tietoa ja kaikki apin käytöstä löytääpi helposti wordpressin omasta handbookista, joten turhaapa minä siitä tämän enempää lätisen.
Clojurescript -frontti
Tykkäilen kovasti devailla bäkkäriä clojurella, joten mikäpä estää koodailemasta fronttia samalla tekillä. ClojureScriptille on olemassa react-wrappereitä, joista yksinkertaisin lienee Reagent. Käytännössä se on käsittääkseni vain tapa kirjoitella react-komponentteja clojurescriptillä à la:
(defn simple-component []
[:div
[:p "I am a component!"]
[:p.someclass
"I have " [:strong "bold"]
[:span {:style {:color "red"}} " and red "] "text."]])
Mikäli haluaa vähän parempaa tilanhallintaa, on re-frame varmasti suosituin framework tähän. Re-framen idea on vähän käyttää event looppia:

Re-framesta puhutaan “kuutena dominona”. Sivulla on jonkinlainen universaali tila eli applikaation db ja kun jotain tapahtuu, esimerkiksi käyttäjä klikkaa nappia, lähetetään tästä event tietona eteenpäin. Varsinaisessa näyttökerroksessa sitten tilataan näitä tapahtumia. Esimerkkinä tästä käy esimerkiksi naviointipalkki, jonka tyylittelyyn halutaan tieto siitä, onko kyseinen navigaatio-item aktiivisena vaiko eikö. Me siis tilaamme (subscribe) tietoa siitä, mikä paneeleista on aktiivinen.
Ensimmäinen domino eli “mitä tapahtuu, kun käyttäjä tekee jotain”, on navigointi juureen:
;; routes.cljs
(defroute "/" []
(re-frame/dispatch [::events/set-active-panel :home-panel]))
Tätä vastaava event on rekisteröity aiemmin, tässä tuo db on siis tämä appiksen tilaa pitävä “tietokanta”, jossa aktiivisen paneelin tilannetta muutetaan:
(re-frame/reg-event-db
::set-active-panel
(fn-traced [db [_ active-panel]]
(assoc db :active-panel active-panel)))
Näyttökerroksessa sitten kuikuillaan tuota aktiivisen paneelin muuttumista.
;; views.cljs
(defn- panels [panel-name]
(case panel-name
:home-panel [home-panel]
:about-panel [about-panel]
:blog-panel [blog-panel]
:blogitem-panel [blogitem-panel]
[:div]))
(defn show-panel [panel-name]
[panels panel-name])
(defn main-panel []
(let [active-panel (re-frame/subscribe [::subs/active-panel])]
[show-panel @active-panel]))
Varsinaisessa navigointidivissä puolestaan tarkistetaan, mikä paneeli on kyseessä ja läiskästään oikea css class sen mukaisesti:
;; views.cljs
[(if (= @panel :home-panel)
:a.nav-item.nav-link.active
:a.nav-item.nav-link) {:href "#/"} "Home"]
Koodit tämän sivun fronttiin löytyvät täältä.
Huomattavaa tuota codebasea käytettäessä on, että wordpressin REST-apin url on näppärästi erilainen riippuen siitä, käytätkö omaa instanssia vai ilmaista wordpress.com -versiota. Jälkimmäisessä slugilla tehty GET on esmes muotoa:
…/posts/slug:esimerkkipostauksen-slug
kun taas standalone-asennuksen url on helpommin
…/posts?slug=esimerkkipostauksen-slug
Lisäksi data tulee vähän eri muodossa. Ensimmäisessä sisältö on suoraan content-objektissa:
{
"content": "\n<p><em>Ensimmäisessä postauksessa seikkailemme metatasolla
kurkkien, kuinka tämä saitti onkaan rakennettu......</p>\n",
"excerpt": "<p>Ensimmäisessä postauksessa seikkailemme metatasolla kurkkien,
kuinka tämä saitti onkaan rakennettu.</p>\n",
"slug": "testipostaus-1",
"guid": "https://anskufail.wordpress.com/?p=9",
Jälkimmäisessä tapauksessa on jsonissa myös tuo “rendered”:
"content": {
"rendered": "\n<h2>What is Lorem Ipsum?</h2>\n\n\n\n<p><strong>Lorem
Ipsum</strong> is simply dummy text of the printing and typesetting industry.
Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when
an unknown printer took a galley of type and scrambled it to make a type specimen
book. It has survived not only five centuries, but also the leap into electronic
typesetting, </p>\n",
"protected": false
}
Joten mikäli käytät ensimmäistä eli tuota wordpress.com-versiota, homma toimii suoraan, mutta muuten joudut vähän edaamaan tuotta wordpress-urlia sekä tuota blogpanelia. Eli uniikkina avaimena (jonka re-frame vaatii kun looppaillaan eri itemien yli) on hassusti ID, eikä id.
(defn blog-panel []
(let [data @(re-frame/subscribe [::subs/all-posts-api-response])]
[:div.container
[header]
[:div.container.pt-4
(map (fn [blogitem]
[:div {:key (:ID blogitem)}
[:h2 [:a {:href (str "#/blog/" (:slug blogitem))} (:title blogitem)]]
[:p {:dangerouslySetInnerHTML {:__html (:excerpt blogitem)}}]
[:a {:href "#"}]]) (:posts data))]]))
Lisäksi yksittäisen blogipostauksen näytössä (:content @blogitemdata)
muuttuu muotoon (:rendered (:content @blogitemdata))
:
(defn blogitem-panel []
(let [blogpost-api-response (re-frame/subscribe [::subs/blogpost-api-response])]
[:div.container
[header]
[:div.row
[:div.col-sm
[:h2 (:title @blogpost-api-response)]
[:p {:dangerouslySetInnerHTML {:__html (:content @blogpost-api-response)}}]]]]))
Re-framessa on se hienous, ettei ole tarvetta käyttää minkäänlaista cljs-http-kirjastoa tai vastaavaa http-kutsuihin. Sen sijaan ikäänkuin suoraan boxista on mahdollista käyttää eventtiä, joka tekee apikutsun puolestamme.
(re-frame/reg-event-fx
::get-all-posts
(fn-traced [{:keys [db]} [_]]
{:http-xhrio {:method :get
:uri wp-url
:response-format (json-response-format {:keywords? true})
:on-success [:get-all-posts-success]
:on-failure [:api-request-error :all-posts]}
:db (assoc-in db [:loading :all-posts] true)}))
(re-frame/reg-event-db
:get-all-posts-success
(fn-traced [db [_ result]]
(-> db
(assoc-in [:loading :all-posts] false)
(assoc :all-posts-api-response result))))
Kehittäminen
Re-frame-appin kehittäminen on siitä kivutonta, että se tarjoaa figwheelin kautta toimivan välittömästi selaimessa muutosten seurauksena päivittyvän näkymän. Lisäksi app-db:n tilaa, eventtien laukeamista ja muuta sovelluksessa tapahtuvaa toimintaa voi helposti käyttää re-framen mukana tulevan kehitystyökalun, re-frame-10x:n avulla:

Domainmumblaus
Tämä on varmaan kaikille about itsestäänselvää, mutta halusin tehdä seuraavaa
- Domain ( ansku.fail) Route 53:ssa
- Softa pyörii palvelimella muualla (ei siis EC2, vaan Ubuntupurkki eri palveluntarjoajalla)
- Ubuntupurkissa luotu sertit Let’s encrypt/certbotilla.
- Saapuminen sivuille CloudFrontin kautta
CloudFrontin distributionia ei voi suoraan forwata kivasti palvelimelle osoitteeseen 95.xx.yy.zz. Eli jos mulla olisi kamat S3-purkissa, no probs, mutta kun pyörittelen apachea omalla palvelimella, niin ei onnistu. Joten, wattodo? Noh, sehän onnistuu niin, että
- Luodaan subdomain hey.ansku.fail
- Luodaan sertit sekä ansku.fail, että hey.ansku.fail
- Upataan nuo sertit AWS:n ACM:ään
- Luodaan CloudFront distribution, jossa originiksi valitaan subdomain hey.ansku.fail
- Luodaan record Route53:ssa, joka pointtaa subdomainin hey.ansku.fail sinne palvelimelle, missä apache pyörii ja käytetään custom serttejä (eli ne, jotka luotiin tuossa alussa ja upattiin ACM:ään)
- Lopuksi ohjataan tuo ansku.fail tuohon luotuun distributioniin luomalla sille Route53:ssa record, joka osoittaa tuohon distributioniin.
Aijjettä, kuinka helppoa! Tässä vielä hienosti piirretty kuva aiheesta:

Itseasiassa ton CF:n ja hey.ansku.failin nuoli kai pitäis olla eri suuntaan, kun hey.ansku.failhan on sen origin. Oh, well.
Deployhommat
Käytin tähän travisia, koska ajattelin, että on eri helppo. Noh, oli siinä vähän säätöä. Semmonen vinkki, että jos tekeepi ssh:n yli siirtoja, niin se avain travisille kandee mielummin kryptata gpg:llä, eikä openssl:llä. Koska vaikka niissä tutoriaaleissa kaikki menee aina ihanasti, niin jostain syystä travis ei saa sitä avainta ajonaikana hienosti käytttöönsä. En tiedä, mikä siinä mätti, mutta gpg:llä homma toimi sukkana. Eli siis tälleen.
Tässä vielä tuo mun ymli:
dist: xenial
language: clojure
addons: ssh_known_hosts:
- "$PRODUCTION_SERVER"
env:
global:
- secure:
nönnöööö
- secure:
nönnönöööbefore_install:
- echo $passphrase | gpg --passphrase-fd 0 new_rsa.gpg
install: lein clean && lein cljsbuild once min
script: eval "$(ssh-agent -s)" && chmod 600 new_rsa && ssh-add new_rsa
deploy:
skip_cleanup: true
on:
branch: master
provider: script
script: >-
rsync -r --delete-after -e "ssh -o StrictHostKeyChecking=no -o
PubkeyAuthentication=yes -o PasswordAuthentication=no" --quiet
-av resources/public/
$PRODUCTION_SERVER_USER@$PRODUCTION_SERVER:/var/www/html/
verbose: true
Eli siis: väännetään clojurescriptistä staattisen sivun kamat, hötskät ja jäsät kansioon public ja sitten kopsataan ne sinne palvelimelle. Mulla oli tossa kaikenmaailman dockerihommaa, mutta vaikka jollekin nginx-certbotille on kyllä oma imagensakin, niin sitten se, että se sertin luominen menee kivasti, niin pitäisi jotenkin jakaa asioita niiden konttien kesken tai sit pyöritellä se sertti samassa kontissa taijjotain. Niin nyt siellä vaan pyörii ihan vanhanaikaisesti apache. Kyllä. Html-kansiossa apache vaan kivasti.
Summa summarum
Näin retrospektiivissä oli ehkä vähän tyhmää lähteä askartelemaan kotisivua itse syistä, että a) en ole mikään innokas css-viilaaja ja vaikkapa wp:ssä olisi saanut parilla kympillä paljon kauniimman teeman ja b) näillä näkymin ei ole tarvetta päästää muita sisällöntuottajia julkaisemaan tälle sivulle. Toisaalta opin re-framesta ja tuntuu, että ehkä vihdoin vähänniinkun tajusin pointsukan noissa eventeissä, subscriptioneissa jne. Ehkä.
Semmosta. Kiva, kun luit, seuraavat postaukset esittelevät varmaankin muutamaa hassua twitterbottia. Tsaude!