diff --git a/src/myWebLog.Data/DataConfig.fs b/src/myWebLog.Data/DataConfig.fs index f3d3738..e4709ff 100644 --- a/src/myWebLog.Data/DataConfig.fs +++ b/src/myWebLog.Data/DataConfig.fs @@ -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 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() } diff --git a/src/myWebLog.Data/Page.fs b/src/myWebLog.Data/Page.fs new file mode 100644 index 0000000..d31bf8b --- /dev/null +++ b/src/myWebLog.Data/Page.fs @@ -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 conn + |> box with + | null -> None + | page -> Some <| unbox page \ No newline at end of file diff --git a/src/myWebLog.Data/Post.fs b/src/myWebLog.Data/Post.fs new file mode 100644 index 0000000..c728d59 --- /dev/null +++ b/src/myWebLog.Data/Post.fs @@ -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 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 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 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 conn + |> Seq.toList diff --git a/src/myWebLog.Data/Rethink.fs b/src/myWebLog.Data/Rethink.fs index be50fdf..cdbc414 100644 --- a/src/myWebLog.Data/Rethink.fs +++ b/src/myWebLog.Data/Rethink.fs @@ -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> 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 \ No newline at end of file diff --git a/src/myWebLog.Data/WebLog.fs b/src/myWebLog.Data/WebLog.fs index 83b90b0..a0c967b 100644 --- a/src/myWebLog.Data/WebLog.fs +++ b/src/myWebLog.Data/WebLog.fs @@ -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 cfg.conn + |> runCursorAsync conn |> Seq.tryHead diff --git a/src/myWebLog.Data/myWebLog.Data.fsproj b/src/myWebLog.Data/myWebLog.Data.fsproj index 16ee24f..c82224d 100644 --- a/src/myWebLog.Data/myWebLog.Data.fsproj +++ b/src/myWebLog.Data/myWebLog.Data.fsproj @@ -56,6 +56,8 @@ + + diff --git a/src/myWebLog.Web/App.fs b/src/myWebLog.Web/App.fs index 095027f..3737906 100644 --- a/src/myWebLog.Web/App.fs +++ b/src/myWebLog.Web/App.fs @@ -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\.(?[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() |> 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(cfg) |> ignore + container.Register(cfg.conn) + |> ignore // I18N in SSVE container.Register>(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) diff --git a/src/myWebLog.Web/Keys.fs b/src/myWebLog.Web/Keys.fs index ae2bf3b..282a3b7 100644 --- a/src/myWebLog.Web/Keys.fs +++ b/src/myWebLog.Web/Keys.fs @@ -1,3 +1,11 @@ module myWebLog.Keys -let User = "user" \ No newline at end of file +let Messages = "messages" + +let RequestStart = "request-start" + +let User = "user" + +let Version = "version" + +let WebLog = "web-log" \ No newline at end of file diff --git a/src/myWebLog.Web/MyWebLogModule.fs b/src/myWebLog.Web/MyWebLogModule.fs new file mode 100644 index 0000000..e91da1b --- /dev/null +++ b/src/myWebLog.Web/MyWebLogModule.fs @@ -0,0 +1,24 @@ +namespace myWebLog + +open myWebLog.Entities +open Nancy +open Nancy.Security + +/// Parent class for all myWebLog Nancy modules +[] +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 |] diff --git a/src/myWebLog.Web/PostModule.fs b/src/myWebLog.Web/PostModule.fs new file mode 100644 index 0000000..4e498ed --- /dev/null +++ b/src/myWebLog.Web/PostModule.fs @@ -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] diff --git a/src/myWebLog.Web/ViewModels.fs b/src/myWebLog.Web/ViewModels.fs new file mode 100644 index 0000000..808c874 --- /dev/null +++ b/src/myWebLog.Web/ViewModels.fs @@ -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(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(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 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 diff --git a/src/myWebLog.Web/myWebLog.Web.fsproj b/src/myWebLog.Web/myWebLog.Web.fsproj index 09fe36d..ac279d5 100644 --- a/src/myWebLog.Web/myWebLog.Web.fsproj +++ b/src/myWebLog.Web/myWebLog.Web.fsproj @@ -52,6 +52,9 @@ + + + diff --git a/src/myWebLog/Properties/AssemblyInfo.cs b/src/myWebLog/Properties/AssemblyInfo.cs index f796b02..34eb3d8 100644 --- a/src/myWebLog/Properties/AssemblyInfo.cs +++ b/src/myWebLog/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/myWebLog/content/styles/admin.css b/src/myWebLog/content/styles/admin.css new file mode 100644 index 0000000..f6da49a --- /dev/null +++ b/src/myWebLog/content/styles/admin.css @@ -0,0 +1,5 @@ +footer { + background-color: #808080; + border-top: solid 1px black; + color: white; +} \ No newline at end of file diff --git a/src/myWebLog/data-config.json b/src/myWebLog/data-config.json new file mode 100644 index 0000000..6ff5578 --- /dev/null +++ b/src/myWebLog/data-config.json @@ -0,0 +1,3 @@ +{ + "database": "myWebLog" +} diff --git a/src/myWebLog/myWebLog.csproj b/src/myWebLog/myWebLog.csproj index 1ebe146..f68710a 100644 --- a/src/myWebLog/myWebLog.csproj +++ b/src/myWebLog/myWebLog.csproj @@ -48,6 +48,7 @@ + @@ -65,9 +66,14 @@ + + + + + + @Section['Content']; + +
+
+
+
@Model.generator
+
+
+
+ + + + @Section['Scripts']; + + \ No newline at end of file diff --git a/src/myWebLog/views/admin/post/list.html b/src/myWebLog/views/admin/post/list.html new file mode 100644 index 0000000..af49cc7 --- /dev/null +++ b/src/myWebLog/views/admin/post/list.html @@ -0,0 +1,60 @@ +@Master['admin/admin-layout'] + +@Section['Content'] + +
+ + + + + + + + @Each.posts + + + + + + + + @EndEach +
@Translate.Date@Translate.Title@Translate.Status@Translate.Tags
+ + // TODO format date + + @Current.title
+ @Translate.View |   + @Translate.Edit |   + @Translate.Delete +
@Current.status + + // TODO fix tags +
+
+
+
+ @If.hasNewer +

«  @Translate.NewerPosts

+ @EndIf +
+
+ @If.hasOlder +

@Translate.OlderPosts  »

+ @EndIf +
+
+@EndSection \ No newline at end of file diff --git a/src/myWebLog/views/default/index-content.html b/src/myWebLog/views/default/index-content.html index 83c18d3..10735ae 100644 --- a/src/myWebLog/views/default/index-content.html +++ b/src/myWebLog/views/default/index-content.html @@ -23,16 +23,17 @@ @EndEach
- // 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") -
- .col-xs-3.text-right - if hasPrior - p: a.btn.btn-primary(href=extraUrl + '/page/' + (page + 1))= __("Older Posts") -
\ No newline at end of file + @If.hasNewer +

+ @Translate.NewerPosts +

+ @EndIf + +
+ @If.hasOlder +

+ @Translate.OlderPosts +

+ @EndIf +
+ \ No newline at end of file diff --git a/src/myWebLog/views/default/page-content.html b/src/myWebLog/views/default/page-content.html new file mode 100644 index 0000000..06df8f9 --- /dev/null +++ b/src/myWebLog/views/default/page-content.html @@ -0,0 +1,4 @@ +
+

@Model.page.title

+ @Model.page.text +
\ No newline at end of file diff --git a/src/myWebLog/views/default/page.html b/src/myWebLog/views/default/page.html new file mode 100644 index 0000000..d6ab560 --- /dev/null +++ b/src/myWebLog/views/default/page.html @@ -0,0 +1,5 @@ +@Master['default/layout'] + +@Section['Content'] + @Partial['default/page-content', Model] +@EndSection \ No newline at end of file