README
WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath.
It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the fediverse via ActivityPub. It's easy to install and light enough to run on a Raspberry Pi.
Features
- Start a blog for yourself, or host a community of writers
- Form larger federated networks, and interact over modern protocols like ActivityPub
- Write on a fast, dead-simple, and distraction-free editor
- Format text with Markdown
- Organize posts with hashtags
- Create static pages
- Publish drafts and let others proofread them by sharing a private link
- Create multiple lightweight blogs under a single account
- Export all data in plain text files
- Read a stream of other posts in your writing community
- Build more advanced apps and extensions with the well-documented API
- Designed around user privacy and consent
Hosting
We offer two kinds of hosting services that make WriteFreely deployment painless: Write.as for individuals, and WriteFreely.host for communities. Besides saving you time, as a customer you directly help fund WriteFreely development.
Start a personal blog on Write.as, our flagship instance. Built to eliminate setup friction and preserve your privacy, Write.as helps you start a blog in seconds. It supports custom domains (with SSL) and multiple blogs / pen names per account. Read more here.
WriteFreely.host makes it easy to start a close-knit community — to share knowledge, complement your Mastodon instance, or publish updates in your organization. We take care of the hosting, upgrades, backups, and maintenance so you can focus on writing.
Quick start
WriteFreely has minimal requirements to get up and running — you only need to be able to run an executable.
Note this is currently alpha software. We're quickly moving out of this v0.x stage, but while we're in it, there are no guarantees that this is ready for production use.
First, download the latest release for your OS. It includes everything you need to start your blog.
Now extract the files from the archive, change into the directory, and do the following steps:
# 1) Configure your blog
./writefreely --config
# 2) (if you chose MySQL in the previous step) Log into MySQL and run:
# CREATE DATABASE writefreely;
# 3) (if you chose Multi-user setup) Import the schema with:
./writefreely --init-db
# 4) Generate data encryption keys
./writefreely --gen-keys
# 5) Run
./writefreely
# 6) Check out your site at the URL you specified in the setup process
# 7) There is no Step 7, you're done!
For running in production, see our guide.
Packages
WriteFreely is available in these package repositories:
Development
Ready to hack on your site? Here's a quick overview.
Prerequisites
Setting up
go get -d github.com/writeas/writefreely/cmd/writefreely
Configure your site, create your database, and import the schema as shown above. Then generate the remaining files you'll need:
make install # Generates encryption keys; installs LESS compiler
make ui # Generates CSS (run this whenever you update your styles)
make run # Runs the application
Docker
Using Docker for Development
If you'd like to use Docker as a base for working on a site's styles and such, you can run the following from a Bash shell.
Note: This process is intended only for working on site styling. If you'd like to run Write Freely in production as a Docker service, it'll require a little more work.
The docker-setup.sh
script will present you with a few questions to set up
your dev instance. You can hit enter for most of them, except for "Admin username"
and "Admin password." You'll probably have to wait a few seconds after running
docker-compose up -d
for the Docker services to come up before running the
bash script.
docker-compose up -d
./docker-setup.sh
Now you should be able to navigate to http://localhost:8080 and start working!
When you're completely done working, you can run docker-compose down
to destroy
your virtual environment, including your database data. Otherwise, docker-compose stop
will shut down your environment without destroying your data.
Using Docker for Production
Write Freely doesn't yet provide an official Docker pathway to production. We're working on it, though!
Contributing
We gladly welcome contributions to WriteFreely, whether in the form of code, bug reports, feature requests, translations, or documentation improvements.
Before contributing anything, please read our Contributing Guide. It describes the correct channels for submitting contributions and any potential requirements.
License
Licensed under the AGPL.
Documentation
Index ¶
- Constants
- Variables
- func AuthenticateUser(db writestore, accessToken string) (int64, error)
- func CachePosts(userID int64, p *[]PublicPost)
- func GetPostsCache(userID int64) *[]PublicPost
- func IsJSON(h string) bool
- func PostsContains(sl *[]PublicPost, s *PublicPost) bool
- func RouteCollections(handler *Handler, r *mux.Router)
- func RouteRead(handler *Handler, readPerm UserLevel, r *mux.Router)
- func Serve()
- func ViewFeed(app *app, w http.ResponseWriter, req *http.Request) error
- type AnonymousAuthPost
- type AnonymousPost
- type AuthCache
- type AuthUser
- type AuthenticatedPost
- type ClaimPostRequest
- type ClaimPostResult
- type Collection
- func (c *Collection) AvatarURL() string
- func (c *Collection) CanonicalURL() string
- func (c *Collection) DisplayCanonicalURL() string
- func (c *Collection) DisplayTitle() string
- func (c *Collection) FederatedAPIBase() string
- func (c *Collection) FederatedAccount() string
- func (c *Collection) ForPublic()
- func (c *Collection) FriendlyVisibility() string
- func (c *Collection) IsPrivate() bool
- func (c *Collection) IsProtected() bool
- func (c *Collection) IsPublic() bool
- func (c *Collection) IsUnlisted() bool
- func (c *Collection) NewFormat() *CollectionFormat
- func (c *Collection) NextPageURL(prefix string, n int, tl bool) string
- func (c *Collection) PersonObject(ids ...int64) *activitystreams.Person
- func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string
- func (c *Collection) RedirectingCanonicalURL(isRedir bool) string
- func (c *Collection) RenderMathJax() bool
- func (c *Collection) ShowFooterBranding() bool
- func (c *Collection) StyleSheetDisplay() template.CSS
- type CollectionFormat
- type CollectionObj
- type CollectionPage
- type DisplayCollection
- type ErrorPages
- type ExportUser
- type Handler
- func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc
- func (h *Handler) All(f handlerFunc) http.HandlerFunc
- func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc
- func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc
- func (h *Handler) Page(n string) http.HandlerFunc
- func (h *Handler) Redirect(url string, ul UserLevel) http.HandlerFunc
- func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc
- func (h *Handler) SetErrorPages(e *ErrorPages)
- func (h *Handler) User(f userHandlerFunc) http.HandlerFunc
- func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc
- func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc
- func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc
- func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc
- type InstanceStats
- type Invite
- type PinPostResult
- type Post
- func (p *Post) Created8601() string
- func (p *Post) CreatedDate() string
- func (p *Post) Direction() string
- func (p *Post) DisplayTitle() string
- func (p *Post) Excerpt() template.HTML
- func (p *Post) FormattedDisplayTitle() template.HTML
- func (p *Post) HasTag(tag string) bool
- func (p *Post) HasTitleLink() bool
- func (p *Post) IsScheduled() bool
- func (p *Post) PlainDisplayTitle() string
- func (p Post) Summary() string
- type PublicPost
- type PublicUser
- type RawPost
- type RemoteUser
- type SubmittedCollection
- type SubmittedPost
- type User
- type UserLevel
- type UserPage
Constants ¶
const ( CollPublic collVisibility = 1 << iota CollPrivate CollProtected )
const CollUnlisted collVisibility = 0
Visibility levels. Values are bitmasks, stored in the database as decimal numbers. If adding types, append them to this list. If removing, replace the desired visibility with a new value.
Variables ¶
var ( ErrBadFormData = impart.HTTPError{http.StatusBadRequest, "Expected valid form data."} ErrBadJSON = impart.HTTPError{http.StatusBadRequest, "Expected valid JSON object."} ErrBadJSONArray = impart.HTTPError{http.StatusBadRequest, "Expected valid JSON array."} ErrBadAccessToken = impart.HTTPError{http.StatusUnauthorized, "Invalid access token."} ErrNoAccessToken = impart.HTTPError{http.StatusBadRequest, "Authorization token required."} ErrNotLoggedIn = impart.HTTPError{http.StatusUnauthorized, "Not logged in."} ErrForbiddenCollection = impart.HTTPError{http.StatusForbidden, "You don't have permission to add to this collection."} ErrForbiddenEditPost = impart.HTTPError{http.StatusForbidden, "You don't have permission to update this post."} ErrBadRequestedType = impart.HTTPError{http.StatusNotAcceptable, "Bad requested Content-Type."} ErrNoPublishableContent = impart.HTTPError{http.StatusBadRequest, "Supply something to publish."} ErrInternalGeneral = impart.HTTPError{http.StatusInternalServerError, "The humans messed something up. They've been notified."} ErrInternalCookieSession = impart.HTTPError{http.StatusInternalServerError, "Could not get cookie session."} ErrCollectionNotFound = impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} ErrCollectionGone = impart.HTTPError{http.StatusGone, "This blog was unpublished."} ErrCollectionPageNotFound = impart.HTTPError{http.StatusNotFound, "Collection page doesn't exist."} ErrPostNotFound = impart.HTTPError{Status: http.StatusNotFound, Message: "Post not found."} ErrPostBanned = impart.HTTPError{Status: http.StatusGone, Message: "Post removed."} ErrPostUnpublished = impart.HTTPError{Status: http.StatusGone, Message: "Post unpublished by author."} ErrPostFetchError = impart.HTTPError{Status: http.StatusInternalServerError, Message: "We encountered an error getting the post. The humans have been alerted."} ErrUserNotFound = impart.HTTPError{http.StatusNotFound, "User doesn't exist."} ErrUserNotFoundEmail = impart.HTTPError{http.StatusNotFound, "Please enter your username instead of your email address."} )
Commonly returned HTTP errors
var (
ErrPostNoUpdatableVals = impart.HTTPError{http.StatusBadRequest, "Supply some properties to update."}
)
Post operation errors
var (
SQLiteEnabled bool
)
Functions ¶
func AuthenticateUser ¶
AuthenticateUser ensures a user with the given accessToken is valid. Call it before any operations that require authentication or optionally associate data with a user account. Returns an error if the given accessToken is invalid. Otherwise the associated user ID is returned.
func CachePosts ¶
func CachePosts(userID int64, p *[]PublicPost)
func GetPostsCache ¶
func GetPostsCache(userID int64) *[]PublicPost
func PostsContains ¶
func PostsContains(sl *[]PublicPost, s *PublicPost) bool
TODO: move this to utils after making it more generic
func RouteCollections ¶
Types ¶
type AnonymousAuthPost ¶
type AnonymousPost ¶
type AuthUser ¶
type AuthUser struct { AccessToken string `json:"access_token,omitempty"` Password string `json:"password,omitempty"` User *User `json:"user"` // Verbose user data Posts *[]PublicPost `json:"posts,omitempty"` Collections *[]Collection `json:"collections,omitempty"` }
AuthUser contains information for a newly authenticated user (either from signing up or logging in).
type AuthenticatedPost ¶
type AuthenticatedPost struct { ID string `json:"id" schema:"id"` *SubmittedPost }
type ClaimPostRequest ¶
type ClaimPostRequest struct { *AnonymousAuthPost CollectionAlias string `json:"collection"` CreateCollection bool `json:"create_collection"` // Generated properties Slug string `json:"-"` }
type ClaimPostResult ¶
type ClaimPostResult struct { ID string `json:"id,omitempty"` Code int `json:"code,omitempty"` ErrorMessage string `json:"error_msg,omitempty"` Post *PublicPost `json:"post,omitempty"` }
type Collection ¶
type Collection struct { ID int64 `datastore:"id" json:"-"` Alias string `datastore:"alias" schema:"alias" json:"alias"` Title string `datastore:"title" schema:"title" json:"title"` Description string `datastore:"description" schema:"description" json:"description"` Direction string `schema:"dir" json:"dir,omitempty"` Language string `schema:"lang" json:"lang,omitempty"` StyleSheet string `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"` Script string `datastore:"script" schema:"script" json:"script,omitempty"` Public bool `datastore:"public" json:"public"` Visibility collVisibility `datastore:"private" json:"-"` Format string `datastore:"format" json:"format,omitempty"` Views int64 `json:"views"` OwnerID int64 `datastore:"owner_id" json:"-"` PublicOwner bool `datastore:"public_owner" json:"-"` URL string `json:"url,omitempty"` // contains filtered or unexported fields }
TODO: add Direction to db TODO: add Language to db
func (*Collection) AvatarURL ¶
func (c *Collection) AvatarURL() string
func (*Collection) CanonicalURL ¶
func (c *Collection) CanonicalURL() string
CanonicalURL returns a fully-qualified URL to the collection.
func (*Collection) DisplayCanonicalURL ¶
func (c *Collection) DisplayCanonicalURL() string
func (*Collection) DisplayTitle ¶
func (c *Collection) DisplayTitle() string
func (*Collection) FederatedAPIBase ¶
func (c *Collection) FederatedAPIBase() string
func (*Collection) FederatedAccount ¶
func (c *Collection) FederatedAccount() string
func (*Collection) ForPublic ¶
func (c *Collection) ForPublic()
ForPublic modifies the Collection for public consumption, such as via the API.
func (*Collection) FriendlyVisibility ¶
func (c *Collection) FriendlyVisibility() string
func (*Collection) IsPrivate ¶
func (c *Collection) IsPrivate() bool
func (*Collection) IsProtected ¶
func (c *Collection) IsProtected() bool
func (*Collection) IsPublic ¶
func (c *Collection) IsPublic() bool
func (*Collection) IsUnlisted ¶
func (c *Collection) IsUnlisted() bool
func (*Collection) NewFormat ¶
func (c *Collection) NewFormat() *CollectionFormat
NewFormat creates a new CollectionFormat object from the Collection.
func (*Collection) NextPageURL ¶
func (c *Collection) NextPageURL(prefix string, n int, tl bool) string
NextPageURL provides a full URL for the next page of collection posts
func (*Collection) PersonObject ¶
func (c *Collection) PersonObject(ids ...int64) *activitystreams.Person
func (*Collection) PrevPageURL ¶
func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string
PrevPageURL provides a full URL for the previous page of collection posts, returning a /page/N result for pages >1
func (*Collection) RedirectingCanonicalURL ¶
func (c *Collection) RedirectingCanonicalURL(isRedir bool) string
func (*Collection) RenderMathJax ¶
func (c *Collection) RenderMathJax() bool
func (*Collection) ShowFooterBranding ¶
func (c *Collection) ShowFooterBranding() bool
func (*Collection) StyleSheetDisplay ¶
func (c *Collection) StyleSheetDisplay() template.CSS
type CollectionFormat ¶
type CollectionFormat struct {
Format string
}
func (*CollectionFormat) Ascending ¶
func (cf *CollectionFormat) Ascending() bool
func (*CollectionFormat) PostsPerPage ¶
func (cf *CollectionFormat) PostsPerPage() int
func (*CollectionFormat) ShowDates ¶
func (cf *CollectionFormat) ShowDates() bool
func (*CollectionFormat) Valid ¶
func (cf *CollectionFormat) Valid() bool
Valid returns whether or not a format value is valid.
type CollectionObj ¶
type CollectionObj struct { Collection TotalPosts int `json:"total_posts"` Owner *User `json:"owner,omitempty"` Posts *[]PublicPost `json:"posts,omitempty"` }
func (*CollectionObj) CanShowScript ¶
func (c *CollectionObj) CanShowScript() bool
func (*CollectionObj) ExternalScripts ¶
func (c *CollectionObj) ExternalScripts() []template.URL
func (*CollectionObj) ScriptDisplay ¶
func (c *CollectionObj) ScriptDisplay() template.JS
type CollectionPage ¶
type CollectionPage struct { page.StaticPage *DisplayCollection IsCustomDomain bool IsWelcome bool IsOwner bool CanPin bool Username string Collections *[]Collection PinnedPosts *[]PublicPost }
type DisplayCollection ¶
type DisplayCollection struct { *CollectionObj Prefix string IsTopLevel bool CurrentPage int TotalPages int Format *CollectionFormat }
type ErrorPages ¶
type ErrorPages struct { NotFound *template.Template Gone *template.Template InternalServerError *template.Template Blank *template.Template }
ErrorPages hold template HTML error pages for displaying errors to the user. In each, there should be a defined template named "base".
type ExportUser ¶
type ExportUser struct { *User Collections *[]CollectionObj `json:"collections"` AnonymousPosts []PublicPost `json:"posts"` }
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
func NewHandler ¶
func NewHandler(app *app) *Handler
NewHandler returns a new Handler instance, using the given StaticPage data, and saving alias to the application's CookieStore.
func (*Handler) Admin ¶
func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc
Admin handles requests on /admin routes
func (*Handler) All ¶
func (h *Handler) All(f handlerFunc) http.HandlerFunc
func (*Handler) Download ¶
func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc
func (*Handler) LogHandlerFunc ¶
func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc
func (*Handler) RedirectOnErr ¶
func (*Handler) SetErrorPages ¶
func (h *Handler) SetErrorPages(e *ErrorPages)
SetErrorPages sets the given set of ErrorPages as templates for any errors that come up.
func (*Handler) User ¶
func (h *Handler) User(f userHandlerFunc) http.HandlerFunc
User handles requests made in the web application by the authenticated user. This provides user-friendly HTML pages and actions that work in the browser.
func (*Handler) UserAPI ¶
func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc
UserAPI handles requests made in the API by the authenticated user. This provides user-friendly HTML pages and actions that work in the browser.
func (*Handler) UserAll ¶
func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc
type InstanceStats ¶
type Invite ¶
type Invite struct { ID string MaxUses sql.NullInt64 Created time.Time Expires *time.Time Inactive bool // contains filtered or unexported fields }
func (Invite) ExpiresFriendly ¶
type PinPostResult ¶
type Post ¶
type Post struct { ID string `db:"id" json:"id"` Slug null.String `db:"slug" json:"slug,omitempty"` Font string `db:"text_appearance" json:"appearance"` Language zero.String `db:"language" json:"language"` RTL zero.Bool `db:"rtl" json:"rtl"` Privacy int64 `db:"privacy" json:"-"` OwnerID null.Int `db:"owner_id" json:"-"` CollectionID null.Int `db:"collection_id" json:"-"` PinnedPosition null.Int `db:"pinned_position" json:"-"` Created time.Time `db:"created" json:"created"` Updated time.Time `db:"updated" json:"updated"` ViewCount int64 `db:"view_count" json:"-"` Title zero.String `db:"title" json:"title"` HTMLTitle template.HTML `db:"title" json:"-"` Content string `db:"content" json:"body"` HTMLContent template.HTML `db:"content" json:"-"` HTMLExcerpt template.HTML `db:"content" json:"-"` Tags []string `json:"tags"` Images []string `json:"images,omitempty"` OwnerName string `json:"owner,omitempty"` }
Post represents a post as found in the database.
func (*Post) Created8601 ¶
func (*Post) CreatedDate ¶
func (*Post) DisplayTitle ¶
DisplayTitle dynamically generates a title from the Post's contents if it doesn't already have an explicit title.
func (*Post) Excerpt ¶
Excerpt shows any text that comes before a (more) tag. TODO: use HTMLExcerpt in templates instead of this method
func (*Post) FormattedDisplayTitle ¶
FormattedDisplayTitle dynamically generates a title from the Post's contents if it doesn't already have an explicit title.
func (*Post) HasTitleLink ¶
func (*Post) IsScheduled ¶
func (*Post) PlainDisplayTitle ¶
PlainDisplayTitle dynamically generates a title from the Post's contents if it doesn't already have an explicit title.
func (Post) Summary ¶
Summary gives a shortened summary of the post based on the post's title, especially for display in a longer list of posts. It extracts a summary for posts in the Title\n\nBody format, returning nothing if the entire was short enough that the extracted title == extracted summary.
type PublicPost ¶
type PublicPost struct { *Post IsSubdomain bool `json:"-"` IsTopLevel bool `json:"-"` DisplayDate string `json:"-"` Views int64 `json:"views"` Owner *PublicUser `json:"-"` IsOwner bool `json:"-"` Collection *CollectionObj `json:"collection,omitempty"` }
PublicPost holds properties for a publicly returned post, i.e. a post in a context where the viewer may not be the owner. As such, sensitive metadata for the post is hidden and properties supporting the display of the post are added.
func (*PublicPost) ActivityObject ¶
func (p *PublicPost) ActivityObject() *activitystreams.Object
func (*PublicPost) CanonicalURL ¶
func (p *PublicPost) CanonicalURL() string
type PublicUser ¶
type PublicUser struct {
Username string `json:"username"`
}
type RawPost ¶
type RawPost struct {
Id, Slug string
Title string
Content string
Views int64
Font string
Created time.Time
IsRTL sql.NullBool
Language sql.NullString
OwnerID int64
CollectionID sql.NullInt64
Found bool
Gone bool
}
func (*RawPost) Created8601 ¶
func (*RawPost) UserFacingCreated ¶
type RemoteUser ¶
func (*RemoteUser) AsPerson ¶
func (ru *RemoteUser) AsPerson() *activitystreams.Person
type SubmittedCollection ¶
type SubmittedCollection struct { // Data used for updating a given collection ID int64 OwnerID uint64 // Form helpers PreferURL string `schema:"prefer_url" json:"prefer_url"` Privacy int `schema:"privacy" json:"privacy"` Pass string `schema:"password" json:"password"` MathJax bool `schema:"mathjax" json:"mathjax"` Handle string `schema:"handle" json:"handle"` // Actual collection values updated in the DB Alias *string `schema:"alias" json:"alias"` Title *string `schema:"title" json:"title"` Description *string `schema:"description" json:"description"` StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"` Script *sql.NullString `schema:"script" json:"script"` Visibility *int `schema:"visibility" json:"public"` Format *sql.NullString `schema:"format" json:"format"` }
func (*SubmittedCollection) FediverseHandle ¶
func (sc *SubmittedCollection) FediverseHandle() string
type SubmittedPost ¶
type SubmittedPost struct { Slug *string `json:"slug" schema:"slug"` Title *string `json:"title" schema:"title"` Content *string `json:"body" schema:"body"` Font string `json:"font" schema:"font"` IsRTL converter.NullJSONBool `json:"rtl" schema:"rtl"` Language converter.NullJSONString `json:"lang" schema:"lang"` Created *string `json:"created" schema:"created"` }
SubmittedPost represents a post supplied by a client for publishing or updating. Since Title and Content can be updated to "", they are pointers that can be easily tested to detect changes.
type User ¶
type User struct { ID int64 `json:"-"` Username string `json:"username"` HashedPass []byte `json:"-"` HasPass bool `json:"has_pass"` Email zero.String `json:"email"` Created time.Time `json:"created"` // contains filtered or unexported fields }
User is a consistent user object in the database and all contexts (auth and non-auth) in the API.
func (User) Cookie ¶
Cookie strips down an AuthUser to contain only information necessary for cookies.
func (User) CreatedFriendly ¶
func (*User) EmailClear ¶
EmailClear decrypts and returns the user's email, caching it in the user object.
type UserPage ¶
type UserPage struct { page.StaticPage PageTitle string Separator template.HTML IsAdmin bool CanInvite bool }
func NewUserPage ¶
func (*UserPage) SetMessaging ¶
Source Files
- account.go
- activitypub.go
- admin.go
- app.go
- auth.go
- cache.go
- collections.go
- database-no-sqlite.go
- database.go
- errors.go
- export.go
- feed.go
- handle.go
- hostmeta.go
- instance.go
- invites.go
- keys.go
- nodeinfo.go
- pad.go
- pages.go
- postrender.go
- posts.go
- read.go
- request.go
- routes.go
- session.go
- sitemap.go
- templates.go
- unregisteredusers.go
- users.go
- webfinger.go
Directories
Path | Synopsis |
---|---|
cmd
|
|
Package config holds and assists in the configuration of a writefreely instance.
|
Package config holds and assists in the configuration of a writefreely instance. |
Package migrations contains database migrations for WriteFreely
|
Package migrations contains database migrations for WriteFreely |
package page provides mechanisms and data for generating a WriteFreely page.
|
package page provides mechanisms and data for generating a WriteFreely page. |