mirror of
https://github.com/labstack/echo.git
synced 2025-04-21 12:17:04 +02:00
recipe: twitter like api
Signed-off-by: Vishal Rana <vr@labstack.com>
This commit is contained in:
parent
ff8a3bdc94
commit
f10daac5d6
11
glide.lock
generated
11
glide.lock
generated
@ -1,5 +1,5 @@
|
|||||||
hash: c3a41d26c94d8101a2aeaea5bc438cb75c5e9b7bd80850c7ec806c00c535d1c7
|
hash: 19ec155dd3ba25b967dd609c52b748b5302a30bede03936645c12fef7e9edd75
|
||||||
updated: 2016-12-02T08:49:24.85691-08:00
|
updated: 2016-12-05T20:47:56.995548765-08:00
|
||||||
imports:
|
imports:
|
||||||
- name: github.com/daaku/go.zipexe
|
- name: github.com/daaku/go.zipexe
|
||||||
version: a5fe2436ffcb3236e175e5149162b41cd28bd27d
|
version: a5fe2436ffcb3236e175e5149162b41cd28bd27d
|
||||||
@ -57,6 +57,13 @@ imports:
|
|||||||
- internal/log
|
- internal/log
|
||||||
- internal/modules
|
- internal/modules
|
||||||
- internal/remote_api
|
- internal/remote_api
|
||||||
|
- name: gopkg.in/mgo.v2
|
||||||
|
version: 3f83fa5005286a7fe593b055f0d7771a7dce4655
|
||||||
|
subpackages:
|
||||||
|
- bson
|
||||||
|
- internal/json
|
||||||
|
- internal/sasl
|
||||||
|
- internal/scram
|
||||||
testImports:
|
testImports:
|
||||||
- name: github.com/davecgh/go-spew
|
- name: github.com/davecgh/go-spew
|
||||||
version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9
|
version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9
|
||||||
|
@ -19,6 +19,7 @@ import:
|
|||||||
subpackages:
|
subpackages:
|
||||||
- websocket
|
- websocket
|
||||||
- package: google.golang.org/appengine
|
- package: google.golang.org/appengine
|
||||||
|
- package: gopkg.in/mgo.v2
|
||||||
testImport:
|
testImport:
|
||||||
- package: github.com/stretchr/testify
|
- package: github.com/stretchr/testify
|
||||||
subpackages:
|
subpackages:
|
||||||
|
14
recipe/twitter/handler/handler.go
Normal file
14
recipe/twitter/handler/handler.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import mgo "gopkg.in/mgo.v2"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Handler struct {
|
||||||
|
DB *mgo.Session
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Key (Should come from somewhere else).
|
||||||
|
Key = "secret"
|
||||||
|
)
|
73
recipe/twitter/handler/post.go
Normal file
73
recipe/twitter/handler/post.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/recipe/twitter/model"
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) CreatePost(c echo.Context) (err error) {
|
||||||
|
u := &model.User{
|
||||||
|
ID: bson.ObjectIdHex(userIDFromToken(c)),
|
||||||
|
}
|
||||||
|
p := &model.Post{
|
||||||
|
ID: bson.NewObjectId(),
|
||||||
|
From: u.ID.Hex(),
|
||||||
|
}
|
||||||
|
if err = c.Bind(p); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if p.To == "" || p.Message == "" {
|
||||||
|
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid to or message fields"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user from database
|
||||||
|
db := h.DB.Clone()
|
||||||
|
defer db.Close()
|
||||||
|
if err = db.DB("twitter").C("users").FindId(u.ID).One(u); err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return echo.ErrNotFound
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save post in database
|
||||||
|
if err = db.DB("twitter").C("posts").Insert(p); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusCreated, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FetchPost(c echo.Context) (err error) {
|
||||||
|
userID := userIDFromToken(c)
|
||||||
|
page, _ := strconv.Atoi(c.QueryParam("page"))
|
||||||
|
limit, _ := strconv.Atoi(c.QueryParam("limit"))
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve posts from database
|
||||||
|
posts := []*model.Post{}
|
||||||
|
db := h.DB.Clone()
|
||||||
|
if err = db.DB("twitter").C("posts").
|
||||||
|
Find(bson.M{"to": userID}).
|
||||||
|
Skip((page - 1) * limit).
|
||||||
|
Limit(limit).
|
||||||
|
All(&posts); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, posts)
|
||||||
|
}
|
97
recipe/twitter/handler/user.go
Normal file
97
recipe/twitter/handler/user.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/recipe/twitter/model"
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) Signup(c echo.Context) (err error) {
|
||||||
|
// Bind
|
||||||
|
u := &model.User{ID: bson.NewObjectId()}
|
||||||
|
if err = c.Bind(u); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if u.Email == "" || u.Password == "" {
|
||||||
|
return &echo.HTTPError{Code: http.StatusBadRequest, Message: "invalid email or password"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save user
|
||||||
|
db := h.DB.Clone()
|
||||||
|
defer db.Close()
|
||||||
|
if err = db.DB("twitter").C("users").Insert(u); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusCreated, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Login(c echo.Context) (err error) {
|
||||||
|
// Bind
|
||||||
|
u := new(model.User)
|
||||||
|
if err = c.Bind(u); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
db := h.DB.Clone()
|
||||||
|
defer db.Close()
|
||||||
|
if err = db.DB("twitter").C("users").
|
||||||
|
Find(bson.M{"email": u.Email, "password": u.Password}).One(u); err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return &echo.HTTPError{Code: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//-----
|
||||||
|
// JWT
|
||||||
|
//-----
|
||||||
|
|
||||||
|
// Create token
|
||||||
|
token := jwt.New(jwt.SigningMethodHS256)
|
||||||
|
|
||||||
|
// Set claims
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
claims["id"] = u.ID
|
||||||
|
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||||
|
|
||||||
|
// Generate encoded token and send it as response
|
||||||
|
u.Token, err = token.SignedString([]byte(Key))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Password = "" // Don't send password
|
||||||
|
return c.JSON(http.StatusOK, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Follow(c echo.Context) (err error) {
|
||||||
|
userID := userIDFromToken(c)
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
// Add a follower to user
|
||||||
|
db := h.DB.Clone()
|
||||||
|
defer db.Close()
|
||||||
|
if err = db.DB("twitter").C("users").
|
||||||
|
UpdateId(bson.ObjectIdHex(id), bson.M{"$addToSet": bson.M{"followers": userID}}); err != nil {
|
||||||
|
if err == mgo.ErrNotFound {
|
||||||
|
return echo.ErrNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func userIDFromToken(c echo.Context) string {
|
||||||
|
user := c.Get("user").(*jwt.Token)
|
||||||
|
claims := user.Claims.(jwt.MapClaims)
|
||||||
|
return claims["id"].(string)
|
||||||
|
}
|
12
recipe/twitter/model/post.go
Normal file
12
recipe/twitter/model/post.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gopkg.in/mgo.v2/bson"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Post struct {
|
||||||
|
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
|
||||||
|
To string `json:"to" bson:"to"`
|
||||||
|
From string `json:"from" bson:"from"`
|
||||||
|
Message string `json:"message" bson:"message"`
|
||||||
|
}
|
||||||
|
)
|
13
recipe/twitter/model/user.go
Normal file
13
recipe/twitter/model/user.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "gopkg.in/mgo.v2/bson"
|
||||||
|
|
||||||
|
type (
|
||||||
|
User struct {
|
||||||
|
ID bson.ObjectId `json:"id" bson:"_id,omitempty"`
|
||||||
|
Email string `json:"email" bson:"email"`
|
||||||
|
Password string `json:"password,omitempty" bson:"password"`
|
||||||
|
Token string `json:"token,omitempty" bson:"-"`
|
||||||
|
Followers []string `json:"followers,omitempty" bson:"followers,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
52
recipe/twitter/server.go
Normal file
52
recipe/twitter/server.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/labstack/echo"
|
||||||
|
"github.com/labstack/echo/middleware"
|
||||||
|
"github.com/labstack/echo/recipe/twitter/handler"
|
||||||
|
"github.com/labstack/gommon/log"
|
||||||
|
mgo "gopkg.in/mgo.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
e := echo.New()
|
||||||
|
e.Logger.SetLevel(log.ERROR)
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
|
||||||
|
SigningKey: []byte(handler.Key),
|
||||||
|
Skipper: func(c echo.Context) bool {
|
||||||
|
// Skip authentication for and signup login requests
|
||||||
|
if c.Path() == "/login" || c.Path() == "/signup" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
db, err := mgo.Dial("localhost")
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indices
|
||||||
|
if err = db.Copy().DB("twitter").C("users").EnsureIndex(mgo.Index{
|
||||||
|
Key: []string{"email"},
|
||||||
|
Unique: true,
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize handler
|
||||||
|
h := &handler.Handler{DB: db}
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
e.POST("/signup", h.Signup)
|
||||||
|
e.POST("/login", h.Login)
|
||||||
|
e.POST("/follow/:id", h.Follow)
|
||||||
|
e.POST("/posts", h.CreatePost)
|
||||||
|
e.GET("/feed", h.FetchPost)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
e.Logger.Fatal(e.Start(":1323"))
|
||||||
|
}
|
202
website/content/recipes/twitter.md
Normal file
202
website/content/recipes/twitter.md
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
+++
|
||||||
|
title = "Twitter API Example"
|
||||||
|
description = "Twitter API example for Echo"
|
||||||
|
[menu.main]
|
||||||
|
name = "Twitter"
|
||||||
|
parent = "recipes"
|
||||||
|
weight = 2
|
||||||
|
+++
|
||||||
|
|
||||||
|
This recipe shows how to create a Twitter like REST API using MongoDB (Database),
|
||||||
|
JWT (API security) and JSON (Data exchange).
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
`user.go`
|
||||||
|
|
||||||
|
{{< embed "twitter/model/user.go" >}}
|
||||||
|
|
||||||
|
`post.go`
|
||||||
|
|
||||||
|
{{< embed "twitter/model/post.go" >}}
|
||||||
|
|
||||||
|
## Handlers
|
||||||
|
|
||||||
|
`handler.go`
|
||||||
|
|
||||||
|
{{< embed "twitter/handler/handler.go" >}}
|
||||||
|
|
||||||
|
`user.go`
|
||||||
|
|
||||||
|
{{< embed "twitter/handler/user.go" >}}
|
||||||
|
|
||||||
|
`post.go`
|
||||||
|
|
||||||
|
{{< embed "twitter/handler/post.go" >}}
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Signup
|
||||||
|
|
||||||
|
User signup
|
||||||
|
|
||||||
|
- Retrieve user credentials from the body and validate against database.
|
||||||
|
- For invalid email or password, send `400 - Bad Request` response.
|
||||||
|
- For valid email and password, save user in database and send `201 - Created` response.
|
||||||
|
-
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
http://localhost:1323/signup \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"jon@labstack.com","password":"shhh!"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
`201 - Created`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"id": "58465b4ea6fe886d3215c6df",
|
||||||
|
"email": "jon@labstack.com",
|
||||||
|
"password": "shhh!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
User login
|
||||||
|
|
||||||
|
- Retrieve user credentials from the body and validate against database.
|
||||||
|
- For invalid credentials, send `401 - Unauthorized` response.
|
||||||
|
- For valid credentials, send `200 - OK` response:
|
||||||
|
- Generate JWT for the user and send it as response.
|
||||||
|
- Each subsequent request must include JWT in the `Authorization` header.
|
||||||
|
|
||||||
|
Method: `POST`<br>
|
||||||
|
Path: `/login`
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
http://localhost:1323/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email":"jon@labstack.com","password":"shhh!"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
`200 - OK`
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"id": "58465b4ea6fe886d3215c6df",
|
||||||
|
"email": "jon@labstack.com",
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Client should store the token, for browsers, you may use local storage.
|
||||||
|
|
||||||
|
### Follow
|
||||||
|
|
||||||
|
Follow a user
|
||||||
|
|
||||||
|
- For invalid token, send `400 - Bad Request` response.
|
||||||
|
- For valid token:
|
||||||
|
- If user is not found, send `404 - Not Found` response.
|
||||||
|
- Add a follower to the specified user in the path parameter and send `200 - OK` response.
|
||||||
|
|
||||||
|
Method: `POST` <br>
|
||||||
|
Path: `/follow/:id`
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
http://localhost:1323/follow/58465b4ea6fe886d3215c6df \
|
||||||
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
`200 - OK`
|
||||||
|
|
||||||
|
### Post
|
||||||
|
|
||||||
|
Post a message to specified user
|
||||||
|
|
||||||
|
- For invalid request payload, send `400 - Bad Request` response.
|
||||||
|
- If user is not found, send `404 - Not Found` response.
|
||||||
|
- Otherwise save post in the database and return it via `201 - Created` response.
|
||||||
|
|
||||||
|
Method: `POST` <br>
|
||||||
|
Path: `/posts`
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl \
|
||||||
|
-X POST \
|
||||||
|
http://localhost:1323/posts \
|
||||||
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"to":"58465b4ea6fe886d3215c6df","message":"hello"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
`201 - Created`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
{
|
||||||
|
"id": "584661b9a6fe8871a3804cba",
|
||||||
|
"to": "58465b4ea6fe886d3215c6df",
|
||||||
|
"from": "58465b4ea6fe886d3215c6df",
|
||||||
|
"message": "hello"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feed
|
||||||
|
|
||||||
|
List most recent messages based on optional `page` and `limit` query parameters
|
||||||
|
|
||||||
|
Method: `GET` <br>
|
||||||
|
Path: `/feed?page=1&limit=5`
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl \
|
||||||
|
-X GET \
|
||||||
|
http://localhost:1323/feed \
|
||||||
|
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODEyNjUxMjgsImlkIjoiNTg0NjViNGVhNmZlODg2ZDMyMTVjNmRmIn0.1IsGGxko1qMCsKkJDQ1NfmrZ945XVC9uZpcvDnKwpL0"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
`200 - OK`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "584661b9a6fe8871a3804cba",
|
||||||
|
"to": "58465b4ea6fe886d3215c6df",
|
||||||
|
"from": "58465b4ea6fe886d3215c6df",
|
||||||
|
"message": "hello"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## [Source Code]({{< source "twitter" >}})
|
||||||
|
|
||||||
|
## Maintainers
|
||||||
|
|
||||||
|
- [vishr](https://github.com/vishr)
|
@ -1,9 +1,9 @@
|
|||||||
{{ partial "head.html" . }}
|
{{ partial "head.html" . }}
|
||||||
<body>
|
<body class="docs">
|
||||||
{{ partial "topnav.html" . }}
|
{{ partial "topnav.html" . }}
|
||||||
<div class="w3-container w3-content">
|
<div class="w3-container w3-content">
|
||||||
{{ partial "sidenav.html" . }}
|
{{ partial "sidenav.html" . }}
|
||||||
<div class="w3-main">
|
<div class="main">
|
||||||
{{ partial "ad.html" }}
|
{{ partial "ad.html" }}
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
<div class="w3-col s12 m10 l10">
|
<div class="w3-col s12 m10 l10">
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
{{ partial "head.html" . }}
|
{{ partial "head.html" . }}
|
||||||
<body>
|
<body class="index">
|
||||||
{{ partial "topnav.html" . }}
|
{{ partial "topnav.html" . }}
|
||||||
<div class="w3-hide-large">
|
<div class="w3-hide-large">
|
||||||
{{ partial "sidenav.html" . }}
|
{{ partial "sidenav.html" . }}
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-container w3-content w3-padding-128">
|
<div class="w3-container w3-content main">
|
||||||
{{ partial "ad.html" }}
|
{{ partial "ad.html" }}
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row">
|
||||||
<div class="w3-col s12 m10 l10">
|
<div class="w3-col s12 m10 l10">
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<h1>{{ .Site.Data.index.heading }}</h1>
|
<h1>{{ .Site.Data.index.heading }}</h1>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{{ partial "head.html" . }}
|
{{ partial "head.html" . }}
|
||||||
<body>
|
<body class="single">
|
||||||
{{ partial "topnav.html" . }}
|
{{ partial "topnav.html" . }}
|
||||||
<div class="w3-container w3-content w3-padding-64">
|
<div class="w3-container w3-content main">
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row">
|
||||||
<div class="w3-col m10 l10">
|
<div class="w3-col m10 l10">
|
||||||
<h1>{{ .Title }}</h1>
|
<h1>{{ .Title }}</h1>
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user