Another interim commit

Home page and some post stuff written
This commit is contained in:
Daniel J. Summers 2016-07-08 23:18:44 -05:00
parent b86ba7b6f6
commit 3656bb384c
21 changed files with 481 additions and 43 deletions

View File

@ -1,9 +1,47 @@
namespace myWebLog.Data
open RethinkDb.Driver
open RethinkDb.Driver.Net
open Newtonsoft.Json
/// Data configuration
type DataConfig = {
/// The hostname for the RethinkDB server
hostname : string
/// The port for the RethinkDB server
port : int
/// The authorization key to use when connecting to the server
authKey : string
/// How long an attempt to connect to the server should wait before giving up
timeout : int
/// The name of the default database to use on the connection
database : string
/// A connection to the RethinkDB server using the configuration in this object
conn : IConnection
}
with
/// Create a data configuration from JSON
static member fromJson json =
let mutable cfg = JsonConvert.DeserializeObject<DataConfig> json
cfg <- match cfg.hostname with
| null -> { cfg with hostname = RethinkDBConstants.DefaultHostname }
| _ -> cfg
cfg <- match cfg.port with
| 0 -> { cfg with port = RethinkDBConstants.DefaultPort }
| _ -> cfg
cfg <- match cfg.authKey with
| null -> { cfg with authKey = RethinkDBConstants.DefaultAuthkey }
| _ -> cfg
cfg <- match cfg.timeout with
| 0 -> { cfg with timeout = RethinkDBConstants.DefaultTimeout }
| _ -> cfg
cfg <- match cfg.database with
| null -> { cfg with database = RethinkDBConstants.DefaultDbName }
| _ -> cfg
{ cfg with conn = RethinkDB.R.Connection()
.Hostname(cfg.hostname)
.Port(cfg.port)
.AuthKey(cfg.authKey)
.Db(cfg.database)
.Timeout(cfg.timeout)
.Connect() }

14
src/myWebLog.Data/Page.fs Normal file
View File

@ -0,0 +1,14 @@
module myWebLog.Data.Page
open myWebLog.Entities
open Rethink
/// Get a page by its Id
let tryFindPage conn webLogId pageId : Page option =
match table Table.Page
|> get pageId
|> filter (fun p -> upcast p.["webLogId"].Eq(webLogId))
|> runAtomAsync<Page> conn
|> box with
| null -> None
| page -> Some <| unbox page

49
src/myWebLog.Data/Post.fs Normal file
View File

@ -0,0 +1,49 @@
module myWebLog.Data.Post
open myWebLog.Entities
open Rethink
open RethinkDb.Driver
let private r = RethinkDB.R
/// Get a page of published posts
let findPageOfPublishedPosts conn webLogId pageNbr nbrPerPage =
table Table.Post
|> getAll [| webLogId, PostStatus.Published |]
|> optArg "index" "webLogAndStatus"
|> orderBy (fun p -> upcast r.Desc(p.["publishedOn"]))
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|> runCursorAsync<Post> conn
|> Seq.toList
/// Try to get the next newest post from the given post
let tryFindNewerPost conn post =
table Table.Post
|> getAll [| post.webLogId, PostStatus.Published |]
|> optArg "index" "webLogAndStatus"
|> filter (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn))
|> orderBy (fun p -> upcast p.["publishedOn"])
|> limit 1
|> runCursorAsync<Post> conn
|> Seq.tryHead
/// Try to get the next oldest post from the given post
let tryFindOlderPost conn post =
table Table.Post
|> getAll [| post.webLogId, PostStatus.Published |]
|> optArg "index" "webLogAndStatus"
|> filter (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn))
|> orderBy (fun p -> upcast r.Desc(p.["publishedOn"]))
|> limit 1
|> runCursorAsync<Post> conn
|> Seq.tryHead
/// Get a page of all posts in all statuses
let findPageOfAllPosts conn webLogId pageNbr nbrPerPage =
table Table.Post
|> getAll [| webLogId |]
|> optArg "index" "webLogId"
|> orderBy (fun p -> upcast r.Desc(r.Branch(p.["publishedOn"].Eq(int64 0), p.["lastUpdatedOn"], p.["publishedOn"])))
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|> runCursorAsync<Post> conn
|> Seq.toList

