aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVidhu Kant Sharma <vidhukant@vidhukant.com>2025-05-06 12:38:32 +0530
committerVidhu Kant Sharma <vidhukant@vidhukant.com>2025-05-06 12:38:32 +0530
commit6c49d1ec3153852f0e5d4cff334a1bc0476f9dce (patch)
tree033bf3d95e5c9f8bd2205591355ddc468d4825ab
first commitv1.0.0
-rw-r--r--LICENSE0
-rw-r--r--README.md87
-rw-r--r--cmd/dump.go85
-rw-r--r--cmd/root.go51
-rw-r--r--cmd/serve.go125
-rw-r--r--db/db.go53
-rw-r--r--go.mod28
-rw-r--r--go.sum62
-rw-r--r--guestbook.toml25
-rw-r--r--main.go26
10 files changed, 542 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/LICENSE
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d7259d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,87 @@
+# Guestbook
+
+Standalone guestbook server meant to be paired up with a static website.
+
+# Usage
+
+## Configuration
+
+Edit `/etc/guestbook.toml`
+
+```toml
+# the idea is to have a single database but different
+# tables for each guestbook (if any)
+
+[database]
+# Supported - MySQL/MariaDB
+username = "guestbook"
+password = "guestbookpass"
+location = "tcp(127.0.0.1:3306)"
+db_name = "guestbook"
+params = "charset=utf8mb4&parseTime=True&loc=Local"
+
+# add as many as you like
+# avoid having hyphens in the name
+[my_guestbook]
+port = 2222
+path = "/sign"
+validation_string = ""
+success_redirect = "/success"
+error_redirect = "/error"
+template = """
+{{range .}}
+{{print "{{"}}
+ guestbook_entry(
+ name=\"{{.Name}}\",
+ website=\"{{.Website}}\",
+ message=\"{{.Message}}\"
+ )
+{{print "}}"}}
+{{end}}"""
+```
+
+## Starting with Systemd
+
+Then use this Systemd service:
+
+`/etc/systemd/system/my_guestbook-guestbook.service`
+
+Replace `my_guestbook` with your guestbook's name (as defined in `/etc/guestbook.toml`),
+create more `.service` files for as many guestbooks you want to have.
+
+```
+[Unit]
+Description=Guestbook for my_guestbook
+After=network.target
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=on-failure
+RestartSec=1
+ExecStart=guestbook --guestbook my_guestbook
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Then, reload the daemon and start the guestbook:
+
+```fish
+systemctl daemon-reload
+systemctl enable --now my_guestbook-guestbook
+```
+
+## Reading entries
+
+```fish
+guestbook dump -g my_guestbook
+```
+
+## Licence
+
+Licenced under GNU General Public Licence
+
+GNU GPL License: [LICENSE](LICENSE)
+
+Copyright (c) 2025 Vidhu Kant Sharma
diff --git a/cmd/dump.go b/cmd/dump.go
new file mode 100644
index 0000000..281b7d6
--- /dev/null
+++ b/cmd/dump.go
@@ -0,0 +1,85 @@
+/* guestbook - Standalone guestbook server for static websites
+ * Copyright (C) 2025 Vidhu Kant Sharma <vidhukant@vidhukant.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package cmd
+
+import (
+ "fmt"
+ "log"
+ "bytes"
+ "text/template"
+
+ "vidhukant.com/guestbook/db"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var dumpCmd = &cobra.Command{
+ Use: "dump",
+ Short: "Dump the entries into STDOUT",
+ Long: "Dump the entries into STDOUT",
+ Run: func(cmd *cobra.Command, args []string) {
+ guestbookName, err := cmd.Flags().GetString("guestbook")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ outputTemplate := viper.GetString(guestbookName + ".template")
+
+ conn := db.Connect()
+ defer conn.Close()
+
+ rows, err := conn.Query(fmt.Sprintf("SELECT name, website, message FROM %s", guestbookName))
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer rows.Close()
+
+
+ var entries []db.GuestbookEntry
+
+ for rows.Next() {
+ var entry db.GuestbookEntry
+ if err := rows.Scan(&entry.Name, &entry.Website, &entry.Message); err != nil {
+ log.Fatal(err)
+ }
+ entries = append(entries, entry)
+ }
+ if rows.Err() != nil {
+ log.Fatal(err)
+ }
+
+ tmpl, err := template.New("render").Parse(outputTemplate)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var buf bytes.Buffer
+ if err := tmpl.Execute(&buf, entries); err != nil {
+ panic(err)
+ }
+
+ fmt.Println(buf.String())
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(dumpCmd)
+
+ dumpCmd.Flags().StringP("guestbook", "g", "my_guestbook", "Guestbook name (defined in /etc/guestbook.toml)")
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..2d1e63d
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,51 @@
+/* guestbook - Standalone guestbook server for static websites
+ * Copyright (C) 2025 Vidhu Kant Sharma <vidhukant@vidhukant.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package cmd
+
+import (
+ "os"
+ "log"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "guestbook",
+ Version: "v1.0.0",
+ Short: "Standalone guestbook server for static websites",
+ Long: "Standalone guestbook server for static websites",
+}
+
+func Execute() {
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+ cobra.OnInitialize(func () {
+ viper.SetConfigName("guestbook")
+ viper.AddConfigPath("/etc")
+
+ if err := viper.ReadInConfig(); err != nil {
+ log.Fatal(err)
+ }
+ })
+}
diff --git a/cmd/serve.go b/cmd/serve.go
new file mode 100644
index 0000000..e3d6b36
--- /dev/null
+++ b/cmd/serve.go
@@ -0,0 +1,125 @@
+/* guestbook - Standalone guestbook server for static websites
+ * Copyright (C) 2025 Vidhu Kant Sharma <vidhukant@vidhukant.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package cmd
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "encoding/json"
+
+ "vidhukant.com/guestbook/db"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+var serveCmd = &cobra.Command{
+ Use: "serve",
+ Short: "Start the guestbook server",
+ Long: "Start the guestbook server",
+ Run: func(cmd *cobra.Command, args []string) {
+ guestbookName, err := cmd.Flags().GetString("guestbook")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ port := viper.GetString(guestbookName + ".port")
+ validationString := viper.GetString(guestbookName + ".validation_string")
+
+ path := viper.GetString(guestbookName + ".path")
+ if path == "/" {
+ log.Fatalf("Setting %s.path to / is not allowed as it may cause issues.", guestbookName)
+ }
+
+ successRedirect := viper.GetString(guestbookName + ".success_redirect")
+ if successRedirect == "" {
+ log.Fatalf("%s.success_redirect cannot be blank.", guestbookName)
+ }
+
+ errorRedirect := viper.GetString(guestbookName + ".error_redirect")
+ if errorRedirect == "" {
+ log.Fatalf("%s.error_redirect cannot be blank.", guestbookName)
+ }
+
+
+ conn := db.Connect()
+ defer conn.Close()
+
+ _, err = conn.Exec(
+ fmt.Sprintf(
+ "CREATE TABLE IF NOT EXISTS %s (" +
+ "id INT AUTO_INCREMENT PRIMARY KEY," +
+ "name TEXT," +
+ "website TEXT," +
+ "message TEXT" +
+ ");",
+ guestbookName,
+ ),
+ )
+ if err != nil {
+ log.Fatalf("Error while creating table: %v\n", err)
+ }
+
+ http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Redirect(w, r, errorRedirect, http.StatusSeeOther)
+ return
+ }
+
+ var data db.GuestbookEntry
+ err := json.NewDecoder(r.Body).Decode(&data)
+ if err != nil {
+ log.Printf("Error occurred while decoding data: %v")
+ http.Redirect(w, r, errorRedirect, http.StatusSeeOther)
+ return
+ }
+
+ if data.Validation != validationString || data.Name == "" || data.Message == "" {
+ http.Redirect(w, r, errorRedirect, http.StatusSeeOther)
+ return
+ }
+
+ stmt, err := conn.Prepare(fmt.Sprintf("INSERT INTO %s (name, website, message) VALUES (?, ?, ?)", guestbookName))
+ if err != nil {
+ log.Printf("Error occurred while preparing statement data: %v\n")
+ http.Redirect(w, r, errorRedirect, http.StatusSeeOther)
+ return
+ }
+ defer stmt.Close()
+
+ _, err = stmt.Exec(data.Name, data.Website, data.Message)
+ if err != nil {
+ log.Printf("Error occurred while saving data: %v\n")
+ http.Redirect(w, r, errorRedirect, http.StatusSeeOther)
+ return
+ }
+
+ http.Redirect(w, r, successRedirect, http.StatusSeeOther)
+ })
+
+ log.Printf("Guestbook %s listening to POST %s on PORT %s.\n", guestbookName, path, port)
+ log.Fatal(http.ListenAndServe(":" + port, nil))
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(serveCmd)
+
+ serveCmd.Flags().StringP("guestbook", "g", "my_guestbook", "Guestbook name (defined in /etc/guestbook.toml)")
+}
diff --git a/db/db.go b/db/db.go
new file mode 100644
index 0000000..11b6fd4
--- /dev/null
+++ b/db/db.go
@@ -0,0 +1,53 @@
+/* guestbook - Standalone guestbook server for static websites
+ * Copyright (C) 2025 Vidhu Kant Sharma <vidhukant@vidhukant.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package db
+
+import (
+ "fmt"
+ "log"
+ "database/sql"
+
+ "github.com/spf13/viper"
+ _ "github.com/go-sql-driver/mysql"
+)
+
+type GuestbookEntry struct {
+ Name string `json:"name"`
+ Website string `json:"website"`
+ Message string `json:"message"`
+ Validation string `json:"validation"`
+}
+
+func Connect() *sql.DB {
+ conn, err := sql.Open(
+ "mysql",
+ fmt.Sprintf(
+ "%s:%s@%s/%s?%s",
+ viper.GetString("database.username"),
+ viper.GetString("database.password"),
+ viper.GetString("database.location"),
+ viper.GetString("database.db_name"),
+ viper.GetString("database.params"),
+ ),
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return conn
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..0298058
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,28 @@
+module vidhukant.com/guestbook
+
+go 1.24.2
+
+require (
+ github.com/go-sql-driver/mysql v1.9.2
+ github.com/spf13/cobra v1.9.1
+ github.com/spf13/viper v1.20.1
+)
+
+require (
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/fsnotify/fsnotify v1.8.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+ github.com/sagikazarmark/locafero v0.7.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.12.0 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..6211c2f
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,62 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
+github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
+github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
+github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/guestbook.toml b/guestbook.toml
new file mode 100644
index 0000000..0a1c7c0
--- /dev/null
+++ b/guestbook.toml
@@ -0,0 +1,25 @@
+# the idea is to have a single database but different
+# tables for each guestbook (if any)
+[database]
+username = "guestbook"
+password = "guestbookpass"
+location = "tcp(127.0.0.1:3306)"
+db_name = "guestbook"
+params = "charset=utf8mb4&parseTime=True&loc=Local"
+
+[my_guestbook]
+port = 2222
+path = "/sign"
+validation_string = ""
+success_redirect = "/success"
+error_redirect = "/error"
+template = """
+{{range .}}
+{{print "{{"}}
+ guestbook_entry(
+ name=\"{{.Name}}\",
+ website=\"{{.Website}}\",
+ message=\"{{.Message}}\"
+ )
+{{print "}}"}}
+{{end}}"""
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..a65d537
--- /dev/null
+++ b/main.go
@@ -0,0 +1,26 @@
+/* guestbook - Standalone guestbook server for static websites
+ * Copyright (C) 2025 Vidhu Kant Sharma <vidhukant@vidhukant.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package main
+
+import (
+ "vidhukant.com/guestbook/cmd"
+)
+
+func main() {
+ cmd.Execute()
+}