Compare commits
15 Commits
activity_p
...
main
Author | SHA1 | Date |
---|---|---|
Niko Abeler | da06541e11 | |
Niko Abeler | 7b047a609f | |
Niko Abeler | a5f24427a1 | |
Niko Abeler | ec13fffbe9 | |
Niko Abeler | 686cd72ec2 | |
Niko Abeler | 29e875d2e5 | |
Niko Abeler | f958fa36fd | |
Niko Abeler | 1b347bcdac | |
Niko Abeler | c8b759a834 | |
Niko Abeler | 943bc10eaf | |
Niko Abeler | 4543e448ed | |
Niko Abeler | e0d6f4f223 | |
Niko Abeler | 49a602f68b | |
Niko Abeler | 596ab0047e | |
Niko Abeler | 5c05f48be3 |
|
@ -0,0 +1,45 @@
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-linux-amd64:
|
||||||
|
name: release linux/amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: E2E Test
|
||||||
|
run: |
|
||||||
|
cd e2e_tests
|
||||||
|
docker compose -f docker-compose.ci.yml up -d
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pytest
|
||||||
|
|
||||||
|
- name: Build Release
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
go build -o owl-linux-amd64 ./cmd/owl
|
||||||
|
gh release upload ${{github.event.release.tag_name}} owl-linux-amd64
|
|
@ -0,0 +1,38 @@
|
||||||
|
# This workflow will build a golang project
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||||
|
|
||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: E2E Test
|
||||||
|
run: |
|
||||||
|
cd e2e_tests
|
||||||
|
docker compose -f docker-compose.ci.yml up -d
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pytest
|
46
README.md
46
README.md
|
@ -2,17 +2,9 @@
|
||||||
|
|
||||||
# Owl Blogs
|
# Owl Blogs
|
||||||
|
|
||||||
A simple web server for blogs generated from Markdown files.
|
Owl-blogs is a blogging software focused on simplicity with IndieWeb and Fediverse support.
|
||||||
|
|
||||||
**_This project is not yet stable. Expect frequent breaking changes! Only use this if you are willing to regularly adjust your project accordingly._**
|
# Usage
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```
|
|
||||||
CGO_ENABLED=1 go build -o owl ./cmd/owl
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
@ -34,4 +26,36 @@ To retrieve a list of all commands run:
|
||||||
|
|
||||||
```
|
```
|
||||||
owl -h
|
owl -h
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
CGO_ENABLED=1 go build -o owl ./cmd/owl
|
||||||
|
```
|
||||||
|
|
||||||
|
For development with live reload use `air` ([has to be install first](https://github.com/cosmtrek/air))
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
The project has two test suites; "unit tests" written in go and "end-to-end tests" written in python.
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-to-End tests
|
||||||
|
|
||||||
|
- Start the docker compose setup in the `e2e_tests` directory.
|
||||||
|
- Install the python dependencies into a virtualenv
|
||||||
|
```
|
||||||
|
cd e2e_tests
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
- Run the e2e_tests with `pytest`
|
||||||
|
|
|
@ -498,7 +498,41 @@ func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) {
|
func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) {
|
||||||
|
slog.Info("Processing Entry Create for ActivityPub")
|
||||||
|
followers, err := svc.AllFollowers()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot retrieve followers")
|
||||||
|
}
|
||||||
|
|
||||||
|
object, err := svc.entryToObject(entry)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot convert object", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := vocab.UpdateNew(object.ID, object)
|
||||||
|
update.Actor = object.AttributedTo
|
||||||
|
update.To = object.To
|
||||||
|
update.Published = object.Published
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
jsonld.Context{
|
||||||
|
jsonld.ContextElement{
|
||||||
|
Term: "toot",
|
||||||
|
IRI: jsonld.IRI("http://joinmastodon.org/ns#"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).Marshal(update)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("marshalling error", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, follower := range followers {
|
||||||
|
actor, err := svc.GetActor(follower)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Unable to retrieve follower actor", "err", err)
|
||||||
|
}
|
||||||
|
svc.sendObject(actor, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) {
|
func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) {
|
||||||
|
@ -543,6 +577,13 @@ func (svc *ActivityPubService) entryToObject(entry model.Entry) (vocab.Object, e
|
||||||
if imageEntry, ok := entry.(*entrytypes.Image); ok {
|
if imageEntry, ok := entry.(*entrytypes.Image); ok {
|
||||||
return svc.imageToObject(imageEntry), nil
|
return svc.imageToObject(imageEntry), nil
|
||||||
}
|
}
|
||||||
|
if articleEntry, ok := entry.(*entrytypes.Article); ok {
|
||||||
|
return svc.articleToObject(articleEntry), nil
|
||||||
|
}
|
||||||
|
if recipeEntry, ok := entry.(*entrytypes.Recipe); ok {
|
||||||
|
return svc.recipeToObject(recipeEntry), nil
|
||||||
|
}
|
||||||
|
|
||||||
slog.Warn("entry type not yet supported for activity pub")
|
slog.Warn("entry type not yet supported for activity pub")
|
||||||
return vocab.Object{}, errors.New("entry type not supported")
|
return vocab.Object{}, errors.New("entry type not supported")
|
||||||
}
|
}
|
||||||
|
@ -595,11 +636,14 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
||||||
Type: vocab.DocumentType,
|
Type: vocab.DocumentType,
|
||||||
MediaType: vocab.MimeType(binaryFile.Mime()),
|
MediaType: vocab.MimeType(binaryFile.Mime()),
|
||||||
URL: vocab.ID(fullImageUrl),
|
URL: vocab.ID(fullImageUrl),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(content)},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
image := vocab.Note{
|
image := vocab.Image{
|
||||||
ID: vocab.ID(imageEntry.FullUrl(siteCfg)),
|
ID: vocab.ID(imageEntry.FullUrl(siteCfg)),
|
||||||
Type: "Note",
|
Type: "Image",
|
||||||
To: vocab.ItemCollection{
|
To: vocab.ItemCollection{
|
||||||
vocab.PublicNS,
|
vocab.PublicNS,
|
||||||
vocab.IRI(svc.FollowersUrl()),
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
@ -610,7 +654,7 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
||||||
{Value: vocab.Content(imageEntry.Title())},
|
{Value: vocab.Content(imageEntry.Title())},
|
||||||
},
|
},
|
||||||
Content: vocab.NaturalLanguageValues{
|
Content: vocab.NaturalLanguageValues{
|
||||||
{Value: vocab.Content(content)},
|
{Value: vocab.Content(imageEntry.Title() + "<br><br>" + string(content))},
|
||||||
},
|
},
|
||||||
Attachment: attachments,
|
Attachment: attachments,
|
||||||
// Tag: tags,
|
// Tag: tags,
|
||||||
|
@ -618,3 +662,51 @@ func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab
|
||||||
return image
|
return image
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) articleToObject(articleEntry *entrytypes.Article) vocab.Object {
|
||||||
|
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
content := articleEntry.Content()
|
||||||
|
|
||||||
|
image := vocab.Article{
|
||||||
|
ID: vocab.ID(articleEntry.FullUrl(siteCfg)),
|
||||||
|
Type: "Article",
|
||||||
|
To: vocab.ItemCollection{
|
||||||
|
vocab.PublicNS,
|
||||||
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
},
|
||||||
|
Published: *articleEntry.PublishedAt(),
|
||||||
|
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(articleEntry.Title())},
|
||||||
|
},
|
||||||
|
Content: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(string(content))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) recipeToObject(recipeEntry *entrytypes.Recipe) vocab.Object {
|
||||||
|
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
content := recipeEntry.Content()
|
||||||
|
|
||||||
|
image := vocab.Article{
|
||||||
|
ID: vocab.ID(recipeEntry.FullUrl(siteCfg)),
|
||||||
|
Type: "Article",
|
||||||
|
To: vocab.ItemCollection{
|
||||||
|
vocab.PublicNS,
|
||||||
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
},
|
||||||
|
Published: *recipeEntry.PublishedAt(),
|
||||||
|
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(recipeEntry.Title())},
|
||||||
|
},
|
||||||
|
Content: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(string(content))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -48,7 +48,13 @@ func (s *EntryService) Create(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Bus.NotifyCreated(entry)
|
// only notify if the publishing date is set
|
||||||
|
// otherwise this is a draft.
|
||||||
|
// listeners might publish the entry to other services/platforms
|
||||||
|
// this should only happen for publshed content
|
||||||
|
if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
||||||
|
s.Bus.NotifyCreated(entry)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +63,13 @@ func (s *EntryService) Update(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Bus.NotifyUpdated(entry)
|
// only notify if the publishing date is set
|
||||||
|
// otherwise this is a draft.
|
||||||
|
// listeners might publish the entry to other services/platforms
|
||||||
|
// this should only happen for publshed content
|
||||||
|
if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
||||||
|
s.Bus.NotifyUpdated(entry)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,6 +78,9 @@ func (s *EntryService) Delete(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// deletes should always be notfied
|
||||||
|
// a published entry might be converted to a draft before deletion
|
||||||
|
// omitting the deletion in this case would prevent deletion on other platforms
|
||||||
s.Bus.NotifyDeleted(entry)
|
s.Bus.NotifyDeleted(entry)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,9 @@ func setupService() *app.EntryService {
|
||||||
register := app.NewEntryTypeRegistry()
|
register := app.NewEntryTypeRegistry()
|
||||||
register.Register(&test.MockEntry{})
|
register.Register(&test.MockEntry{})
|
||||||
repo := infra.NewEntryRepository(db, register)
|
repo := infra.NewEntryRepository(db, register)
|
||||||
service := app.NewEntryService(repo, nil, app.NewEventBus())
|
cfgRepo := infra.NewConfigRepo(db)
|
||||||
|
cfgService := app.NewSiteConfigService(cfgRepo)
|
||||||
|
service := app.NewEntryService(repo, cfgService, app.NewEventBus())
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ func (svc *SiteConfigService) defaultConfig() model.SiteConfig {
|
||||||
return model.SiteConfig{
|
return model.SiteConfig{
|
||||||
Title: "My Owl-Blog",
|
Title: "My Owl-Blog",
|
||||||
SubTitle: "A freshly created blog",
|
SubTitle: "A freshly created blog",
|
||||||
HeaderColor: "#efc48c",
|
|
||||||
PrimaryColor: "#d37f12",
|
PrimaryColor: "#d37f12",
|
||||||
AuthorName: "",
|
AuthorName: "",
|
||||||
Me: []model.MeLinks{},
|
Me: []model.MeLinks{},
|
||||||
|
|
|
@ -92,7 +92,6 @@ var importCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
v2Config.Title = v1Config.Title
|
v2Config.Title = v1Config.Title
|
||||||
v2Config.SubTitle = v1Config.SubTitle
|
v2Config.SubTitle = v1Config.SubTitle
|
||||||
v2Config.HeaderColor = v1Config.HeaderColor
|
|
||||||
v2Config.AuthorName = v1Config.AuthorName
|
v2Config.AuthorName = v1Config.AuthorName
|
||||||
v2Config.Me = mes
|
v2Config.Me = mes
|
||||||
v2Config.Lists = lists
|
v2Config.Lists = lists
|
||||||
|
|
|
@ -22,7 +22,6 @@ type MenuItem struct {
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
Title string
|
Title string
|
||||||
SubTitle string
|
SubTitle string
|
||||||
HeaderColor string
|
|
||||||
PrimaryColor string
|
PrimaryColor string
|
||||||
AuthorName string
|
AuthorName string
|
||||||
Me []MeLinks
|
Me []MeLinks
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: web
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
mock_masto:
|
||||||
|
build: mock_masto
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
|
@ -1,11 +1,17 @@
|
||||||
certifi==2024.2.2
|
certifi==2024.2.2
|
||||||
|
cffi==1.16.0
|
||||||
charset-normalizer==3.3.2
|
charset-normalizer==3.3.2
|
||||||
|
cryptography==42.0.7
|
||||||
exceptiongroup==1.2.1
|
exceptiongroup==1.2.1
|
||||||
|
http-message-signatures==0.5.0
|
||||||
|
http_sfv==0.9.9
|
||||||
idna==3.7
|
idna==3.7
|
||||||
iniconfig==2.0.0
|
iniconfig==2.0.0
|
||||||
packaging==24.0
|
packaging==24.0
|
||||||
pluggy==1.5.0
|
pluggy==1.5.0
|
||||||
|
pycparser==2.22
|
||||||
pytest==8.2.0
|
pytest==8.2.0
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
|
typing_extensions==4.11.0
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
|
|
|
@ -57,6 +57,7 @@ def test_following(client, inbox_url, followers_url, actor_url):
|
||||||
|
|
||||||
def test_unfollow(client, inbox_url, followers_url, actor_url):
|
def test_unfollow(client, inbox_url, followers_url, actor_url):
|
||||||
ensure_follow(client, inbox_url, actor_url)
|
ensure_follow(client, inbox_url, actor_url)
|
||||||
|
sleep(0.5)
|
||||||
with msg_inc(1):
|
with msg_inc(1):
|
||||||
req = sign(
|
req = sign(
|
||||||
"POST",
|
"POST",
|
||||||
|
|
|
@ -13,7 +13,12 @@
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/index.xml">
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/index.xml">
|
||||||
|
|
||||||
<link rel='stylesheet' href='/static/owl.css'>
|
<link rel='stylesheet' href='/static/owl.css'>
|
||||||
<link rel='stylesheet' href='/static/style.css'>
|
<link rel='stylesheet' href='/static/style.css'>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: {{.SiteConfig.PrimaryColor}};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{{ .SiteConfig.HtmlHeadExtra }}
|
{{ .SiteConfig.HtmlHeadExtra }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -67,6 +72,10 @@
|
||||||
<div>
|
<div>
|
||||||
{{ .SiteConfig.FooterExtra}}
|
{{ .SiteConfig.FooterExtra}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:var(--s2);">
|
||||||
|
powered by <i><a href="https://github.com/H4kor/owl-blogs" target="_blank">owl-blogs</a></i>
|
||||||
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -14,9 +14,6 @@
|
||||||
<label for="SubTitle">SubTitle</label>
|
<label for="SubTitle">SubTitle</label>
|
||||||
<input type="text" name="SubTitle" id="SubTitle" value="{{.SubTitle}}"/>
|
<input type="text" name="SubTitle" id="SubTitle" value="{{.SubTitle}}"/>
|
||||||
|
|
||||||
<label for="HeaderColor">HeaderColor</label>
|
|
||||||
<input type="color" name="HeaderColor" id="HeaderColor" value="{{.HeaderColor}}"/>
|
|
||||||
|
|
||||||
<label for="PrimaryColor">PrimaryColor</label>
|
<label for="PrimaryColor">PrimaryColor</label>
|
||||||
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,8 @@ func NewWebApp(
|
||||||
apService *app.ActivityPubService,
|
apService *app.ActivityPubService,
|
||||||
) *WebApp {
|
) *WebApp {
|
||||||
fiberApp := fiber.New(fiber.Config{
|
fiberApp := fiber.New(fiber.Config{
|
||||||
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
||||||
|
DisableStartupMessage: true,
|
||||||
})
|
})
|
||||||
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
|
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ func (h *SiteConfigHandler) HandlePost(c *fiber.Ctx) error {
|
||||||
|
|
||||||
siteConfig.Title = c.FormValue("Title")
|
siteConfig.Title = c.FormValue("Title")
|
||||||
siteConfig.SubTitle = c.FormValue("SubTitle")
|
siteConfig.SubTitle = c.FormValue("SubTitle")
|
||||||
siteConfig.HeaderColor = c.FormValue("HeaderColor")
|
|
||||||
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
||||||
siteConfig.AuthorName = c.FormValue("AuthorName")
|
siteConfig.AuthorName = c.FormValue("AuthorName")
|
||||||
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
||||||
|
|
Loading…
Reference in New Issue