View File

@ -7,17 +7,20 @@ let private r = RethinkDb.Driver.RethinkDB.R
let private await task = task |> Async.AwaitTask |> Async.RunSynchronously
let delete (expr : ReqlExpr) = expr.Delete ()
let filter (expr : ReqlExpr -> ReqlExpr) (table : ReqlExpr) = table.Filter expr
let get (expr : obj) (table : Table) = table.Get expr
let getAll (exprs : obj[]) (table : Table) = table.GetAll exprs
let insert (expr : obj) (table : Table) = table.Insert expr
let limit (expr : obj) (table : ReqlExpr) = table.Limit expr
let optArg key (value : obj) (expr : GetAll) = expr.OptArg (key, value)
let orderBy (exprA : obj) (expr : ReqlExpr) = expr.OrderBy exprA
let orderBy (exprA : ReqlExpr -> ReqlExpr) (expr : ReqlExpr) = expr.OrderBy exprA
let replace (exprA : obj) (expr : ReqlExpr) = expr.Replace exprA
let runAtomAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<'T> conn |> await
let runCursorAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunCursorAsync<'T> conn |> await
let runListAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<System.Collections.Generic.List<'T>> conn
|> await
let runResultAsync (conn : IConnection) (ast : ReqlAst) = ast.RunResultAsync conn |> await
let slice (exprA : obj) (exprB : obj) (ast : ReqlExpr) = ast.Slice (exprA, exprB)
let table (expr : obj) = r.Table expr
let update (exprA : obj) (expr : ReqlExpr) = expr.Update exprA
let without (exprs : obj[]) (expr : ReqlExpr) = expr.Without exprs

View File

@ -3,18 +3,19 @@
open myWebLog.Entities
open Rethink
open RethinkDb.Driver
open RethinkDb.Driver.Net
let private r = RethinkDB.R
type PageList = { pageList : Ast.CoerceTo }
/// Detemine the web log by the URL base
let tryFindWebLogByUrlBase (cfg : DataConfig) (urlBase : string) =
let tryFindWebLogByUrlBase (conn : IConnection) (urlBase : string) =
r.Table(Table.WebLog).GetAll([| urlBase |]).OptArg("index", "urlBase")
.Merge(fun webLog -> { pageList = r.Table(Table.Page)
.GetAll([| webLog.["id"], true |]).OptArg("index", "pageList")
.OrderBy("title")
.Pluck([| "title", "permalink" |])
.CoerceTo("array") })
|> runCursorAsync<WebLog> cfg.conn
|> runCursorAsync<WebLog> conn
|> Seq.tryHead

View File

@ -56,6 +56,8 @@
<Compile Include="DataConfig.fs" />
<Compile Include="Rethink.fs" />
<Compile Include="SetUp.fs" />
<Compile Include="Page.fs" />
<Compile Include="Post.fs" />
<Compile Include="WebLog.fs" />
<Content Include="packages.config" />
</ItemGroup>

View File

