Developing a Cloud Application

Jan Poeschko
Wolfram Research

Idea: Build a Blog

Site generator built in the Wolfram Language

Content coming from (cloud) notebooks

… deployed as cloud notebooks

… with index page, navigation header, comments

Final result

“Source” notebooks -> Generator code -> App (“Target” notebooks + index page)

Directory setup

targetDir=CloudObject["MyBlog"]

sourceDir=FileNameJoin[{targetDir,"Content"}]

commentsDir=FileNameJoin[{targetDir,"Comments"}]

CreateDirectory/@{sourceDir,commentsDir}

Content

CopyFile[FileNameJoin[{NotebookDirectory[],"Example.nb"}],FileNameJoin[{sourceDir,"2018-06-01-Example.nb"}]]

CloudDeploy[Manipulate[Factor[x^n+1],{n,10,100,1}],FileNameJoin[{sourceDir,"2018-06-15-Manipulate.nb"}]]

Mapping source to target objects

Construct simple ASCII-based URLs for deployed objects:

target[source_CloudObject]:=FileNameJoin[{targetDir,slugify[FileBaseName[First[source]]]}]

slugify[str_String]:=StringReplace[ToLowerCase[RemoveDiacritics[str]],{" "->"-"}]

The title of a page is everything after the date in its file name:

title[source_CloudObject]:=Last[StringSplit[FileBaseName[First[source]],"-",4]]

Index page

sources:=Sort[CloudObjects[sourceDir]]

sources

indexObj:=FileNameJoin[{targetDir,"index.nb"}]

indexObj

indexEntry[source_CloudObject]:=cell[Hyperlink[title[source],target[source]]]

cell[content_,opts___]:=Cell[BoxData[ToBoxes[content]],opts,ShowStringCharacters->False]

deployIndexPage[sources_]:=deployNotebook[Notebook[indexEntry/@sources],indexObj]

deployNotebook[nb_Notebook,obj_CloudObject]:=CloudExport[nb,"NB",obj,Permissions->"Public"]

deployIndexPage[sources]

Posts

date[source_CloudObject]:=DateString[DateObject[StringJoin[Take[StringSplit[FileBaseName[First[source]],"-",4],3],"-"]],{"MonthName"," ","DayShort",", ","Year"}]

dateCell[source_CloudObject]:=cell[date[source],"Subtitle",FontSize->13]

addDateCell[nb_Notebook,source_CloudObject]:=With[{pos=FirstPosition[nb,Cell[_,"Title",___]]},If[!MissingQ[pos],ReplacePart[nb,pos:>Sequence[Extract[nb,pos],dateCell[source]]],nb]]

navCell[prev_,next_]:=cell[Grid[{{If[prev=!=Null,Hyperlink[title[prev],target[prev]],""],If[next=!=Null,Hyperlink[title[next],target[next]],""]}},Alignment->{{Left,Right}},ItemSize->Scaled[0.5]]]

addNavCell[nb_Notebook,prev_,next_]:=Insert[nb,navCell[prev,next],{1,1}]

createPostNotebook[source_CloudObject,prev_,next_]:=addNavCell[addDateCell[CloudGet[source],source],prev,next]

deployPost[sources_,index_]:=With[{source=sources[[index]]},deployNotebook[createPostNotebook[source,previous[sources,index],next[sources,index]],target[source]]]

previous[list_,index_]:=If[index>1,list[[index-1]],Null]

next[list_,index_]:=If[index<Length[list],list[[index+1]],Null]

