かきスタンプ

福岡でフリーランスの物流系のエンジニアやってます。

Netlify:JAMstack templates を使った、お手軽サイト作成

静的サイトのホスティング先として、かなりメジャーな感じになってきた Netlify

GitHub や Bitbuket のリポジトリを指定するだけでデプロイできてしまう手軽さが魅力ですが、リポジトリが無くても、テンプレートを指定するだけで静的サイトを作れたりします。

テンプレートには、Gatsby を使った爆速テンプレートもあったりします。

作り方

以下、Github アカウントは作成済みで、Netlify アカウントと連携している事を前提としています。

1.

JAMstack project templates にアクセスします。
f:id:kakisoft:20190512221221p:plain

2.

テンプレートを選択します。
f:id:kakisoft:20190512221435p:plain

今回は、「Gatsby + Netlify CMS Starter」を選択しています。
『Deploy to netlify』ボタンを押します。

3.

『Connect to Github』ボタンを押します。
f:id:kakisoft:20190512220850p:plain

4.

「Repository name」をデフォルトから変えたければ変更し、
『Save & Deploy』をボタンを押します。
 
f:id:kakisoft:20190512220912p:plain

5.

Site deploy in progress ...
の状態からしばらく待つと、デプロイが完了します。
URLをクリックすると、デプロイされたページに遷移します。
 
f:id:kakisoft:20190512220941p:plain
今回選択したテーマのデモページ(本家)は、こんな感じです。
 
f:id:kakisoft:20190512221008p:plain

6.

GitHub に、新しいリポジトリが自動で作成されています。
masterブランチを push すると、自動でデプロイされます。  
使い方はテンプレートごとに異なるので、各種 Readme をご参照下さい。  
 
 
 

実際に作ってみたもの

Gatsby developer blog というテンプレートを使って、サイトを作成してみました。
 
福岡市周辺の、公園などの子供が遊ぶ場所の情報をかいてます。
kaki-playground.com

お手軽に使う PHP

『お手軽に使う PHP』というスライドを作成してみました。
 
https://gitpitch.com/kakisoft/UsePHPLightly    
f:id:kakisoft:20190428192416p:plain  
 
PHPは他の軽量言語と比較し、「軽く触ってみる」というケースが少ないんじゃないかと思い、スライドにしてみました。
 
Fukuoka.php Vol.29』 のLT資料として使用しました。

Node.js :csvファイルから jsonファイルへの変換は、convert-csv-to-json がいい感じ。

Node.js は標準で csvを扱うライブラリを持ってないんで、npm で引っ張ってこないといけないみたい。
という訳で、以下を試してみました。

ダウンロード数はそれほど多くないけど、ファイル変換に使うなら、convert-csv-to-json がいい感じ。

<インストール>

npm i convert-csv-to-json

csvファイル → jsonファイルへの変換が、わずか5行。

<変換プログラム例>

let csvToJson = require('convert-csv-to-json');
 
let fileInputName = 'translate_01.csv'; 
let fileOutputName = 'translate_11.json';
 
csvToJson.fieldDelimiter(',');
csvToJson.generateJsonFileFromCsv(fileInputName,fileOutputName);

デリミタ(区切り文字)のデフォルトが「;」となっています。
カンマ区切りの csv を対象とする場合、上記のように fieldDelimiter にて「,」を指定します。
 
詳細はnpmドキュメントをご参照ください。

Visual Studio 2019 Launch Event in Fukuoka振り返り:ハンズオンの Visual Studio 2019 使用バージョン

こちらのイベントに参加させて頂きました。
fukuten.connpass.com

Visual Studio をはじめとした Microsoft の最新情報を届けてくれると同時に、ハンズオンまであるという濃いイベント。
 
ハンズオンにて紹介している資料では、Azure Cloud Shell を使用していますが、せっかくなので、Visual Studio 2019 でやってみました。
 
なお、ハンズオン資料は以下です。
Visual Studio 2019 Launch Event in Fukuoka

ASP.NET Core を使用して Web API を構築する - Learn | Microsoft Docs

1.新しいプロジェクトの作成

f:id:kakisoft:20190416015035p:plain

2.ASP.NET Core Webアプリケーション

f:id:kakisoft:20190416015046p:plain

3.プロジェクト名を入力

プロジェクト名は、チュートリアル同様「RetailApi」としました。
f:id:kakisoft:20190416015058p:plain

4.「API」を選択

f:id:kakisoft:20190416015112p:plain

5.実行

▶ボタンを押すと、Webサーバが起動します。
f:id:kakisoft:20190416015124p:plain

f:id:kakisoft:20190416015137p:plain

6.フォルダを追加

一旦、Webサーバを停止させます。
フォルダを追加します。RetailApiの階層にて右クリックし、フォルダを追加。

f:id:kakisoft:20190416015155p:plain

7.

以下の3つのフォルダを作成します。

  • Controllers
  • Data
  • Models

f:id:kakisoft:20190416015215p:plain

8.ファイルを追加

ファイルを追加します。
追加する階層にて右クリックし、「新しい項目」を選択。

f:id:kakisoft:20190416015227p:plain

9.

コードファイルを選択し、作成。

f:id:kakisoft:20190416015237p:plain

10.

最終的にこんな感じになります。

f:id:kakisoft:20190416015247p:plain

追加・編集するコードは以下のようになります。

Controllers/ProductsController.cs

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RetailApi.Data;
using RetailApi.Models;