@ -11,37 +11,32 @@ open Nancy.Bootstrapper
open Nancy.Cryptography
open Nancy.Owin
open Nancy.Security
open Nancy.Session
open Nancy.Session.Persistable
open Nancy.Session.RethinkDb
open Nancy.TinyIoc
open Nancy.ViewEngines.SuperSimpleViewEngine
open RethinkDb.Driver
open RethinkDb.Driver.Net
open Suave
open Suave.Owin
open System
open System.Text.RegularExpressions
/// Set up a database connection
let cfg =
{ database = "myWebLog"
conn = RethinkDB.R.Connection()
.Hostname(RethinkDBConstants.DefaultHostname)
.Port(RethinkDBConstants.DefaultPort)
.AuthKey(RethinkDBConstants.DefaultAuthkey)
.Db("myWebLog")
.Timeout(RethinkDBConstants.DefaultTimeout)
.Connect() }
let cfg = try DataConfig.fromJson (System.IO.File.ReadAllText "data-config.json")
with ex -> ApplicationException("Could not convert data-config.json to RethinkDB connection", ex)
|> raise
do
startUpCheck cfg
/// Support RESX lookup via the @Translate SSVE alias
type TranslateTokenViewEngineMatcher() =
static let regex = Regex("@Translate\.(?<TranslationKey>[a-zA-Z0-9-_]+);?", RegexOptions.Compiled)
interface ISuperSimpleViewEngineMatcher with
member this.Invoke (content, model, host) =
regex.Replace(content, fun m -> let key = m.Groups.["TranslationKey"].Value
match Resources.ResourceManager.GetString key with
match myWebLog.Resources.ResourceManager.GetString key with
| null -> key
| xlat -> xlat)
@ -51,8 +46,8 @@ type MyWebLogUser(name, claims) =
interface IUserIdentity with
member this.UserName with get() = name
member this.Claims with get() = claims
member this.UserName with get() = (this :> IUserIdentity).UserName
member this.Claims with get() = (this :> IUserIdentity).Claims
(*member this.UserName with get() = (this :> IUserIdentity).UserName
member this.Claims with get() = (this :> IUserIdentity).Claims -- do we need these? *)
type MyWebLogUserMapper(container : TinyIoCContainer) =
@ -63,25 +58,30 @@ type MyWebLogUserMapper(container : TinyIoCContainer) =
| _ -> null
/// Set up the RethinkDB connection instance to be used by the IoC container
type ApplicationBootstrapper() =
/// Set up the application environment
type MyWebLogBootstrapper() =
inherit DefaultNancyBootstrapper()
override this.ConfigureRequestContainer (container, context) =
base.ConfigureRequestContainer (container, context)
/// User mapper for forms authentication
container.Register<IUserMapper, MyWebLogUserMapper>()
|> ignore
override this.ApplicationStartup (container, pipelines) =
base.ApplicationStartup (container, pipelines)
// Data configuration
// Data configuration (both config and the connection; Nancy modules just need the connection)
container.Register<DataConfig>(cfg)
|> ignore
container.Register<IConnection>(cfg.conn)
|> ignore
// I18N in SSVE
container.Register<seq<ISuperSimpleViewEngineMatcher>>(fun _ _ ->
Seq.singleton (TranslateTokenViewEngineMatcher() :> ISuperSimpleViewEngineMatcher))
|> ignore
// Forms authentication configuration
let salt = (System.Text.ASCIIEncoding()).GetBytes "NoneOfYourBeesWax"
let auth =
let auth =
FormsAuthenticationConfiguration(
CryptographyConfiguration = CryptographyConfiguration
(RijndaelEncryptionProvider(PassphraseKeyGenerator("Secrets", salt)),
@ -99,7 +99,7 @@ type ApplicationBootstrapper() =
let version =
let v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version
let v = Reflection.Assembly.GetExecutingAssembly().GetName().Version
match v.Build with
| 0 -> match v.Minor with
| 0 -> string v.Major
@ -112,13 +112,13 @@ type RequestEnvironment() =
interface IRequestStartup with
member this.Initialize (pipelines, context) =
pipelines.BeforeRequest.AddItemToStartOfPipeline
(fun ctx -> ctx.Items.["requestStart"] <- System.DateTime.Now.Ticks
match tryFindWebLogByUrlBase cfg ctx.Request.Url.HostName with
| Some webLog -> ctx.Items.["webLog"] <- webLog
| None -> System.ApplicationException
(fun ctx -> ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
match tryFindWebLogByUrlBase cfg.conn ctx.Request.Url.HostName with
| Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
| None -> ApplicationException
(sprintf "%s is not properly configured for myWebLog" ctx.Request.Url.HostName)
|> raise
ctx.Items.["version"] <- version
ctx.Items.[Keys.Version] <- version
null)

View File

@ -1,3 +1,11 @@
module myWebLog.Keys
let Messages = "messages"
let RequestStart = "request-start"
let User = "user"
let Version = "version"
let WebLog = "web-log"

View File

@ -0,0 +1,24 @@
namespace myWebLog
open myWebLog.Entities
open Nancy
open Nancy.Security
/// Parent class for all myWebLog Nancy modules
[<AbstractClass>]
type MyWebLogModule() =
inherit NancyModule()
/// Strongly-typed access to the web log for the current request
member this.WebLog = this.Context.Items.[Keys.WebLog] :?> WebLog
/// Display a view using the theme specified for the web log
member this.ThemedRender view model = this.View.[(sprintf "%s/%s" this.WebLog.themePath view), model]
/// Return a 404
member this.NotFound () = this.Negotiate.WithStatusCode 404
/// Require a specific level of access for the current web log
member this.RequiresAccessLevel level =
this.RequiresAuthentication()
this.RequiresClaims [| sprintf "%s|%s" this.WebLog.id level |]

View File

@ -0,0 +1,67 @@
namespace myWebLog
open myWebLog.Data.Page
open myWebLog.Data.Post
open Nancy
open Nancy.Authentication.Forms
open Nancy.Security
open RethinkDb.Driver.Net
open myWebLog.Entities
type PostModule(conn : IConnection) as this =
inherit MyWebLogModule()
do
this.Get.["/"] <- fun _ -> upcast this.HomePage ()
this.Get.["/posts/page/{page:int}"] <- fun parms -> upcast this.GetPageOfPublishedPosts (downcast parms)
this.Get.["/posts/list"] <- fun _ -> upcast this.PostList 1
this.Get.["/posts/list/page/{page:int"] <- fun parms -> upcast this.PostList
((parms :?> DynamicDictionary).["page"] :?> int)
// ---- Display posts to users ----
/// Display a page of published posts
member private this.DisplayPageOfPublishedPosts pageNbr =
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10
model.hasNewer <- match List.isEmpty model.posts with
| true -> false
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts)
model.hasOlder <- match List.isEmpty model.posts with
| true -> false
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts)
model.urlPrefix <- "/posts"
model.pageTitle <- match pageNbr with
| 1 -> ""
| _ -> sprintf "Page #%i" pageNbr
this.ThemedRender "posts" model
/// Display either the newest posts or the configured home page
member this.HomePage () =
match this.WebLog.defaultPage with
| "posts" -> this.DisplayPageOfPublishedPosts 1
| page -> match tryFindPage conn this.WebLog.id page with
| Some page -> let model = PageModel(this.Context, this.WebLog, page)
model.pageTitle <- page.title
this.ThemedRender "page" model
| None -> this.Negotiate.WithStatusCode 404
/// Get a page of public posts (other than the first one if the home page is a page of posts)
member this.GetPageOfPublishedPosts (parameters : DynamicDictionary) =
this.DisplayPageOfPublishedPosts (parameters.["page"] :?> int)
// ---- Administer posts ----
/// Display a page of posts in the admin area
member this.PostList pageNbr =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25
model.hasNewer <- pageNbr > 1
model.hasOlder <- 25 > List.length model.posts
model.urlPrefix <- "/post/list"
model.pageTitle <- "Posts"
this.View.["admin/post/list", model]

View File

@ -0,0 +1,95 @@
namespace myWebLog
open myWebLog.Entities
open Nancy
open Nancy.Session.Persistable
/// Levels for a user message
module Level =
/// An informational message
let Info = "Info"
/// A message regarding a non-fatal but non-optimal condition
let Warning = "WARNING"
/// A message regarding a failure of the expected result
let Error = "ERROR"
/// A message for the user
type UserMessage = {
/// The level of the message (use Level module constants)
level : string
/// The text of the message
message : string
/// Further details regarding the message
details : string option
}
with
/// An empty message
static member empty =
{ level = Level.Info
message = ""
details = None }
/// Parent view model for all myWebLog views
type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
/// Get the messages from the session
let getMessages () =
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, List.empty)
match List.length msg with
| 0 -> ()
| _ -> ctx.Request.Session.Delete Keys.Messages
msg
/// The web log for this request
member this.webLog = webLog
/// User messages
member val messages = getMessages () with get, set
/// The currently logged in user
member this.user = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.empty)
/// The title of the page
member val pageTitle = "" with get, set
/// The request start time
member this.requestStart = ctx.Items.[Keys.RequestStart] :?> int64
/// Is a user authenticated for this request?
member this.isAuthenticated = "" <> this.user.id
/// Add a message to the output
member this.addMessage message = this.messages <- message :: this.messages
/// Model for all page-of-posts pages
type PostsModel(ctx, webLog) =
inherit MyWebLogModel(ctx, webLog)
/// The posts to display
member val posts = List.empty<Post> with get, set
/// The page number of the post list
member val pageNbr = 0 with get, set
/// Whether there is a newer page of posts for the list
member val hasNewer = false with get, set
/// Whether there is an older page of posts for the list
member val hasOlder = true with get, set
/// The prefix for the next/prior links
member val urlPrefix = "" with get, set
/// The link for the next newer page of posts
member this.newerLink =
match this.urlPrefix = "/posts" && this.pageNbr = 2 && this.webLog.defaultPage = "posts" with
| true -> "/"
| _ -> sprintf "%s/page/%i" this.urlPrefix (this.pageNbr - 1)
/// The link for the prior (older) page of posts
member this.olderLink = sprintf "%s/page/%i" this.urlPrefix (this.pageNbr + 1)
/// Model for page display
type PageModel(ctx, webLog, page) =
inherit MyWebLogModel(ctx, webLog)
/// The page to be displayed
member this.page : Page = page