deployPosts[sources_]:=deployPost[sources,#]&/@Range[Length[sources]]

Function to deploy everything

deploy[]:=With[{index=deployIndexPage[sources],posts=deployPosts[sources]},{ParentDirectory[index],Length[posts]}]

deploy[]

Adding a header

header:=cell[Hyperlink[Style["My Blog",FontSize->24,FontFamily->"Arial",FontColor->White],ParentDirectory[indexObj]],Background->Lighter[Blue],CellFrameMargins->{{16,16},{4,10}}]

CellPrint[header]

Also hide any cell brackets in the deployed notebook:

notebookOptions:={DockedCells->{header},ShowCellBracket->False}

deployNotebook[nb_Notebook,obj_CloudObject]:=CloudExport[Append[nb,notebookOptions],"NB",obj,Permissions->"Public"]

deploy[]

Adding dates and summaries to entries on index page

summary[source_CloudObject]:=With[{nb=CloudGet[source]},With[{cells=Cases[nb,_Cell,Infinity]},Append[Take[DeleteCases[cells,Cell[_,"Title",___]],UpTo[3]],cell[Hyperlink["Read more…",target[source]],"Text"]]]]

indexEntry[source_CloudObject]:=Cell[CellGroupData[Join[{cell[Hyperlink[title[source],target[source]],"Title"],cell[date[source],"Subtitle",FontSize->13]},summary[source]]]]

deploy[]

Comments

Comments are stored as individual cloud objects, in one directory per post.

Render a user interface displaying a list of comments and a form (input field + button) to add a comment (if the user is authenticated; otherwise show a link to log in):

comments[postName_,postID_,commentsDir_,loginAPI_]:=DynamicModule[{comments={},commentForm,addComment,renderComment,commentCell},Dynamic[commentCell[comments],"Text"],Initialization:>(
commentForm[]:=DynamicModule[{comment=""},Column[{Style[InputField[Dynamic[comment],String,BaseStyle->"Text"],"Text"],Button["Comment",addComment[comments,comment,$RequesterWolframID,Now];comment=""]}]];
addComment[_,text_,author_,date_]:=With[{comment=<|"text"->text,"author"->author,"date"->date|>},AppendTo[comments,comment];Export[FileNameJoin[{commentsDir,postID,CreateUUID[]}],comment,"ExpressionJSON"]];
renderComment=Panel[#text,#author,Left]&;
commentCell[comments_]:=With[{loginURL=First[loginAPI]<>"/"<>URLEncode[postName]},Column[{If[$RequesterWolframID=!=None,commentForm[],Hyperlink["Log in to comment",loginURL]],Column[renderComment/@comments]}]];
comments=Import[#,"ExpressionJSON"]&/@With[{dir=FileNameJoin[{commentsDir,postID}]},If[DirectoryQ[dir],CloudObjects[dir],{}]];
)]

To log in a user, link to a non-public URLDispatcher that redirects back to the original post:

loginAPI:=CloudDeploy[URLDispatcher["/" ~~target___~~EndOfString:>HTTPRedirect[FileNameJoin[{targetDir,target}]]],FileNameJoin[{targetDir,"login"}],Permissions->{"Authenticated"->"Execute"}]

addCommentsSection[nb_Notebook,source_CloudObject]:=With[{uuid=CloudObjectInformation[source,"UUID"]},Insert[nb,Cell[CellGroupData[{cell["Comments","Section"],cell[comments[FileBaseName[First[target[source]]],uuid,commentsDir,loginAPI],"Text"]}]],{1,-1}]]

createPostNotebook[source_CloudObject,prev_,next_]:=addCommentsSection[addNavCell[addDateCell[CloudGet[source],source],prev,next],source]

deploy[]

Custom stylesheet

stylesheet=Notebook[{Cell[StyleData[StyleDefinitions->"Default.nb"]],Cell[StyleData["Text"],FontFamily->"Gentium Basic"]}];

CloudDeploy[stylesheet,FileNameJoin[{targetDir,"StyleSheets/styles.nb"}],Permissions->"Public"]

AppendTo[notebookOptions,StyleDefinitions->FrontEnd`FileName[{"StyleSheets/styles.nb"}]]

deploy[]

Deployment API

An API endpoint to deploy everything:

CloudDeploy[Delayed[deploy[]],FileNameJoin[{targetDir,"update"}]]

The many faces of the notebook interface

We’ve been using notebooks for…

defining content

deployment target

application development

defining stylesheets

this presentation

github.com/poeschko/WolframCloudBlogGenerator