From 63c04a9b44d1e6b81f2ab742e2d6fd810585b46e Mon Sep 17 00:00:00 2001 From: partisan Date: Wed, 14 Aug 2024 12:43:07 +0200 Subject: [PATCH] wip --- data/news/1.md | 5 + data/news/2.md | 3 + data/news/3.md | 3 + data/news/4.md | 3 + discord.go | 34 ++++ go.mod | 13 ++ go.sum | 14 ++ main.go | 466 +++++++++++++++++++++++++++++++++++++++++++- rss.go | 67 +++++++ run.sh | 3 + telegram.go | 26 +++ templates/news.html | 108 ++++++++++ 12 files changed, 737 insertions(+), 8 deletions(-) create mode 100644 data/news/1.md create mode 100644 data/news/2.md create mode 100644 data/news/3.md create mode 100644 data/news/4.md create mode 100644 discord.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 rss.go create mode 100755 run.sh create mode 100644 telegram.go create mode 100644 templates/news.html diff --git a/data/news/1.md b/data/news/1.md new file mode 100644 index 0000000..6531a22 --- /dev/null +++ b/data/news/1.md @@ -0,0 +1,5 @@ +# lorem ipsum + + Vestibulum fermentum tortor id mi. Nullam at arcu a est sollicitudin euismod. Nullam faucibus mi quis velit. Mauris dictum facilisis augue. Nullam sapien sem, ornare ac, nonummy non, lobortis a enim. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Etiam commodo dui eget wisi. Mauris dictum facilisis augue. Etiam posuere lacus quis dolor. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. + +Vivamus luctus egestas leo. Phasellus faucibus molestie nisl. Etiam commodo dui eget wisi. Donec vitae arcu. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Etiam neque. Suspendisse sagittis ultrices augue. Suspendisse nisl. Etiam sapien elit, consequat eget, tristique non, venenatis quis, ante. Phasellus rhoncus. Maecenas libero. \ No newline at end of file diff --git a/data/news/2.md b/data/news/2.md new file mode 100644 index 0000000..9967b1f --- /dev/null +++ b/data/news/2.md @@ -0,0 +1,3 @@ +# lorem ipsum + +Fusce nibh. Duis sapien nunc, commodo et, interdum suscipit, sollicitudin et, dolor. Aliquam in lorem sit amet leo accumsan lacinia. Fusce wisi. Curabitur sagittis hendrerit ante. Mauris elementum mauris vitae tortor. Nulla accumsan, elit sit amet varius semper, nulla mauris mollis quam, tempor suscipit diam nulla vel leo. Phasellus rhoncus. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Duis sapien nunc, commodo et, interdum suscipit, sollicitudin et, dolor. Aenean placerat. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Cras elementum. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Maecenas lorem. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. \ No newline at end of file diff --git a/data/news/3.md b/data/news/3.md new file mode 100644 index 0000000..4970246 --- /dev/null +++ b/data/news/3.md @@ -0,0 +1,3 @@ +# lorem ipsum + +Etiam sapien elit, consequat eget, tristique non, venenatis quis, ante. Nulla turpis magna, cursus sit amet, suscipit a, interdum id, felis. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Nullam faucibus mi quis velit. Aliquam erat volutpat. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis risus. In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Nam sed tellus id magna elementum tincidunt. Donec vitae arcu. Etiam bibendum elit eget erat. \ No newline at end of file diff --git a/data/news/4.md b/data/news/4.md new file mode 100644 index 0000000..58fd6e3 --- /dev/null +++ b/data/news/4.md @@ -0,0 +1,3 @@ +# lorem ipsum + +Mauris dictum facilisis augue. Nullam at arcu a est sollicitudin euismod. Praesent dapibus. Suspendisse sagittis ultrices augue. Integer imperdiet lectus quis justo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. Pellentesque sapien. Fusce dui leo, imperdiet in, aliquam sit amet, feugiat eu, orci. Maecenas ipsum velit, consectetuer eu lobortis ut, dictum at dui. Etiam dictum tincidunt diam. Vestibulum fermentum tortor id mi. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Pellentesque pretium lectus id turpis. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Maecenas aliquet accumsan leo. Morbi leo mi, nonummy eget tristique non, rhoncus non leo. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. \ No newline at end of file diff --git a/discord.go b/discord.go new file mode 100644 index 0000000..28daaf6 --- /dev/null +++ b/discord.go @@ -0,0 +1,34 @@ +package main + +import ( + "bytes" + "encoding/json" + "log" + "net/http" +) + +const discordWebhookURL = "YOUR_DISCORD_WEBHOOK_URL" // Replace with your Discord webhook URL + +type DiscordWebhookPayload struct { + Content string `json:"content"` +} + +func sendDiscordNotification(content string) { + payload := DiscordWebhookPayload{Content: content} + payloadBytes, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling Discord webhook payload: %v", err) + return + } + + resp, err := http.Post(discordWebhookURL, "application/json", bytes.NewBuffer(payloadBytes)) + if err != nil { + log.Printf("Error sending Discord webhook: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + log.Printf("Unexpected status code from Discord webhook: %d", resp.StatusCode) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a825ae9 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module my-web + +go 1.18 + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect + github.com/gorilla/feeds v1.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/technoweenie/multipartstreamer v1.0.1 // indirect + golang.org/x/sys v0.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f539dca --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= +github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= +github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 49e6681..a92190e 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,469 @@ package main import ( + "flag" "fmt" + "html/template" + "io/ioutil" + "log" "net/http" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/fsnotify/fsnotify" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/russross/blackfriday/v2" ) -func main() { - // Define the directory where your HTML and CSS files are located - http.Handle("/", http.FileServer(http.Dir("."))) +const ( + dataDir = "./data" + templateDir = "./templates" + staticDir = "./static" + defaultPort = 8080 + botTokenEnv = "YOUR_TELEGRAM_BOT_TOKEN" // Replace with your bot's token or set via environment variable + pageSize = 5 // Number of blog entries per page +) - // Start the web server on specfied port - port := 10369 - fmt.Printf("Server is running on http://localhost:%d\n", port) - err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) +type Blog struct { + Name string + Entries []BlogEntry +} + +type BlogEntry struct { + Content string + Date time.Time + Number int +} + +type PageData struct { + Title string + Description string + BlogLinks []string + Content template.HTML + PrevLink string + NextLink string +} + +var ( + blogs []Blog + bot *tgbotapi.BotAPI + port int + creationTimes = make(map[string]time.Time) + creationTimesM sync.Mutex +) + +func init() { + flag.IntVar(&port, "p", defaultPort, "Specify the port to run the server on") + flag.IntVar(&port, "port", defaultPort, "Specify the port to run the server on") +} + +func main() { + flag.Parse() + + botToken := os.Getenv("TELEGRAM_BOT_TOKEN") + if botToken == "" { + botToken = botTokenEnv + } + + var err error + bot, err = tgbotapi.NewBotAPI(botToken) if err != nil { - fmt.Println("Error:", err) + log.Printf("Warning: Error creating Telegram bot: %v", err) + } else { + go startTelegramBot() + } + + blogs, err = getBlogs(dataDir) + if err != nil { + log.Fatalf("Error getting blogs: %v", err) + } + + go watchForChanges(dataDir) + + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + pageData := PageData{ + Title: "My Blog", + Description: "Welcome to my blog about various topics.", + BlogLinks: getBlogLinks(), + Content: template.HTML(""), + } + renderTemplate(w, pageData) + return + } + + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) < 2 { + http.NotFound(w, r) + return + } + + siteURL := fmt.Sprintf("http://localhost:%d", port) // Define siteURL here + + if pathParts[1] == "rss" { + generateRSSFeed(w, blogs, siteURL) + return + } + + if pathParts[1] == "all" { + page, err := strconv.Atoi(r.URL.Query().Get("page")) + if err != nil || page < 1 { + page = 1 + } + renderAllBlogs(w, r, page) + return + } + + if len(pathParts) == 3 { + blogName := pathParts[1] + entryNumber, err := strconv.Atoi(pathParts[2]) + if err == nil { + renderBlogEntry(w, blogName, entryNumber) + return + } + } + + blogName := pathParts[1] + for _, blog := range blogs { + if blog.Name == blogName { + http.Redirect(w, r, fmt.Sprintf("/%s/%d", blogName, blog.Entries[0].Number), http.StatusFound) + return + } + } + + http.NotFound(w, r) + }) + + serverURL := fmt.Sprintf("http://localhost:%d", port) + log.Printf("Starting server on %s", serverURL) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) +} + +func getBlogs(dir string) ([]Blog, error) { + var blogs []Blog + + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, file := range files { + if file.IsDir() { + blog, err := getBlogEntries(filepath.Join(dir, file.Name())) + if err != nil { + return nil, err + } + blogs = append(blogs, blog) + } + } + + return blogs, nil +} + +func getBlogEntries(dir string) (Blog, error) { + var entries []BlogEntry + + files, err := ioutil.ReadDir(dir) + if err != nil { + return Blog{}, err + } + + for _, file := range files { + if filepath.Ext(file.Name()) == ".md" { + content, err := ioutil.ReadFile(filepath.Join(dir, file.Name())) + if err != nil { + return Blog{}, err + } + + date, err := getFileModTime(filepath.Join(dir, file.Name())) + if err != nil { + return Blog{}, err + } + + number, err := strconv.Atoi(strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))) + if err != nil { + return Blog{}, err + } + + entry := BlogEntry{ + Content: string(content), + Date: date, + Number: number, + } + entries = append(entries, entry) + sendDiscordNotification(fmt.Sprintf("New blog entry: %d", number)) + } + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Number > entries[j].Number + }) + + blog := Blog{ + Name: filepath.Base(dir), + Entries: entries, + } + + return blog, nil +} + +func watchForChanges(dir string) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatalf("Error creating file watcher: %v", err) + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&fsnotify.Create == fsnotify.Create { + handleFileChange(event.Name, true) + } else if event.Op&fsnotify.Write == fsnotify.Write { + handleFileChange(event.Name, false) + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Printf("File watcher error: %v", err) + } + } + }() + + err = watcher.Add(dir) + if err != nil { + log.Fatalf("Error adding directory to watcher: %v", err) + } + + // Add subdirectories + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && path != dir { + return watcher.Add(path) + } + return nil + }) + if err != nil { + log.Fatalf("Error adding subdirectories to watcher: %v", err) + } + + // Block forever + select {} +} + +func handleFileChange(path string, isNew bool) { + if filepath.Ext(path) == ".md" { + creationTimesM.Lock() + defer creationTimesM.Unlock() + if isNew { + creationTimes[path] = time.Now() + } + + dir := filepath.Dir(path) + blogName := filepath.Base(dir) + updateBlogEntries(blogName, path) } } + +func updateBlogEntries(blogName, path string) { + for i, blog := range blogs { + if blog.Name == blogName { + content, err := ioutil.ReadFile(path) + if err != nil { + log.Printf("Error reading file: %v", err) + return + } + + number, err := strconv.Atoi(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))) + if err != nil { + log.Printf("Error extracting number from file: %v", err) + return + } + + entry := BlogEntry{ + Content: string(content), + Date: creationTimes[path], + Number: number, + } + + updated := false + for j, e := range blogs[i].Entries { + if e.Number == number { + blogs[i].Entries[j] = entry + updated = true + break + } + } + + if !updated { + blogs[i].Entries = append(blogs[i].Entries, entry) + } + + sort.Slice(blogs[i].Entries, func(a, b int) bool { + return blogs[i].Entries[a].Number > blogs[i].Entries[b].Number + }) + + log.Printf("Updated blog %s with entry %d", blogName, number) + return + } + } + + // If blog not found, create new one + content, err := ioutil.ReadFile(path) + if err != nil { + log.Printf("Error reading file: %v", err) + return + } + + number, err := strconv.Atoi(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))) + if err != nil { + log.Printf("Error extracting number from file: %v", err) + return + } + + entry := BlogEntry{ + Content: string(content), + Date: creationTimes[path], + Number: number, + } + + newBlog := Blog{ + Name: blogName, + Entries: []BlogEntry{entry}, + } + + blogs = append(blogs, newBlog) + log.Printf("Created new blog %s with entry %d", blogName, number) +} + +func renderBlogEntry(w http.ResponseWriter, blogName string, entryNumber int) { + var blog Blog + for _, b := range blogs { + if b.Name == blogName { + blog = b + break + } + } + + var entry BlogEntry + var prevLink, nextLink string + for i, e := range blog.Entries { + if e.Number == entryNumber { + entry = e + if i > 0 { + prevLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i-1].Number) + } + if i < len(blog.Entries)-1 { + nextLink = fmt.Sprintf("/%s/%d", blog.Name, blog.Entries[i+1].Number) + } + break + } + } + + htmlContent := blackfriday.Run([]byte(entry.Content)) + content := fmt.Sprintf("
%s
", htmlContent) + + pageData := PageData{ + Title: "", + Description: "", + BlogLinks: getBlogLinks(), + Content: template.HTML(content), + PrevLink: prevLink, + NextLink: nextLink, + } + + renderTemplate(w, pageData) +} + +func getBlogLinks() []string { + var links []string + for _, blog := range blogs { + links = append(links, blog.Name) + } + return links +} + +func renderTemplate(w http.ResponseWriter, pageData PageData) { + tmpl, err := template.ParseFiles(filepath.Join(templateDir, "news.html")) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + log.Printf("Error parsing template: %v", err) + return + } + + err = tmpl.Execute(w, pageData) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + log.Printf("Error executing template: %v", err) + } +} + +func renderAllBlogs(w http.ResponseWriter, r *http.Request, page int) { + var allEntries []BlogEntry + + for _, blog := range blogs { + allEntries = append(allEntries, blog.Entries...) + } + + sort.Slice(allEntries, func(i, j int) bool { + return allEntries[i].Date.After(allEntries[j].Date) + }) + + totalEntries := len(allEntries) + startIndex := (page - 1) * pageSize + endIndex := startIndex + pageSize + if startIndex >= totalEntries { + http.NotFound(w, r) + return + } + if endIndex > totalEntries { + endIndex = totalEntries + } + + pagedEntries := allEntries[startIndex:endIndex] + + var content strings.Builder + for _, entry := range pagedEntries { + htmlContent := blackfriday.Run([]byte(entry.Content)) + content.WriteString(fmt.Sprintf("
%s
", htmlContent)) + } + + var prevLink, nextLink string + if startIndex > 0 { + prevLink = fmt.Sprintf("/all?page=%d", page-1) + } + if endIndex < totalEntries { + nextLink = fmt.Sprintf("/all?page=%d", page+1) + } + + renderTemplate(w, PageData{ + Title: "All Blogs", + Description: "Combined blog entries from all blogs.", + BlogLinks: getBlogLinks(), + Content: template.HTML(content.String()), + PrevLink: prevLink, + NextLink: nextLink, + }) +} + +func getFileModTime(path string) (time.Time, error) { + info, err := os.Stat(path) + if err != nil { + return time.Time{}, err + } + return info.ModTime(), nil +} diff --git a/rss.go b/rss.go new file mode 100644 index 0000000..80c3665 --- /dev/null +++ b/rss.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/gorilla/feeds" +) + +func generateRSSFeed(w http.ResponseWriter, blogs []Blog, siteURL string) { + feed := &feeds.Feed{ + Title: "My Blog", + Link: &feeds.Link{Href: siteURL}, + Description: "A blog about various topics.", + Author: &feeds.Author{Name: "Your Name", Email: "your-email@example.com"}, + Created: time.Now(), + } + + for _, blog := range blogs { + for _, entry := range blog.Entries { + feed.Items = append(feed.Items, &feeds.Item{ + Title: fmt.Sprintf("Entry %d", entry.Number), // Use entry number as title + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/%d", siteURL, blog.Name, entry.Number)}, + Description: entry.Content, + Created: entry.Date, + }) + } + } + + rss, err := feed.ToRss() + if err != nil { + http.Error(w, "Error generating RSS feed", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/rss+xml") + w.Write([]byte(rss)) +} + +func generateBlogRSSFeed(w http.ResponseWriter, blog Blog, siteURL string) { + feed := &feeds.Feed{ + Title: blog.Name, + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", siteURL, blog.Name)}, + Description: blog.Name, + Author: &feeds.Author{Name: "Your Name", Email: "your-email@example.com"}, + Created: time.Now(), + } + + for _, entry := range blog.Entries { + feed.Items = append(feed.Items, &feeds.Item{ + Title: fmt.Sprintf("Entry %d", entry.Number), // Use entry number as title + Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/%d", siteURL, blog.Name, entry.Number)}, + Description: entry.Content, + Created: entry.Date, + }) + } + + rss, err := feed.ToRss() + if err != nil { + http.Error(w, "Error generating RSS feed", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/rss+xml") + w.Write([]byte(rss)) +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..fd938e4 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ + + +go run discord.go rss.go telegram.go main.go \ No newline at end of file diff --git a/telegram.go b/telegram.go new file mode 100644 index 0000000..efc036c --- /dev/null +++ b/telegram.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func startTelegramBot() { + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + + for update := range updates { + if update.Message == nil { + continue + } + + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + _, err := bot.Send(msg) + if err != nil { + log.Printf("Error sending message: %v", err) + } + } +} diff --git a/templates/news.html b/templates/news.html new file mode 100644 index 0000000..8a9ab39 --- /dev/null +++ b/templates/news.html @@ -0,0 +1,108 @@ + + + + + + {{.Title}} + + + + + + + + + + + + + + + + +
+
+
+
+
+ + +
+ + + + + +
+ +
+
+
+ {{.Content}} +
+ +
+
+
+ + + +
+ + + + + + + + + + + +