View File

@ -52,6 +52,9 @@
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Keys.fs" />
<Compile Include="ViewModels.fs" />
<Compile Include="MyWebLogModule.fs" />
<Compile Include="PostModule.fs" />
<Compile Include="App.fs" />
<Content Include="packages.config" />
</ItemGroup>

View File

@ -2,7 +2,7 @@
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("myWebLog")]
[assembly: AssemblyDescription("A lightweight blogging platform built on Nancy and RethinkDB")]
[assembly: AssemblyDescription("A lightweight blogging platform built on Suave, Nancy, and RethinkDB")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("myWebLog")]

View File

@ -0,0 +1,5 @@
footer {
background-color: #808080;
border-top: solid 1px black;
color: white;
}

View File

@ -0,0 +1,3 @@
{
"database": "myWebLog"
}

View File

@ -48,6 +48,7 @@
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="data-config.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\myWebLog.Data\myWebLog.Data.fsproj">
@ -65,9 +66,14 @@
</ItemGroup>
<ItemGroup />
<ItemGroup>
<Content Include="content\styles\admin.css" />
<Content Include="views\admin\admin-layout.html" />
<Content Include="views\admin\post\list.html" />
<Content Include="views\default\index-content.html" />
<Content Include="views\default\index.html" />
<Content Include="views\default\layout.html" />
<Content Include="views\default\page-content.html" />
<Content Include="views\default\page.html" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@Model.pageTitle | @Translate.Admin | @Model.webLog.name</title>
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.4/cosmo/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
<link rel="stylesheet" type="text/css" href="/content/styles/admin.css" />
</head>
<body>
<header>
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">@Model.webLog.name</a>
</div>
<div class="navbar-left">
<p class="navbar-text">@Model.pageTitle</p>
</div>
<ul class="nav navbar-nav navbar-right">
@If.isAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li>
@EndIf
@IfNot.isAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li>
@EndIf
</ul>
</div>
</nav>
</header>
<div class="container">
<!-- Partial['admin/messages', Model] // TODO -->
@Section['Content'];
</div>
<footer>
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-right">@Model.generator</div>
</div>
</div>
</footer>
<script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script type="text/javascript" src="//tinymce.cachefly.net/4.1/tinymce.min.js"></script>
@Section['Scripts'];
</body>
</html>

