Started working on the frontend
- Added the new post frontend - Hooked up publishing of posts to the site publisher - Added an site exporter as a publishing target
This commit is contained in:
parent
a59008b3e8
commit
e77cac2fd5
46
.air.toml
Normal file
46
.air.toml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./build/weiro"
|
||||
cmd = "make build"
|
||||
delay = 1000
|
||||
exclude_dir = ["static", "build", "node_modules"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html", "css", "js"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = true
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,6 @@
|
|||
build/
|
||||
.idea/
|
||||
node_modules/
|
||||
static/assets/
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
|
|
|||
17
Makefile
17
Makefile
|
|
@ -2,10 +2,27 @@ BUILD_DIR=build
|
|||
|
||||
all: clean build
|
||||
|
||||
.Phony: init
|
||||
init:
|
||||
npm install --save-exact --save-dev esbuild
|
||||
|
||||
.Phony: clean
|
||||
clean:
|
||||
-rm -r $(BUILD_DIR)
|
||||
|
||||
.Phony: frontend
|
||||
frontend:
|
||||
npm install
|
||||
npx esbuild --bundle ./assets/css/main.css --outfile=./static/assets/main.css
|
||||
|
||||
.Phony: gen
|
||||
gen:
|
||||
go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.30.0 generate
|
||||
|
||||
.Phony: build
|
||||
build: frontend
|
||||
go build -o ./build/weiro
|
||||
|
||||
.Phony: run
|
||||
run: build
|
||||
./build/weiro
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
---
|
||||
date: 2026-02-18T22:17:00+11:00
|
||||
id: atHn1bZa7Z0D
|
||||
title: First Post
|
||||
date: 2026-02-18T11:17:00Z
|
||||
tags: []
|
||||
slug: /2026/02/18/first-post
|
||||
|
||||
---
|
||||
Hello World!
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
---
|
||||
date: 2026-02-19T22:17:00+11:00
|
||||
id: ANRTjDSaCZcj
|
||||
title: About a DB
|
||||
date: 2026-02-19T11:17:00Z
|
||||
tags: []
|
||||
slug: /2026/02/19/about-a-db
|
||||
|
||||
---
|
||||
Hello again.
|
||||
|
||||
|
|
|
|||
8
_test-site/posts/2026/02/20-another-publish-tests.md
Normal file
8
_test-site/posts/2026/02/20-another-publish-tests.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
id: XFpDGhCLRSiV
|
||||
title: Another Publish Tests
|
||||
date: 2026-02-20T22:51:32Z
|
||||
tags: []
|
||||
slug: /2026/02/21/another-publish-tests
|
||||
---
|
||||
Okay, that wasn't as smooth sailing as I was hoping for. I forgot to set the publish key so Netlify refused to publish the site. Let see how this attempt goes.
|
||||
8
_test-site/posts/2026/02/20-export-as-publish.md
Normal file
8
_test-site/posts/2026/02/20-export-as-publish.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
id: yvLBWj26Zsgp
|
||||
title: Export As Publish Target
|
||||
date: 2026-02-20T23:17:26Z
|
||||
tags: []
|
||||
slug: /2026/02/21/export-as-publish
|
||||
---
|
||||
I've added a site exporter which will export the site as a series of Markdown files whenever I publish a post. With any luck, both the exporter and the Netlify publisher will work when I publish this. This will save the posts I've made so far, while allowing me to init the database without having to save new migrations.
|
||||
14
_test-site/posts/2026/02/20-first-attempt-at.md
Normal file
14
_test-site/posts/2026/02/20-first-attempt-at.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
id: ARFyO2TiZ0Ju
|
||||
title: First Attempt at Posting From the UI
|
||||
date: 2026-02-20T22:48:58Z
|
||||
tags: []
|
||||
slug: /2026/02/21/first-attempt-at
|
||||
---
|
||||
If you're seeing this, then posting from the UI works.
|
||||
|
||||
The UI is using service-side HTTP rendering, so there's nothing fancy going on here. I've chosen to use Boostrap for the frontend. It's a little traditional but hey, it looks good. I can't post a screenshot of the UI as Weiro doesn't have attachments yet, but I will once I have attachments working.
|
||||
|
||||
The new post should have been saved in the Sqlite3 database, which should trigger a rebuild of the site and a publish to Netlify. The existing posts should still be there too. At the moment, the publishing is done inline, which won't be the case for long, but I want to make sure the publishing flow is working properly.
|
||||
|
||||
Okay, here we go.
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
---
|
||||
date: 2026-02-20T17:36:00+11:00
|
||||
id: Go4SilAxyruR
|
||||
title: Direct Publish To Netlify
|
||||
date: 2026-02-20T06:36:00Z
|
||||
tags: []
|
||||
slug: /2026/02/20/netlify
|
||||
|
||||
---
|
||||
Just a quick one right now. Integrated the Netlify client allowing direct publish to Netlify.
|
||||
Previous attempts were using the Netlify CLI, but after learning that Go has a Netlify client,
|
||||
I have zero use for that now.
|
||||
|
||||
I think we're ready to start building the UI.
|
||||
13
_test-site/posts/2026/02/20-success.md
Normal file
13
_test-site/posts/2026/02/20-success.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
id: f-mK1xJDUlUg
|
||||
title: Success!
|
||||
date: 2026-02-20T22:59:18Z
|
||||
tags: []
|
||||
slug: /2026/02/21/success
|
||||
|
||||
---
|
||||
Okay, publishing from the frontend works.
|
||||
|
||||
Of course now I need to make sure all these posts are kept, as I would like to prevent a publish every time I make a change. So either enable a local preview mode or have a way to export the posts I save: both possible with the multiple publishing targets. And I want to be able to easily save posts as drafts, and also allow for local saves should the updated post be unable to reach the server.
|
||||
|
||||
Basically, I want to make sure the writing and publishing flow is as good as it can be, otherwise I wouldn't be comfortable using it. And for any blogging CMS, having a way to write posts and be comfortable that your words won't be lost is paramount.
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
title: Weiro
|
||||
tagline: A blogging CMS
|
||||
base_url: https://jolly-boba-9e2486.netlify.app/
|
||||
public:
|
||||
netlify:
|
||||
site_id: 55c878a7-189e-42cf-aa02-5c60908143f3
|
||||
base_url: https://jolly-boba-9e2486.netlify.app
|
||||
|
|
|
|||
15
assets/css/main.css
Normal file
15
assets/css/main.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
@import "bootstrap/dist/css/bootstrap.css";
|
||||
|
||||
.post-form {
|
||||
display: grid;
|
||||
grid-template-rows: min-content auto min-content;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.post-form textarea {
|
||||
height: 100%;
|
||||
}
|
||||
23
go.mod
23
go.mod
|
|
@ -1,6 +1,6 @@
|
|||
module lmika.dev/lmika/weiro
|
||||
|
||||
go 1.24.3
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
emperror.dev/errors v0.8.1
|
||||
|
|
@ -20,6 +20,7 @@ require (
|
|||
github.com/Azure/go-autorest/tracing v0.5.0 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
|
|
@ -36,27 +37,41 @@ require (
|
|||
github.com/go-openapi/swag v0.19.12 // indirect
|
||||
github.com/go-openapi/validate v0.20.0 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/gofiber/fiber/v3 v3.0.0 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // indirect
|
||||
github.com/gofiber/template v1.8.3 // indirect
|
||||
github.com/gofiber/template/html/v3 v3.0.2 // indirect
|
||||
github.com/gofiber/template/v2 v2.1.0 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
|
||||
github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/netlify/open-api/v2 v2.49.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rsc/goversion v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.4.4 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
|
|
|
|||
37
go.sum
37
go.sum
|
|
@ -43,6 +43,8 @@ github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4Rq
|
|||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
|
|
@ -203,6 +205,18 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
|
|||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/gofiber/fiber/v3 v3.0.0 h1:GPeCG8X60L42wLKrzgeewDHBr6pE6veAvwaXsqD3Xjk=
|
||||
github.com/gofiber/fiber/v3 v3.0.0/go.mod h1:kVZiO/AwyT5Pq6PgC8qRCJ+j/BHrMy5jNw1O9yH38aY=
|
||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||
github.com/gofiber/template/html/v3 v3.0.2 h1:/Fh8UcEsB4uhf1QWNbYaAOwXxSORebJ2zXkb5tgG/TI=
|
||||
github.com/gofiber/template/html/v3 v3.0.2/go.mod h1:9phaCZLPZq2nFNTZj9zrmR8FSA8ydtBQFL9SEsr4jqI=
|
||||
github.com/gofiber/template/v2 v2.1.0 h1:vrLY6uEW2HdioJm6J5FGUpYZuapVQhHciNz21XQjR/4=
|
||||
github.com/gofiber/template/v2 v2.1.0/go.mod h1:ohgpR/Ng90nJbK+IyNzrgR/XpnBNt862/oTF5G7SAmE=
|
||||
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
|
||||
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
|
|
@ -286,6 +300,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
|
|||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
|
|
@ -311,7 +327,11 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
|
|||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
|
|
@ -344,6 +364,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
|
|
@ -409,8 +431,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
|
|
@ -451,6 +479,8 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
@ -503,6 +533,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -548,6 +580,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -559,6 +593,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
@ -592,6 +628,7 @@ golang.org/x/tools v0.0.0-20200612220849-54c614fe050c/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package sitereader
|
||||
package importexport
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
|
@ -11,13 +11,13 @@ type ReadSiteModels struct {
|
|||
Posts []*models.Post
|
||||
}
|
||||
|
||||
type siteMeta struct {
|
||||
type Site struct {
|
||||
Title string `yaml:"title"`
|
||||
Tagline string `yaml:"tagline"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
}
|
||||
|
||||
type postMeta struct {
|
||||
type Post struct {
|
||||
ID string `yaml:"id"`
|
||||
Title string `yaml:"title"`
|
||||
Date time.Time `yaml:"date"`
|
||||
41
handlers/middleware/site.go
Normal file
41
handlers/middleware/site.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/providers/db"
|
||||
)
|
||||
|
||||
func RequiresSite(db *db.Provider) func(c fiber.Ctx) error {
|
||||
return func(c fiber.Ctx) error {
|
||||
siteIDStr := c.Params("siteID")
|
||||
if siteIDStr == "" {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
siteID, err := strconv.ParseInt(siteIDStr, 10, 64)
|
||||
if err != nil {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
user, ok := models.GetUser(c.Context())
|
||||
if !ok {
|
||||
return fiber.ErrUnauthorized
|
||||
}
|
||||
|
||||
site, err := db.SelectSiteByID(c.Context(), siteID)
|
||||
if err != nil {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
if site.OwnerID != user.ID {
|
||||
return fiber.ErrForbidden
|
||||
}
|
||||
|
||||
c.Locals("site", site)
|
||||
c.SetContext(models.WithSite(c.Context(), site))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
24
handlers/middleware/user.go
Normal file
24
handlers/middleware/user.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
)
|
||||
|
||||
func AuthUser() func(c fiber.Ctx) error {
|
||||
return func(c fiber.Ctx) error {
|
||||
// TEMP - Actually do the auth here
|
||||
user := models.User{
|
||||
ID: 1,
|
||||
Username: "testuser",
|
||||
}
|
||||
|
||||
c.Locals("user", user)
|
||||
c.SetContext(models.WithUser(c.Context(), user))
|
||||
log.Printf("User %s authenticated", user.Username)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
37
handlers/posts.go
Normal file
37
handlers/posts.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/services/posts"
|
||||
)
|
||||
|
||||
type PostsHandler struct {
|
||||
PostService *posts.Service
|
||||
}
|
||||
|
||||
func (ph PostsHandler) Index(c fiber.Ctx) error {
|
||||
return c.Render("posts/index", fiber.Map{})
|
||||
}
|
||||
|
||||
func (ph PostsHandler) New(c fiber.Ctx) error {
|
||||
return c.Render("posts/new", fiber.Map{
|
||||
"guid": models.NewNanoID(),
|
||||
})
|
||||
}
|
||||
|
||||
func (ph PostsHandler) Update(c fiber.Ctx) error {
|
||||
var req posts.CreatePostParams
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post, err := ph.PostService.PublishPost(c.Context(), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect().To(fmt.Sprintf("/sites/%v/posts", post.SiteID))
|
||||
}
|
||||
98
main.go
98
main.go
|
|
@ -1,15 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/static"
|
||||
"github.com/gofiber/template/html/v3"
|
||||
"lmika.dev/lmika/weiro/handlers"
|
||||
"lmika.dev/lmika/weiro/handlers/middleware"
|
||||
"lmika.dev/lmika/weiro/providers/db"
|
||||
"lmika.dev/lmika/weiro/services/importer"
|
||||
"lmika.dev/lmika/weiro/services/posts"
|
||||
"lmika.dev/lmika/weiro/services/publisher"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
|
|
@ -20,41 +21,66 @@ func main() {
|
|||
}
|
||||
defer dbp.Close()
|
||||
|
||||
user, err := dbp.SelectUserByUsername(context.Background(), "testuser")
|
||||
if err != nil {
|
||||
user = models.User{
|
||||
Username: "testuser",
|
||||
PasswordHashed: []byte("changeme"),
|
||||
}
|
||||
if err := dbp.SaveUser(context.Background(), &user); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
importerSvc := importer.New(dbp)
|
||||
publisherSvc := publisher.New(dbp)
|
||||
|
||||
ctx := models.WithUser(context.Background(), user)
|
||||
postService := posts.New(dbp, publisherSvc)
|
||||
|
||||
site, err := importerSvc.Import(ctx, "_test-site")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//user, err := dbp.SelectUserByUsername(context.Background(), "testuser")
|
||||
//if err != nil {
|
||||
// user = models.User{
|
||||
// Username: "testuser",
|
||||
// PasswordHashed: []byte("changeme"),
|
||||
// }
|
||||
// if err := dbp.SaveUser(context.Background(), &user); err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//}
|
||||
|
||||
target := models.SitePublishTarget{
|
||||
SiteID: site.ID,
|
||||
BaseURL: "https://jolly-boba-9e2486.netlify.app",
|
||||
TargetType: "netlify",
|
||||
TargetRef: "55c878a7-189e-42cf-aa02-5c60908143f3",
|
||||
TargetKey: os.Getenv("NETLIFY_AUTH_TOKEN"),
|
||||
}
|
||||
if err := dbp.SavePublishTarget(ctx, &target); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
app := fiber.New(fiber.Config{
|
||||
Views: html.New("./views", ".html"),
|
||||
ViewsLayout: "layouts/main",
|
||||
PassLocalsToViews: true,
|
||||
})
|
||||
|
||||
if err := publisherSvc.Publish(ctx, site.ID); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
siteGroup := app.Group("/sites/:siteID", middleware.AuthUser(), middleware.RequiresSite(dbp))
|
||||
|
||||
log.Println("Done")
|
||||
ph := handlers.PostsHandler{PostService: postService}
|
||||
|
||||
siteGroup.Get("/posts", ph.Index)
|
||||
siteGroup.Get("/posts/new", ph.New)
|
||||
siteGroup.Post("/posts", ph.Update)
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
return c.Redirect().To("/sites/1/posts")
|
||||
})
|
||||
app.Get("/static/*", static.New("./static"))
|
||||
|
||||
// TEMP
|
||||
//
|
||||
/*
|
||||
dbp.SaveUser(context.Background(), &models.User{Username: "testuser"})
|
||||
|
||||
ctx := models.WithUser(context.Background(), models.User{ID: 1})
|
||||
site, err := importer.New(dbp).Import(ctx, "_test-site")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
target := models.SitePublishTarget{
|
||||
SiteID: site.ID,
|
||||
BaseURL: "https://jolly-boba-9e2486.netlify.app",
|
||||
TargetType: "netlify",
|
||||
TargetRef: "55c878a7-189e-42cf-aa02-5c60908143f3",
|
||||
TargetKey: os.Getenv("NETLIFY_AUTH_TOKEN"),
|
||||
}
|
||||
|
||||
if err := dbp.SavePublishTarget(ctx, &target); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
*/
|
||||
//if err := publisherSvc.Publish(ctx, site.ID); err != nil {
|
||||
// log.Fatal(err)
|
||||
//}
|
||||
|
||||
log.Fatal(app.Listen(":3000"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package models
|
|||
import "context"
|
||||
|
||||
type userKeyType struct{}
|
||||
type siteKeyType struct{}
|
||||
|
||||
var userKey = userKeyType{}
|
||||
var siteKey = userKeyType{}
|
||||
|
||||
func WithUser(ctx context.Context, user User) context.Context {
|
||||
return context.WithValue(ctx, userKey, user)
|
||||
|
|
@ -14,3 +16,12 @@ func GetUser(ctx context.Context) (User, bool) {
|
|||
user, ok := ctx.Value(userKey).(User)
|
||||
return user, ok
|
||||
}
|
||||
|
||||
func WithSite(ctx context.Context, site Site) context.Context {
|
||||
return context.WithValue(ctx, siteKey, site)
|
||||
}
|
||||
|
||||
func GetSite(ctx context.Context) (Site, bool) {
|
||||
site, ok := ctx.Value(siteKey).(Site)
|
||||
return site, ok
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,3 +4,5 @@ import "emperror.dev/errors"
|
|||
|
||||
var UserRequiredError = errors.New("user required")
|
||||
var PermissionError = errors.New("permission denied")
|
||||
var NotFoundError = errors.New("not found")
|
||||
var SiteRequiredError = errors.New("site required")
|
||||
|
|
|
|||
8
models/ids.go
Normal file
8
models/ids.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package models
|
||||
|
||||
import "github.com/matoous/go-nanoid/v2"
|
||||
|
||||
func NewNanoID() string {
|
||||
id, _ := gonanoid.New(12)
|
||||
return id
|
||||
}
|
||||
34
models/ids_test.go
Normal file
34
models/ids_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewNanoID(t *testing.T) {
|
||||
id := NewNanoID()
|
||||
|
||||
if len(id) != 12 {
|
||||
t.Errorf("Expected ID length of 12, got %d", len(id))
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
t.Error("Expected non-empty ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewNanoID_Uniqueness(t *testing.T) {
|
||||
ids := make(map[string]bool)
|
||||
iterations := 1000
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
id := NewNanoID()
|
||||
if ids[id] {
|
||||
t.Errorf("Duplicate ID generated: %s", id)
|
||||
}
|
||||
ids[id] = true
|
||||
}
|
||||
|
||||
if len(ids) != iterations {
|
||||
t.Errorf("Expected %d unique IDs, got %d", iterations, len(ids))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
ID int64
|
||||
|
|
@ -12,3 +18,61 @@ type Post struct {
|
|||
CreatedAt time.Time
|
||||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
func (p *Post) BestSlug() string {
|
||||
if p.Slug != "" {
|
||||
return p.Slug
|
||||
}
|
||||
|
||||
bestDateToUse := p.PublishedAt
|
||||
if bestDateToUse.IsZero() {
|
||||
bestDateToUse = p.CreatedAt
|
||||
}
|
||||
|
||||
slugPath := firstNWords(p.Title, 3, wordForSlug)
|
||||
if slugPath == "" {
|
||||
slugPath = firstNWords(p.Body, 3, wordForSlug)
|
||||
}
|
||||
if slugPath != "" {
|
||||
slugPath = strings.Replace(strings.ToLower(slugPath), " ", "-", -1)
|
||||
} else {
|
||||
slugPath = p.GUID
|
||||
if slugPath == "" {
|
||||
slugPath = bestDateToUse.Format("150405")
|
||||
}
|
||||
}
|
||||
|
||||
datePart := fmt.Sprintf("%04d/%02d/%02d", bestDateToUse.Year(), bestDateToUse.Month(), bestDateToUse.Day())
|
||||
return fmt.Sprintf("/%s/%s", datePart, slugPath)
|
||||
}
|
||||
|
||||
func wordForSlug(word string) string {
|
||||
var sb strings.Builder
|
||||
for _, c := range word {
|
||||
if unicode.IsLetter(c) || unicode.IsNumber(c) {
|
||||
sb.WriteRune(c)
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func firstNWords(s string, n int, keepWord func(word string) string) string {
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
suitableWords := make([]string, 0, n)
|
||||
|
||||
scnr := bufio.NewScanner(strings.NewReader(s))
|
||||
scnr.Split(bufio.ScanWords)
|
||||
for scnr.Scan() {
|
||||
word := scnr.Text()
|
||||
if w := keepWord(word); w != "" {
|
||||
suitableWords = append(suitableWords, w)
|
||||
if len(suitableWords) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(suitableWords, " ")
|
||||
}
|
||||
|
|
|
|||
78
models/posts_test.go
Normal file
78
models/posts_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFirstNWords(t *testing.T) {
|
||||
tests := []struct {
|
||||
words int
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{words: 3, input: "This is a test string with multiple words", expected: "This is a"},
|
||||
{words: 5, input: "Short string", expected: "Short string"},
|
||||
{words: 0, input: "Empty string", expected: ""},
|
||||
{words: 3, input: " The rain in Spain etc.", expected: "The rain in"},
|
||||
{words: 3, input: " The? rain! in$ Spain etc.", expected: "The rain in"},
|
||||
{words: 3, input: " !!! The 23123 rain ++_+_+ in Spain etc.", expected: "The 23123 rain"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
result := firstNWords(test.input, test.words, wordForSlug)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPost_BestSlug(t *testing.T) {
|
||||
postDate := time.Date(2023, time.January, 1, 15, 12, 11, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
post Post
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "returns slug when slug is set",
|
||||
post: Post{Slug: "my-custom-slug", Title: "My Title", PublishedAt: postDate},
|
||||
expected: "my-custom-slug",
|
||||
},
|
||||
{
|
||||
name: "use title when slug is empty",
|
||||
post: Post{Slug: "", Title: "My Title", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/my-title",
|
||||
},
|
||||
{
|
||||
name: "use body when slug is empty",
|
||||
post: Post{Slug: "", Body: "My body", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/my-body",
|
||||
},
|
||||
{
|
||||
name: "use guid when body is empty",
|
||||
post: Post{GUID: "abc123", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/abc123",
|
||||
},
|
||||
{
|
||||
name: "use time component when guid is empty",
|
||||
post: Post{Slug: "", Title: "", PublishedAt: postDate},
|
||||
expected: "/2023/01/01/151211",
|
||||
},
|
||||
{
|
||||
name: "use created date if publish date is unset",
|
||||
post: Post{Slug: "", Title: "a title", CreatedAt: postDate},
|
||||
expected: "/2023/01/01/a-title",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := test.post.BestSlug()
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
529
package-lock.json
generated
Normal file
529
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
{
|
||||
"name": "weiro",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.8",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
package.json
Normal file
8
package.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"esbuild": "0.27.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8"
|
||||
}
|
||||
}
|
||||
11
providers/db/errors.go
Normal file
11
providers/db/errors.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"emperror.dev/errors"
|
||||
)
|
||||
|
||||
func ErrorIsNoRows(err error) bool {
|
||||
return errors.Is(err, sql.ErrNoRows)
|
||||
}
|
||||
|
|
@ -47,6 +47,26 @@ func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (int64,
|
|||
return id, err
|
||||
}
|
||||
|
||||
const selectPostByGUID = `-- name: SelectPostByGUID :one
|
||||
SELECT id, site_id, guid, title, body, slug, created_at, published_at FROM posts WHERE guid = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) SelectPostByGUID(ctx context.Context, guid string) (Post, error) {
|
||||
row := q.db.QueryRowContext(ctx, selectPostByGUID, guid)
|
||||
var i Post
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SiteID,
|
||||
&i.Guid,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.Slug,
|
||||
&i.CreatedAt,
|
||||
&i.PublishedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const selectPostsOfSite = `-- name: SelectPostsOfSite :many
|
||||
SELECT id, site_id, guid, title, body, slug, created_at, published_at FROM posts WHERE site_id = ? ORDER BY created_at DESC LIMIT 10
|
||||
`
|
||||
|
|
@ -82,3 +102,31 @@ func (q *Queries) SelectPostsOfSite(ctx context.Context, siteID int64) ([]Post,
|
|||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updatePost = `-- name: UpdatePost :exec
|
||||
UPDATE posts SET
|
||||
title = ?,
|
||||
body = ?,
|
||||
slug = ?,
|
||||
published_at = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdatePostParams struct {
|
||||
Title string
|
||||
Body string
|
||||
Slug string
|
||||
PublishedAt int64
|
||||
ID int64
|
||||
}
|
||||
|
||||
func (q *Queries) UpdatePost(ctx context.Context, arg UpdatePostParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updatePost,
|
||||
arg.Title,
|
||||
arg.Body,
|
||||
arg.Slug,
|
||||
arg.PublishedAt,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,20 +16,20 @@ func (db *Provider) SelectPostsOfSite(ctx context.Context, siteID int64) ([]*mod
|
|||
|
||||
posts := make([]*models.Post, len(rows))
|
||||
for i, row := range rows {
|
||||
posts[i] = &models.Post{
|
||||
ID: row.ID,
|
||||
SiteID: row.SiteID,
|
||||
GUID: row.Guid,
|
||||
Title: row.Title,
|
||||
Body: row.Body,
|
||||
Slug: row.Slug,
|
||||
CreatedAt: time.Unix(row.CreatedAt, 0).UTC(),
|
||||
PublishedAt: time.Unix(row.PublishedAt, 0).UTC(),
|
||||
}
|
||||
posts[i] = dbPostToPost(row)
|
||||
}
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func (db *Provider) SelectPostByGUID(ctx context.Context, guid string) (*models.Post, error) {
|
||||
row, err := db.queries.SelectPostByGUID(ctx, guid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dbPostToPost(row), nil
|
||||
}
|
||||
|
||||
func (db *Provider) SavePost(ctx context.Context, post *models.Post) error {
|
||||
if post.ID == 0 {
|
||||
newID, err := db.queries.InsertPost(ctx, sqlgen.InsertPostParams{
|
||||
|
|
@ -48,6 +48,24 @@ func (db *Provider) SavePost(ctx context.Context, post *models.Post) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// No update query defined in sqlgen yet
|
||||
return nil
|
||||
return db.queries.UpdatePost(ctx, sqlgen.UpdatePostParams{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Body: post.Body,
|
||||
Slug: post.Slug,
|
||||
PublishedAt: post.PublishedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
func dbPostToPost(row sqlgen.Post) *models.Post {
|
||||
return &models.Post{
|
||||
ID: row.ID,
|
||||
SiteID: row.SiteID,
|
||||
GUID: row.Guid,
|
||||
Title: row.Title,
|
||||
Body: row.Body,
|
||||
Slug: row.Slug,
|
||||
CreatedAt: time.Unix(row.CreatedAt, 0).UTC(),
|
||||
PublishedAt: time.Unix(row.PublishedAt, 0).UTC(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
80
providers/siteexporter/exporter.go
Normal file
80
providers/siteexporter/exporter.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
package siteexporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"lmika.dev/lmika/weiro/handlers/importexport"
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
)
|
||||
|
||||
type SiteExporter struct {
|
||||
site models.Site
|
||||
baseURL string
|
||||
baseDir string
|
||||
}
|
||||
|
||||
func New(site models.Site, baseURL, baseDir string) *SiteExporter {
|
||||
return &SiteExporter{
|
||||
site: site,
|
||||
baseDir: baseDir,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SiteExporter) WriteSiteYAML() error {
|
||||
siteYAML := importexport.Site{
|
||||
Title: s.site.Title,
|
||||
Tagline: s.site.Tagline,
|
||||
BaseURL: s.baseURL,
|
||||
}
|
||||
|
||||
bytes, err := yaml.Marshal(siteYAML)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(s.baseDir, "site.yaml"), bytes, 0644)
|
||||
}
|
||||
|
||||
func (s *SiteExporter) WritePost(post *models.Post) error {
|
||||
postMeta := importexport.Post{
|
||||
ID: post.GUID,
|
||||
Title: post.Title,
|
||||
Date: post.PublishedAt,
|
||||
Slug: post.Slug,
|
||||
}
|
||||
frontMatter, err := yaml.Marshal(postMeta)
|
||||
|
||||
slugBasePath := filepath.Base(post.Slug)
|
||||
postFilename := filepath.Join(s.baseDir, fmt.Sprintf("posts/%04d/%02d/%02d-%s.md",
|
||||
post.PublishedAt.Year(), post.PublishedAt.Month(), post.PublishedAt.Day(), slugBasePath))
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(postFilename), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(postFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := f.WriteString("---\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = f.Write(frontMatter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.WriteString("---\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(post.Body)
|
||||
return err
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package importer
|
||||
package _import
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -42,6 +42,9 @@ func (s *Service) Import(ctx context.Context, sitePath string) (models.Site, err
|
|||
|
||||
for _, post := range readSite.Posts {
|
||||
post.SiteID = site.ID
|
||||
if post.GUID == "" {
|
||||
post.GUID = models.NewNanoID()
|
||||
}
|
||||
if err := s.db.SavePost(ctx, post); err != nil {
|
||||
return models.Site{}, errors.Wrap(err, "failed to save post")
|
||||
}
|
||||
78
services/posts/service.go
Normal file
78
services/posts/service.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package posts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"lmika.dev/lmika/weiro/models"
|
||||
"lmika.dev/lmika/weiro/providers/db"
|
||||
"lmika.dev/lmika/weiro/services/publisher"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *db.Provider
|
||||
publisher *publisher.Publisher
|
||||
}
|
||||
|
||||
func New(db *db.Provider, publisher *publisher.Publisher) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
publisher: publisher,
|
||||
}
|
||||
}
|
||||
|
||||
type CreatePostParams struct {
|
||||
GUID string `form:"guid" json:"guid"`
|
||||
Title string `form:"title" json:"title"`
|
||||
Body string `form:"body" json:"body"`
|
||||
}
|
||||
|
||||
func (s *Service) PublishPost(ctx context.Context, params CreatePostParams) (*models.Post, error) {
|
||||
site, ok := models.GetSite(ctx)
|
||||
if !ok {
|
||||
return nil, models.SiteRequiredError
|
||||
}
|
||||
|
||||
post, err := s.fetchOrCreatePost(ctx, site, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
post.Title = params.Title
|
||||
post.Body = params.Body
|
||||
post.PublishedAt = time.Now()
|
||||
post.Slug = post.BestSlug()
|
||||
|
||||
if err := s.db.SavePost(ctx, post); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: do on separate thread
|
||||
if err := s.publisher.Publish(ctx, site); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func (s *Service) fetchOrCreatePost(ctx context.Context, site models.Site, params CreatePostParams) (*models.Post, error) {
|
||||
post, err := s.db.SelectPostByGUID(ctx, params.GUID)
|
||||
if err == nil {
|
||||
if post.SiteID != site.ID {
|
||||
return nil, models.NotFoundError
|
||||
}
|
||||
|
||||
return post, nil
|
||||
} else if !db.ErrorIsNoRows(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
post = &models.Post{
|
||||
SiteID: site.ID,
|
||||
GUID: params.GUID,
|
||||
Title: params.Title,
|
||||
Body: params.Body,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"lmika.dev/lmika/weiro/models/pubmodel"
|
||||
"lmika.dev/lmika/weiro/providers/db"
|
||||
"lmika.dev/lmika/weiro/providers/sitebuilder"
|
||||
"lmika.dev/lmika/weiro/providers/siteexporter"
|
||||
)
|
||||
|
||||
type Publisher struct {
|
||||
|
|
@ -27,27 +28,14 @@ func New(db *db.Provider) *Publisher {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Publisher) Publish(ctx context.Context, siteID int64) error {
|
||||
// Fetch site, ensure user is owner
|
||||
site, err := p.db.SelectSiteByID(ctx, siteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, ok := models.GetUser(ctx)
|
||||
if !ok {
|
||||
return models.UserRequiredError
|
||||
} else if user.ID != site.OwnerID {
|
||||
return models.PermissionError
|
||||
}
|
||||
|
||||
targets, err := p.db.SelectPublishTargetsOfSite(ctx, siteID)
|
||||
func (p *Publisher) Publish(ctx context.Context, site models.Site) error {
|
||||
targets, err := p.db.SelectPublishTargetsOfSite(ctx, site.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch all content of site
|
||||
posts, err := p.db.SelectPostsOfSite(ctx, siteID)
|
||||
posts, err := p.db.SelectPostsOfSite(ctx, site.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -77,6 +65,17 @@ func (p *Publisher) publishSite(ctx context.Context, pubSite pubmodel.Site, targ
|
|||
}
|
||||
|
||||
switch target.TargetType {
|
||||
case "export":
|
||||
exporter := siteexporter.New(pubSite.Site, target.BaseURL, target.TargetRef)
|
||||
if err := exporter.WriteSiteYAML(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range pubSite.Posts {
|
||||
if err := exporter.WritePost(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case "localfs":
|
||||
log.Printf("Building site at %s", target.TargetRef)
|
||||
return sb.BuildSite(target.TargetRef)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
-- name: SelectPostsOfSite :many
|
||||
SELECT * FROM posts WHERE site_id = ? ORDER BY created_at DESC LIMIT 10;
|
||||
|
||||
-- name: SelectPostByGUID :one
|
||||
SELECT * FROM posts WHERE guid = ? LIMIT 1;
|
||||
|
||||
-- name: InsertPost :one
|
||||
INSERT INTO posts (
|
||||
site_id,
|
||||
|
|
@ -12,3 +15,11 @@ INSERT INTO posts (
|
|||
published_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id;
|
||||
|
||||
-- name: UpdatePost :exec
|
||||
UPDATE posts SET
|
||||
title = ?,
|
||||
body = ?,
|
||||
slug = ?,
|
||||
published_at = ?
|
||||
WHERE id = ?;
|
||||
|
|
@ -36,3 +36,4 @@ CREATE TABLE posts (
|
|||
published_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_post_site ON posts (site_id);
|
||||
CREATE UNIQUE INDEX idx_post_guid ON posts (guid);
|
||||
20
views/_common/nav.html
Normal file
20
views/_common/nav.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Weiro</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Posts</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex" role="search">
|
||||
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"/>
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
14
views/layouts/main.html
Normal file
14
views/layouts/main.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/static/assets/main.css">
|
||||
</head>
|
||||
<body class="min-vh-100 d-flex flex-column">
|
||||
{{ template "_common/nav" . }}
|
||||
|
||||
{{ embed }}
|
||||
</body>
|
||||
</html>
|
||||
1
views/posts/index.html
Normal file
1
views/posts/index.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<h1>Posts go here</h1>
|
||||
14
views/posts/new.html
Normal file
14
views/posts/new.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<main class="flex-grow-1 position-relative">
|
||||
<form action="/sites/{{.site.ID}}/posts" method="post" class="container-fluid post-form p-2">
|
||||
<input type="hidden" name="guid" value="{{ .guid }}">
|
||||
<div class="mb-2">
|
||||
<input type="text" name="title" class="form-control" placeholder="Title">
|
||||
</div>
|
||||
<div>
|
||||
<textarea name="body" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" class="btn btn-primary mt-2" value="Publish">
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
Loading…
Reference in a new issue