-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathMagic-of-Saturn-controllers.html
501 lines (341 loc) · 26.9 KB
/
Magic-of-Saturn-controllers.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Magic of Saturn controllers</title>
<meta name="description" content="Magic of Saturn controllers">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/favicon/favicon.ico">
<link rel="apple-touch-icon-precomposed" href="/favicon/lattice-optic-illusion-152-204321.png" >
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-TileImage" content="/favicon/lattice-optic-illusion-144-204321.png">
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Lato:400,400italic,700,700italic" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/styles/default.min.css">
<link rel="stylesheet" href="/css/main.css" />
<link rel="stylesheet" href="/css/font-awesome.min.css" />
<link rel="canonical" href="http://localhost:4000/Magic-of-Saturn-controllers">
<link rel="alternate" type="application/rss+xml" title="F# Reflections Feed" href="http://localhost:4000/feed.xml">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
</head>
<body>
<nav id="top" class="site-navigation">
<div class="inner">
<button id="menu-toggle" aria-expanded="false">Menu</button>
<div class="nav-menu">
<ul class="menu">
<li class="">
<a class="" href="/">Home</a>
</li>
<li class="">
<a class="" href="/about">About</a>
</li>
<li class="">
<a class="" href="/tags/">Archives</a>
</li>
<li class="">
<a class="" href="/2016">2016</a>
</li>
</ul>
</div><!-- .nav-menu -->
<div class="subscribe-button">
<a class="fa-feed square fill-horizontal" href="/feed.xml"><span class="screen-reader-text">RSS</span></a>
</div><!-- .social-links -->
</div><!-- .inner -->
</nav><!-- .site-navigation -->
<header class="site-header">
<div class="inner">
<h1 class="site-title"><a class="logo-text" href="/">F# Reflections</a></h1>
<p class="site-description">Thoughts, stories and ideas.</p>
</div><!-- .inner -->
</header>
<div class="site-content">
<div class="inner">
<main class="site-main">
<article class="post">
<header class="entry-header">
<div class="entry-header-wrap">
<div class="post-thumbnail">
<img src="/images/posts/landscape_10.jpg" alt="Magic of Saturn controllers">
</div><!-- .post-thumbnail -->
<h1 class="entry-title">Magic of Saturn controllers</h1>
</div><!-- .entry-header-wrap -->
<div class="entry-meta">
by <span class="post-author">Krzysztof Cieslak</span> on <time class="published" datetime="2018-07-20">July 20, 2018</time>
</div><!-- .entry-meta -->
</header><!-- .entry-header -->
<div class="entry-content">
<h1>Introduction</h1>
<p><a href="https://saturnframework.org">Saturn</a> is new F# web framework that implements well know design pattern - MVC - in more functional way. Despite Saturn being fairly young project it’s getting more and more popular among F# community and industrial users. One of the main Saturn’s goals is to create high level abstractions that will enable developers to focus on writing domain, business code instead of focusing on creating correct routing for your application or setting right response headers. One of such abstractions, that I want to talk about today, is <code>controller</code>.</p>
<blockquote>
<p>This article was created for Saturn version 0.7.x</p>
</blockquote>
<!--more-->
<h1>Basic usage</h1>
<p><code>controller</code> is simple computation expression (CE) that enables you to easily implement application endpoint following REST-ish conventions. It can be used for implementing endpoints that renders views if you’re building application using server side rendering, or just return serialized data if you’re building API or your application is using client side rendering. Just like all other CEs used in Saturn, <code>controller</code> provides set of custom operations that you can use. And, what’s important, all operations in <code>controller</code> CE are optional, which means you can easily choose which subset of functionality you need. Example, basic implementation looks like this:</p>
<pre><code>let resource = controller {
index indexAction
show showAction
add addAction
edit editAction
create createAction
update updateAction
patch patchAction
delete deleteAction
deleteAll deleteAllAction
}
</code></pre>
<p>Let’s now go one by one, and describe each operation:</p>
<ul>
<li><code>index</code> - mapped into <code>GET</code> request at <code>/</code> endpoint. Usually used to render a view displaying list of items, or return whole list of items.</li>
<li><code>show</code> - mapped into <code>GET</code> request at <code>/:id</code> endpoint. Usually used to render a view displaying details of particular item, or return single item with given id.</li>
<li><code>add</code> - mapped into <code>GET</code> request at <code>/add</code> endpoint. Used to render a form for adding new item. Usually not used in API controllers.</li>
<li><code>edit</code> - mapped into <code>GET</code> request at <code>/:id/edit</code>. Used to render a form for editing existing item. Usually not used in API controllers.</li>
<li><code>create</code> - mapped into <code>POST</code> request at <code>/</code> endpoint. Used to create and save new item.</li>
<li><code>update</code> - mapped into <code>POST</code> and <code>PUT</code> requests at <code>/:id</code> endpoint. Used to update existing item. Usually replaces original item (keeping id), and requires are fields to be filled in the incoming item.</li>
<li><code>patch</code> - mapped into <code>PATCH</code> request at <code>/:id</code> endpoint. Used to update existing item. Usually only changes some fields of original item, request body contains only changed fields or JSON Patch object.</li>
<li><code>delete</code> - mapped into <code>DELETE</code> request at <code>/:id</code> endpoint. Used to delete or deactivate existing item.</li>
<li><code>deleteAll</code> - mapped into <code>DELETE</code> request at <code>/</code> endpoint. Used to delete or deactivate all items.</li>
</ul>
<blockquote>
<p>Please remember that Saturn is not enforcing behaviour or inputs of actions any way, so above descriptions are suggestions and best practices, not something that’s encoded in framework. The only thing that controller provides is set in stone routing structure.</p>
</blockquote>
<h1>Actions implementation</h1>
<p>All actions <code>indexAction</code>, <code>showAction</code> … are simple F# functions. All of them as first parameter accepts <code>HttpContext</code> object - it’s an ASP.NET class that contains all information about incoming request, response, server, environment and other data that was injected into it by framework. Actions that are using ID of the item, such as <code>showAction</code> or <code>editAction</code> are functions that get <code>id</code> as a second parameter. The <code>id</code> may be generic but we currently supports limited set of the possible types to which we can decode ID from URL.</p>
<p>Supported types:</p>
<ul>
<li><code>string</code></li>
<li><code>char</code></li>
<li><code>int</code></li>
<li><code>int64</code></li>
<li><code>float</code></li>
<li><code>bool</code></li>
<li><code>System.Guid</code></li>
</ul>
<blockquote>
<p>In case you’d need some custom ID type, I’d recommend using <code>string</code> and deserializing it manually. Another important limitation of current controllers is fact that all actions needs to use same ID type in one controller instance. Again, if you’d need different ID types - use <code>string</code> and deserialize it manually.</p>
</blockquote>
<p>Example action implementation may look like this:</p>
<pre><code>let myIndex (ctx: HttpContext) = Controller.text ctx "Hello world"
let myShow (ctx: HttpContext) (id: string) =
id
|> sprintf "Hello world, %s"
|> Controller.text ctx
let myController = controller {
index myIndex
show myShow
}
</code></pre>
<h1>Action output type</h1>
<p>If you’ll hover over <code>myIndex</code> or <code>myShow</code> you’ll notice that return type of those functions is <code>Task<HttpContext option></code>. First thing - all actions in Saturn’s controllers are asynchronous by design, and they are using standard .Net Task to model it. However, they’re generic over what type is actually returned by the task. If you return <code>HttpContext option</code> you’re following standard path of integration with Giraffe (web library on top of which Saturn is built). This gives you not only full control over what’s going on and how your response is modified, but also provides ability to integrate with existing Giraffe ecosystem. Additionally Saturn itself provides rich set of helpers that return <code>Task<HttpContext option></code> in <code>Controller</code> module (example of this is <code>Controller.text</code> function used in example, that sets content of the response to given string, and also sets appropriate response header).</p>
<p>But returning <code>Task<HttpContext option></code> is not only option. You can also return <code>Task<'a></code> (where <code>'a</code> is any type) and Saturn will perform automatic output content negotiation. In such case Saturn will check output type of your action, check what’s the client preference based on the <code>Accept</code> header (if <code>Accept</code> header is not present, <code>Content-Type</code> header will be used instead) and decide what’s the best way to handle response object:</p>
<ul>
<li>If you return <code>string</code> Saturn will return string with <code>text/plain</code> or <code>text/html</code><code>Content-Type</code> depending on <code>Accept</code> header</li>
<li>If you return <code>GiraffeViewEngine.XmlNode</code> (Giraffe’s view object) and client accepts <code>text/html</code> responses Saturn will render the view and return to client</li>
<li>If you return any other type it will be deserialized to JSON (with <code>application/json</code> <code>Content-Type</code>) unless client doesn’t accept JSON response - in such case XML will be tried.</li>
</ul>
<p>Same output content negotiation algorithm is provided by <code>Controller.response</code> helper.</p>
<p>Example action implementation using output content negotiation:</p>
<pre><code>let myIndex (ctx: HttpContext) =
task { return "Hello world" }
let myShow (ctx: HttpContext) (id: string) =
task {
return sprintf "Hello world, %s" id
}
let myAdd (ctx: HttpContext) =
task { return DateTime.Now }
let myController = controller {
index myIndex
show myShow
add myAdd
}
</code></pre>
<blockquote>
</blockquote>
<h1>Versioning</h1>
<p>Versioning of the endpoints is one of the most important cross cutting concernes in web applications… and most of the web frameworks don’t provide any built-in ways to handle it easily. Saturn provides opinionated way to easily version your controllers. Saturn is using <a href="https://www.troyhunt.com/your-api-versioning-is-wrong-which-is/">1 of 3 wrong ways</a> - custom header <code>x-controller-version</code> to decide which version of the control should be called. Of course, if you don’t like this strategy, Saturn makes it easy to fallback to bit lower level of abstraction, so you can create differently wrong versioning strategy.</p>
<p>The implementation of versioning in your controllers is trivial - it’s just adding one additional operation to your controllers - <code>version</code>. Here’s an example:</p>
<pre><code>
let myController = controller {
index myIndex
show myShow
add myAdd
}
let myControllerV1 = controller {
version "1"
index myIndex
show myShow
add myAdd
}
let appRouter = router {
forward "/endpoint" myControllerV1
forward "/endpoint" myController
}
</code></pre>
<blockquote>
<p>Since controller without version is not performing any checks it’s important to plug controllers in correct order in your router - the controller without any version should go lowest.</p>
</blockquote>
<h1>Plugs</h1>
<p>Another important feature of any web framework is ability of (declaratively) plugging some additional actions/modifications for particular actions in controllers. For example, in ASP .NET MVC this is done with attributes and enables features like authorization and authentication for particular actions in controller (and many, many more other features and cross cutting concerns). Saturn provides flexible mechanism to provide such functionalities using controller plugs, using one simple CE operation - <code>plug</code> - that accepts list of the actions to which it should apply and the plug function. Plug implementation is any <code>HttpHandler</code> which means that it integrates well if existing ecosystem and helpers, and plug implementation is decoupled from the controller itself, which means you can easily create plugs for cross cutting concerns such as logging or authorization and reuse them across many controllers in your application.</p>
<p>Example implementation:</p>
<pre><code>
let myControllerV1 = controller {
plug [All] (setHttpHeader "user-controller-all" "123")
plug [Index; Show] (setHttpHeader "user-controller-some" "456")
plug (except Index) (setHttpHeader "user-controller-except" "789")
index myIndex
show myShow
add myAdd
}
</code></pre>
<h1>Subcontrollers</h1>
<p>Last important feature of the <code>controller</code> is ability to embed controllers. This, again, is fairly opinionated feature that follows REST-ish conventions. Subcontroller should be used in case when one particular item (represented in controller by <code>/:id</code>) has some child items - for example <code>blog</code> item contains list of <code>post</code> items. Or <code>post</code> item contains list of <code>comment</code> items. Subcontroller is subrouted into <code>/:id/:subcontrollerPath</code> route of original controller (so for example <code>/:id/:subcontrollerPath/:id2</code> shows the particular comment, or <code>/:id/:subcontrollerPath/add</code> will show form for adding new child item to the parent item with given ID). Adding subcontroller to your controller is done by using yet another custom operation in CE - <code>subController</code> that takes path of the subcontroller and child controller as inputs (passing ID to this subcontroller).</p>
<p>Example implementation:</p>
<pre><code>let commentController userId = controller {
index (fun ctx -> (sprintf "Comment Index handler for user %i" userId ) |> Controller.text ctx)
add (fun ctx -> (sprintf "Comment Add handler for user %i" userId ) |> Controller.text ctx)
show (fun ctx id -> (sprintf "Show comment %s handler for user %i" id userId ) |> Controller.text ctx)
edit (fun ctx id -> (sprintf "Edit comment %s handler for user %i" id userId ) |> Controller.text ctx)
}
let userControllerVersion1 = controller {
subController "/comments" commentController
index (fun ctx -> "Index handler" |> Controller.text ctx)
add (fun ctx -> "Add handler" |> Controller.text ctx)
show (fun ctx id -> (sprintf "Show handler - %i" id) |> Controller.text ctx)
edit (fun ctx id -> (sprintf "Edit handler - %i" id) |> Controller.text ctx)
}
</code></pre>
<blockquote>
<p><code>subController</code> operation actually accepts any <code>HttpHandler</code> not only controllers, which means you anything in there. Also, you can add multiple subcontrollers to one controller which may be useful… for example in combination with controller versioning feature.</p>
</blockquote>
<h1>Summary</h1>
<p>In this post I’ve tried to present all features and power of Saturn’s high level abstraction - <code>controller</code>, and flexible design they allow.</p>
</div><!-- .entry-content -->
<footer class="entry-footer">
<div class="share-post">
<span>Share</span>
<a class="fa-twitter" target="_blank" href="https://twitter.com/intent/tweet?text=Magic%20of%20Saturn%20controllers&url=http://localhost:4000/Magic-of-Saturn-controllers"><span class="screen-reader-text">Twitter</span></a>
<a class="fa-facebook" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u=http://localhost:4000/Magic-of-Saturn-controllers&t=Magic%20of%20Saturn%20controllers"><span class="screen-reader-text">Facebook</span></a>
<a class="fa-google-plus" target="_blank" href="http://plus.google.com/share?url=http://localhost:4000/Magic-of-Saturn-controllers"><span class="screen-reader-text">Google+</span></a>
</div><!-- .share-post -->
<div class="tag-links">
Tags: <a href="/tags/index.html#F%23" title="Pages tagged F#" rel="tag">F#</a><a href="/tags/index.html#Saturn" title="Pages tagged Saturn" rel="tag">Saturn</a><a href="/tags/index.html#Web" title="Pages tagged Web" rel="tag">Web</a>
</div>
</footer><!-- .entry-footer -->
</article><!-- .post -->
<div class="author-box">
<div class="author-info">
<a href="http://kcieslak.io/about" target="_blank"> <div class="author-avatar" style="background-image: url(/images/authorimage.jpg)"><span class="screen-reader-text">Krzysztof Cieslak's Picture</span></div> </a>
<div class="author-details">
<h2 class="author-title">About Krzysztof Cieslak</h2>
<p class="author-bio">Krzysztof is a F# developer and consultant, open source contributor, conference speaker</p>
<span class="author-location"><i class="fa-map-marker" aria-hidden="true"></i> Lodz, Poland</span>
<span class="author-website"><i class="fa-chain" aria-hidden="true"></i> <a href="http://kcieslak.io" target="_blank"> http://kcieslak.io</a></span>
</div><!-- .author-details -->
</div><!-- .author-info -->
</div><!-- .author-box -->
<nav class="post-navigation">
<h2 class="screen-reader-text">Post navigation</h2>
<div class="nav-previous">
<div class="nav-inside">
<a href="/OSS-The-Story" class="nav-thumb" style="background-image: url(/images/posts/landscape_4.jpg)"></a>
<div class="nav-before">Previous</div>
<div class="nav-title"><a href="/OSS-The-Story">OSS and Community. The Story</a></div>
<div class="nav-date">February 23, 2018</div>
</div><!-- .nav-inside -->
</div><!-- .nav-previous -->
<div class="nav-next">
<div class="nav-inside">
<a href="/Introducing-Saturn-on-Functions" class="nav-thumb" style="background-image: url(/images/posts/landscape_3.jpg)"></a>
<div class="nav-before">Next</div>
<div class="nav-title"><a href="/Introducing-Saturn-on-Functions">Introducing Saturn on Functions</a></div>
<div class="nav-date">July 21, 2018</div>
</div><!-- .nav-inside -->
</div><!-- .nav-next -->
</nav><!-- .post-navigation -->
</main><!-- .site-main -->
<aside class="sidebar">
<section class="widget widget-text">
<h2 class="widget-title">About Krzysztof Cieslak</h2>
<p>Krzysztof is a F# developer and consultant, open source contributor, conference speaker</p>
</section><!-- .widget-text -->
<section class="widget widget-text">
<p><a href="http://lambdafactory.io" target="_blank"><img src="/images/banner.png" alt="Optional Banner Ad" /></a></p>
</section><!-- .widget-text-->
<section class="widget widget-recent-posts">
<h2 class="widget-title">Latest Posts</h2>
<ul class="recent-posts">
<li class="recent-item"><a href="/F-Editor-Tooling-Part1">Behind the F# editor tootling. Part 1: introduction to compilers.</a> <span>August 27, 2019</span></li>
<li class="recent-item"><a href="/Ionide-Introducing-Info-Panel">Ionide — Introducing Info Panel</a> <span>May 6, 2019</span></li>
<li class="recent-item"><a href="/Ionide-New-Hope">Ionide — A New Hope</a> <span>March 24, 2019</span></li>
<li class="recent-item"><a href="/PureScript-on-Azure-Functions">PureScript on Azure Functions</a> <span>January 12, 2019</span></li>
<li class="recent-item"><a href="/Future-of-F-Cross-platform-editor-tooling">Future of F# cross-platform editor tooling</a> <span>December 31, 2018</span></li>
</ul><!-- .recent-posts -->
</section><!-- .widget-recent-posts -->
<!--Create a sorted array of tags-->
<section class="widget widget-tags">
<h2 class="widget-title">Tags</h2>
<div class="tagcloud">
<a href="/tags/#Azure">Azure</a>
<a href="/tags/#Azure+Functions">Azure Functions</a>
<a href="/tags/#Community">Community</a>
<a href="/tags/#Compiler">Compiler</a>
<a href="/tags/#F%23">F#</a>
<a href="/tags/#FAKE">FAKE</a>
<a href="/tags/#FSharp.Compiler.Services">FSharp.Compiler.Services</a>
<a href="/tags/#Fable">Fable</a>
<a href="/tags/#JS">JS</a>
<a href="/tags/#MVP">MVP</a>
<a href="/tags/#OAuth">OAuth</a>
<a href="/tags/#Open+Source">Open Source</a>
<a href="/tags/#Paket">Paket</a>
<a href="/tags/#PureScript">PureScript</a>
<a href="/tags/#Saturn">Saturn</a>
<a href="/tags/#Serverless">Serverless</a>
<a href="/tags/#Tooling">Tooling</a>
<a href="/tags/#VS+Code">VS Code</a>
<a href="/tags/#Web">Web</a>
<a href="/tags/#Webpack">Webpack</a>
</div><!-- .tagcloud -->
</section><!-- .widget -->
<section class="widget widget-text">
<h2 class="widget-title">Contact me</h2>
<a href="https://twitter.com/k_cieslak" class="fa-twitter square fill-horizontal"><span class="screen-reader-text">Twitter</span></a>
<a href="https://github.com/Krzysztof-Cieslak" class="fa-github-alt square fill-horizontal"><span class="screen-reader-text">GitHub</span></a>
<a href="https://gitlab.com/Krzysztof-Cieslak" class="fa-gitlab square fill-horizontal"><span class="screen-reader-text">GitHub</span></a>
<a href="mailto:[email protected]" class="fa-envelope square fill-horizontal"><span class="screen-reader-text">E-Mail</span></a>
</section><!-- .widget-text -->
</aside><!-- .sidebar -->
</div><!-- .inner -->
</div><!-- .site-content -->
<footer class="site-footer">
<div class="inner">
<div class="social-links">
<a href="https://twitter.com/k_cieslak" class="fa-twitter square fill-horizontal"><span class="screen-reader-text">Twitter</span></a>
<a href="https://github.com/Krzysztof-Cieslak" class="fa-github-alt square fill-horizontal"><span class="screen-reader-text">GitHub</span></a>
<a href="https://gitlab.com/Krzysztof-Cieslak" class="fa-gitlab square fill-horizontal"><span class="screen-reader-text">GitHub</span></a>
<a href="mailto:[email protected]" class="fa-envelope square fill-horizontal"><span class="screen-reader-text">E-Mail</span></a>
<a class="fa-feed square fill-horizontal" href="/feed.xml"><span class="screen-reader-text">RSS</span></a>
</div><!-- .social-links -->
<div class="site-info">
© <a href="/">Krzysztof Cieslak</a> all rights reserved. <br />Powered by <a target="_blank" href="https://jekyllrb.com/">Jekyll</a>. BlogInn theme by <a target="_blank" href="http://justgoodthemes.com/">JustGoodThemes</a>
<a href="#top" id="top-link" class="fa-chevron-up top-link square fill-horizontal"><span class="screen-reader-text">Back to the top</span></a>
</div><!-- .site-info -->
</div><!-- .inner -->
</footer><!-- .site-footer -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/languages/fsharp.min.js"></script>
<script type="text/javascript" src="/js/plugins.js"></script>
<script type="text/javascript" src="/js/custom.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<!-- Asynchronous Google Analytics snippet -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-74514389-1', 'auto');
ga('require', 'linkid', 'linkid.js');
ga('send', 'pageview');
</script>
</body>
</html>