View File

@ -0,0 +1,60 @@
@Master['admin/admin-layout']
@Section['Content']
<div class="row">
<p>
<a class="btn btn-primary" href="/post/new/edit">
<i class="fa fa-plus"></i> | &nbsp; @Translate.AddNew
</a>
</p>
</div>
<div class="row">
<table class="table table-hover">
<tr>
<th>@Translate.Date</th>
<th>@Translate.Title</th>
<th>@Translate.Status</th>
<th>@Translate.Tags</th>
</tr>
@Each.posts
<!-- - var theDate = moment(post.publishedDate ? post.publishedDate : post.lastUpdatedDate)
- var tags = (post.tag || []).sort() -->
<tr>
<td>
<!-- =theDate.format('MMM D, YYYY')
br
#{__("at")} #{theDate.format('h:mma')} -->
// TODO format date
</td>
<td>
@Current.title<br />
<a href="/@Current.permalink">@Translate.View</a> | &nbsp;
<a href="/post/@Current.id/edit">@Translate.Edit</a> | &nbsp;
<a href="/post/@Current.id/delete">@Translate.Delete</a>
</td>
<td>@Current.status</td>
<td>
<!-- if 5 > tags.length
=tags.join(' | ')
else
=tags.slice(0, 3).join(' | ') + ' | '
span(title=tags.slice(3).join(' | '))= __("and %d more...", tags.slice(3).length) -->
// TODO fix tags
</td>
</tr>
@EndEach
</table>
</div>
<div class="row">
<div class="col-xs-3 col-xs-offset-2">
@If.hasNewer
<p><a class="btn btn-default" href="@Model.newerLink">&#xab; &nbsp;@Translate.NewerPosts</a></p>
@EndIf
</div>
<div class="col-xs-3 col-xs-offset-1 text-right">
@If.hasOlder
<p><a class="btn btn-default" href="@Model.olderLink">@Translate.OlderPosts&nbsp; &#xbb;</a></p>
@EndIf
</div>
</div>
@EndSection

View File

@ -23,16 +23,17 @@
@EndEach
<div class="row">
<div class="col-xs-3 col-xs-offset-3">
// TODO: stopped here
if hasNext
- var nextLink = (2 < page) ? '/page/' + (page - 1) : '/'
if '/posts' == extraUrl && 2 == page
//- Leave the "next" link at '/'
else
- nextLink = extraUrl + nextLink
p: a.btn.btn-primary(href="#{nextLink}")= __("Newer Posts")
</div>
.col-xs-3.text-right
if hasPrior
p: a.btn.btn-primary(href=extraUrl + '/page/' + (page + 1))= __("Older Posts")
@If.hasNewer
<p>
<a class="btn btn-primary" href="@Model.newerLink">@Translate.NewerPosts</a>
</p>
@EndIf
</div>
<div class="col-xs-3 text-right">
@If.hasOlder
<p>
<a class="btn btn-primary" href="@Model.olderLink">@Translate.OlderPosts</a>
</p>
@EndIf
</div>
</div>

View File

@ -0,0 +1,4 @@
<article>
<h1>@Model.page.title</h1>
@Model.page.text
</article>

View File

@ -0,0 +1,5 @@
@Master['default/layout']
@Section['Content']
@Partial['default/page-content', Model]
@EndSection