namespace RetailApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ProductsContext _context;

        public ProductsController(ProductsContext context)
        {
            _context = context;
        }

        [HttpGet]
        public ActionResult<List<Product>> GetAll() =>
            _context.Products.ToList();

        // GET by ID action
        [HttpGet("{id}")]
        public async Task<ActionResult<Product>> GetById(long id)
        {
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            return product;
        }

        // POST action
        [HttpPost]
        public async Task<ActionResult<Product>> Create(Product product)
        {
            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
        }

        // PUT action
        [HttpPut("{id}")]
        public async Task<IActionResult> Update(long id, Product product)
        {
            if (id != product.Id)
            {
                return BadRequest();
            }

            _context.Entry(product).State = EntityState.Modified;
            await _context.SaveChangesAsync();

            return NoContent();
        }

        // DELETE action
        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(long id)
        {
            var product = await _context.Products.FindAsync(id);

            if (product == null)
            {
                return NotFound();
            }

            _context.Products.Remove(product);
            await _context.SaveChangesAsync();

            return NoContent();
        }

    }
}

Controllers/ValuesController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace RetailApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Data/ProductsContext.cs

using Microsoft.EntityFrameworkCore;
using RetailApi.Models;

namespace RetailApi.Data
{
    public class ProductsContext : DbContext
    {
        public ProductsContext(DbContextOptions<ProductsContext> options)
            : base(options)
        {
        }

        public DbSet<Product> Products { get; set; }
    }
}

Data/SeedData.cs

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RetailApi.Models;

namespace RetailApi.Data
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new ProductsContext(serviceProvider
                .GetRequiredService<DbContextOptions<ProductsContext>>()))
            {
                if (!context.Products.Any())
                {
                    context.Products.AddRange(
                        new Product { Name = "Squeaky Bone", Price = 20.99m },
                        new Product { Name = "Knotted Rope", Price = 12.99m }
                    );

                    context.SaveChanges();
                }
            }
        }
    }
}   

Models/Product.cs

using System.ComponentModel.DataAnnotations;

namespace RetailApi.Models
{
    public class Product
    {
        public long Id { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        [Range(minimum: 0.01, maximum: (double)decimal.MaxValue)]
        public decimal Price { get; set; }
    }
}

Program.cs

using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RetailApi.Data;

namespace RetailApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateWebHostBuilder(args).Build();
            SeedDatabase(host);
            host.Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();

        private static void SeedDatabase(IWebHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;

                try
                {
                    var context = services.GetRequiredService<ProductsContext>();
                    context.Database.EnsureCreated();
                    SeedData.Initialize(services);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "A database seeding error occurred.");
                }
            }
        }
    }
}

Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.EntityFrameworkCore;
using RetailApi.Data;

namespace RetailApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ProductsContext>(options =>
                options.UseInMemoryDatabase("Products"));

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseMvc();
        }
    }
}

ソースの詳細については、公式サイトをご参照ください。

docs.microsoft.com

Apache:mod_rewrite モジュールの、『RewriteCond %{HTTPS} off』がよく分からなかったから調べた。

.htaccess の記述設定で

RewriteCond %{HTTPS} off
RewriteCond %{HTTPS} !on

といった記述があった。

が、RewriteCond の構文は、

RewriteCond TestString CondPattern(正規表現) [flags]

となっている。 https://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritecond

え? 「off」って正規表現じゃなくね? って思ってマニュアルをよく調べたら、こんな記述があった。

If the TestString has the special value expr, the CondPattern will be treated as an ap_expr. HTTP headers referenced in the expression will be added to the Vary header if the novary flag is not given.
 
(TestStringが特別な値だったら、CondPattern は、正規表現でなく ap_exprとして扱われます。 )

って事だった。

という訳で、参照するべきページはここ。 https://httpd.apache.org/docs/current/expr.html

HTTPS  -  "on" if the request uses https, "off" otherwise
(リクエストがhttpsを使用している場合は 'on'、それ以外の場合は 'off')

って事らしい。

なので、常時SSL対応として .htaccess(または httpd.conf)を編集する場合、

RewriteCond %{HTTPS} off

を決まり文句として考えて良さそう。

GitHub に csv・tsv のファイルをアップすると、いい感じに表現してくれる。

タイトル通りです。
特別な事は何もせず、csv や tsv をアップし、ブラウザ上でアップしたファイルを見ると、こんな感じに表現してくれます。

f:id:kakisoft:20190304211256p:plain

フィルタリング機能も付いてたりと、色々と気が利いています。
 
無料ダウンロードできるデータを適当に拾い、リポジトリに上げてみたので、見てみたい方はどうぞ。
csv
tsv
 
 
 

ただ、あんまり大きなデータは表現してくれないみたいです。

この機能についての、GitHubのリファレンスはこちらです。

PHP:処理結果をログに吐いて動作確認。(LAMPならどんな環境でも多分OK)

Linux + PHP + Apache で開発していて、特定の処理をログに吐いて動作確認したい場合、設定の自由度の高さゆえ、サービスで設定しているログの出力先を解析するのが面倒臭いケースも多々あるかと思います。
 
加えて、ディストリビューションやバージョンの違いによる差異もあるし。
 
そんな場合、一時的な出力先を作成して、ログを吐くのが楽なんじゃないかと思います。
具体的には以下のような操作。LAMPなら、多分どんなディストリやバージョンでも行けます。
 

サーバ側の設定

  • 一時的なログ出力先のフォルダとファイルを作成
  • ファイルのオーナーを apache に変更
  • ファイルの権限を変更

具体的なコマンド

sudo mkdir /test01
sudo touch testlog01

sudo chown apache:apache /test01
sudo chown apache:apache /test01/testlog01

sudo chmod 777 /test01
sudo chmod 777 /test01/testlog01

PHP側の記述

こんな感じ。

error_log($message,"3","/test01/testlog01");

第2引数に「3」を渡すと、ログファイルのパスを指定できます。
 
 
Vagrant を使ってる場合、特に設定を変えていなければ、suパスワードは「vagrant」です。